Software QA / Editorial solution guide
UI tests fail intermittently: synchronize with meaningful state
Diagnose a browser check that sometimes misses a confirmation or reads stale search results. Replace timing guesses with isolated state and bounded, observable conditions.
Read-only knowledge base · No public comments
Recognize the symptoms
The same build sometimes passes and sometimes fails while waiting for a confirmation. A screenshot may show an unfinished spinner, or a search result may belong to the previous query. Preserve the first failure before rerunning; an eventual pass does not explain it.
Understand the likely causes
- Likely causes include a fixed sleep shorter than real completion time, a locator matching the wrong element, shared cookies or data, and responses arriving out of order. Product races and test defects can look alike. Do not classify the failure as “only flaky” without examining what the user experienced.
1. Preserve a small, comparable failure record
Record build, browser, viewport, locale, test role, synthetic record identifiers, and the action/response timeline. Capture a trace or screenshot with private values removed. Compare one failing and one passing run under the same declared setup. Ask whether the requested operation completed and whether the final value matched storage. A missing element can mean a delayed page, a wrong account, an application error, or a locator defect; these need different repairs.
2. Wait for the result the case actually needs
Replace arbitrary delays with a bounded assertion on the confirmation for the requested course. Prefer an accessible role or label that identifies purpose. If the page displays success optimistically, add a read-back or reload observation to establish persistence. The illustrative Playwright snippet assumes a locally prepared page and synthetic course; it is not a complete runnable test. A timeout remains useful evidence and should not be increased indefinitely to conceal an unresolved failure.
await page.getByRole('button', { name: 'Enroll' }).click();
await expect(page.getByRole('status')).toHaveText('Enrollment confirmed: QA lab');
The assertion retries within its configured bound and names the expected business result. A separate check may still be required for durable state.
3. Reproduce ordering instead of adding sleeps
For stale search, arrange local controlled responses: send Q1=data, then Q2=design; return Q2 first and Q1 second. The final results must still belong to design under the agreed latest-query rule. Assert query identity and result identifiers, not merely that a result exists. If the page regresses to data, repair request-version handling in the application. Faster selectors or longer waits cannot make that user-visible race correct.
4. Remove hidden state dependencies
Create a fresh browser context and purpose-built data for the case. Do not inherit login, filters, or records from another test. If failure depends on a preceding case, run that pair in both orders against an isolated store and inspect cleanup. Keep an owner and review date for any temporary quarantine, and preserve its first-failure evidence rather than silently converting retries into a pass.
Verify the fix
Verify the original reproducer alone and with the known triggering neighbor, then rerun the controlled response-order case. Expect stable query identity, one persisted enrollment, and no arbitrary sleeps. Targeted successful runs support the repair; they do not prove that every schedule is safe.
Keep it from recurring
- Design explicit readiness signals, isolated fixtures, and useful failure traces. Review locators when semantics change, and retain a bounded asynchronous case for a repaired race. Treat intermittent product behavior as a user risk until evidence explains it.