Yesterday sharpened what you assert; today decides when you are allowed to assert it. An unreliable test passes today and fails tomorrow with no code change. One is annoying. Ten, and the team stops trusting the suite, and a suite nobody trusts is dead even if it still runs every night. I have debugged more of these than I care to remember, and the single biggest cause is waiting done wrong. Tumble Kitchen is built to teach the fix: its product feed takes about 700 milliseconds, on purpose.
The wrong way, and why it feels right
The wrong way is a fixed sleep, a guess about time, usually paired with a one-shot read:
// The classic mistake. Do not do this.
test('search shows jerk dishes', async ({ page }) => {
const store = new TumbleKitchenPage(page);
await store.goto();
await store.searchFor('jerk');
await page.waitForTimeout(400);
const text = await store.firstCard().textContent();
expect(text).toContain('Jerk');
});
Type it in and run it. At 400 milliseconds the feed has not answered yet, the screen still shows the old catalogue with the Deluxe Plantain Plate first, and the one-shot read grabs that stale text. The test fails while nothing is broken. Now change 400 to 2000 and it passes, slowly, today. On a busier day, 2000 loses too.
One honest aside: the retrying assertions from Day 6 quietly rescue many short sleeps, because toContainText keeps re-asking for a few seconds. That rescue is why people believe sleeps work. The rescue has a deadline too, and a slow day does not care about your deadline. A fixed sleep is a guess about the future, and guesses lose eventually. Every flaky suite I have rescued was full of them; tearing them out was the single biggest fix.
The right way: wait on the real signal
Ask what the test is actually waiting for. Not time. The app fires a request for products, the server answers, the screen updates. So wait on that exact request. The search call carries the query in its address, which lets you name it precisely:
test('verify that searching for jerk finds the Jerk Chicken Plate', async ({ page }) => {
const store = new TumbleKitchenPage(page);
await store.goto();
const feed = page.waitForResponse((r) => r.url().includes('?q=jerk') && r.ok());
await store.searchFor('jerk');
await feed;
await expect(store.firstCard()).toContainText('Jerk Chicken Plate');
});
Read the order: start listening, act, then wait on what you heard. Fast day, it waits 700 milliseconds. Slow day, it waits what the slow day takes. And if the request fails or never fires, the test fails at the wait, pointing straight at the real problem. Matching ?q=jerk matters: the page also fetches the feed once on load, and a looser match could hear that first call instead of yours.
Cypress makes the same pattern explicit with an alias, a name you give the request so you can wait on it by name. It is worth knowing in both dialects because you will meet both:
// Cypress: the same wait, spoken with an alias
cy.intercept({ method: 'GET', pathname: '**/api/products.json', query: { q: 'jerk' } }).as('feed');
cy.get('[data-testid="search-input"]').type('jerk');
cy.get('[data-testid="search-submit"]').click();
cy.wait('@feed');
cy.get('[data-testid="product-card"]').first().should('contain.text', 'Jerk');
The order is the part people get wrong: intercept first, act second, wait third. Start listening after the click and the call fires unheard, and the wait times out mysteriously.
Bake the wait into the page object
On a real suite I run, the rule that keeps this clean: the waiting lives inside the page object action, never in the test. The action is not “type and click”, it is “search and wait for the results”:
async searchAndWaitForResults(term) {
const feed = this.page.waitForResponse((r) =>
r.url().includes('?q=' + term) && r.ok()
);
await this.searchFor(term);
await feed;
}
Now no test can forget to wait, because waiting is part of what the action means. That one convention removes a whole class of flake from a suite.
Never wait for time. Wait for the thing you are actually waiting for: the request, the element, the state change.
Do this today
Do the full arc at the keyboard: write the fixed-sleep version, watch it fail against the slow feed, pad it, feel how wrong the padding is, then convert it to waitForResponse and watch it pass at honest speed. Finish by adding searchAndWaitForResults to your page object. Tomorrow: test data, and tests that refuse to lean on each other.
