The contacts page shows a spinner, then the table appears. Your test opens the page and immediately counts the rows. It finds zero and fails.
Nothing is wrong with the app. Your test arrived during the spinner. This lecture is about the three shapes this takes and what to do about each.
Content that arrives late.
The page loads, then data is fetched, then the list renders. The mistake is checking for a count too early:
cy.visit('/contacts');
cy.get('[data-testid="contact-row"]').should('have.length', 5);
That actually works, because Cypress retries the assertion until it passes or times out. The version that does not work is when you take the value out of Cypress's hands:
cy.get('[data-testid="contact-row"]').then((rows) => {
expect(rows.length).to.equal(5); // runs once, no retry
});
Inside .then() you have a plain value, and plain values do not retry. If the rows were not there yet, this fails permanently. Keep assertions in the chain where Cypress can retry them.
Content that changes in place.
You click "mark as won" and the status cell changes from Open to Won. The row was always there, so nothing appears or disappears. Assert on the new value, not on existence:
cy.get('[data-testid="status-dana-fox"]').should('have.text', 'Won');
Cypress retries this until the text matches. If the app never updates it, the test fails with a clear message showing what it found instead.
Content that disappears.
Toast messages are the classic case. A green "Contact created" message appears and vanishes after three seconds. Two things to know.
Checking that it is gone needs its own assertion:
cy.get('[data-testid="toast"]').should('not.exist');
And more importantly: do not build the rest of your test on a message that vanishes. A test that checks the toast, then does three more things, then checks the toast again will fail on the second check, correctly, because the toast is designed to disappear. Check it once, immediately, then move on to something permanent.
That last point is worth pushing further, because it is the real lesson. A toast tells you the app claims it saved. The row in the table, still there after a reload, tells you it did. When a test can check either, check the durable thing.
Remember this: keep assertions in the chain so they retry, and prefer the permanent result over the temporary message.
Reference: Retry-ability.