flaky(e2e): `thread_resume` intermittently fails on a detached `#chat-submit`
Summary
cypress/e2e/thread_resume/spec.cy.ts fails roughly 20–33% of runs with a detached-DOM error when clicking the send button. It is not tied to any dependency version — it reproduces across react-router-dom 6.30.4, 6.30.5 and 6.30.6 with an identical stack — and it burns ~2 minutes per failure because Cypress retries the spec four times before giving up.
The defect is in the test helper, not in the application.
Error
CypressError: Timed out retrying after 30050ms: `cy.click()` failed because
the page updated while this command was executing. Cypress tried to locate
elements based on this query:
> <button#chat-submit ...>
We initially found matching element(s), but while waiting for them to become
actionable, they disappeared from the page.Root cause
cypress/support/testUtils.ts:16:
export function submitMessage(message: string) {
cy.get('#chat-input')
.should('be.visible')
.should('not.be.disabled')
.type(message);
cy.get('#chat-submit').should('not.be.disabled').click(); // <-- here
}cy.get('#chat-submit') resolves the button once. .should('not.be.disabled') then retries the assertion against that already-resolved subject, and .click() finally acts on it. If React re-renders in the window between the assertion passing and the click landing, the original node is detached and the click fails.
thread_resume is the spec that hits this most often because it is the one that calls submitMessage immediately after cy.reload():
cy.reload(); // triggers thread resume
cy.get('#message-composer').should('be.visible');
submitMessage('still here'); // composer still settlingOn resume the composer remounts as the thread's history streams back in, which is exactly the re-render that detaches the button. Other specs call submitMessage against a settled page, so they rarely trip it.
Evidence
Isolated runs of just this spec, rebuilding between each version change:
react-router-dom |
Result |
|---|---|
| 6.30.4 | 2 pass / 0 fail |
| 6.30.5 | 2 pass / 1 fail |
| 6.30.6 | 4 pass / 1 fail |
Same error signature in every failure. The version is not the variable.
Reproduction
pnpm build
cd backend && uv sync --all-extras && cd ..
# repeat — expect an intermittent failure within a handful of runs
for i in $(seq 1 5); do
pnpm exec cypress run --spec "cypress/e2e/thread_resume/spec.cy.ts"
doneProposed fix
Break the chain so the element is re-queried at click time:
export function submitMessage(message: string) {
cy.get('#chat-input')
.should('be.visible')
.should('not.be.disabled')
.type(message);
cy.get('#chat-submit').should('not.be.disabled');
cy.get('#chat-submit').click();
}This fixes every caller at once, since they all funnel through submitMessage.
If it still proves racy after resume specifically, the sturdier option is to wait for the app to be idle before submitting — e.g. asserting the resumed history has finished rendering in thread_resume before the second submitMessage, rather than relying on #message-composer being visible (which becomes true before the thread finishes hydrating).
Impact
- ~2 minutes of CI time per occurrence (4 retries × 30s timeout)
- Produces red builds that look like real regressions on unrelated PRs — it caused a false "regression" diagnosis while reviewing #2999 before sampling showed it to be version-independent
Notes
Pre-existing on main; not introduced by any recent dependency work.
Source: Chainlit/chainlit