If you take one lecture from this track, take this one. More beginner hours are lost here than anywhere else, and the fix is a single change in how you think.
Here is the situation. You click save. The app sends a request to the server, waits for the answer, and then shows a green message. That takes maybe 700 milliseconds.
Your test clicks save and checks for the message in about 5 milliseconds. The message is not there yet. The test fails, and the app is perfectly fine.
So you do the obvious thing:
click(saveButton);
sleep(1000); // wait one second
expectText('Contact created');
It passes. Please do not do this, and here is why it is worse than it looks.
A fixed sleep is a guess about how long something takes, and you lose the guess in both directions.
Too short, and the test fails for no real reason. Your laptop is fast and idle. A build server is slower and running twenty things at once. The pause that always worked locally will fail there, and it will fail intermittently, which is the hardest kind of failure to diagnose.
Too long, and you pay forever. One extra second in eighty tests adds more than a minute to every run, every day, for the life of the suite.
⚠️ This is the critical beginner challenge.
A test that fails sometimes and passes sometimes, with no change to the app, is called flaky. Flaky tests are worse than no tests. People start re-running the suite until it goes green, and once a team learns to ignore red, a real bug walks straight through.
Nearly every flaky test comes from timing, and nearly every timing bug comes from waiting for a duration instead of a condition.
The fix is one sentence. Wait for the thing you actually want, not for time.
click(saveButton);
expectText('Contact created'); // keeps checking until it appears
Every modern framework can do this. It checks for the message again and again, many times a second, until it appears or a timeout is reached. If the save takes 200ms, the test continues after 200ms. If it takes 3 seconds, it waits 3 seconds. Fast when the app is fast, patient when it is slow.
The frameworks differ in how much of this they do for you, and that is one of the biggest differences between them:
- Cypress retries commands automatically. You rarely wait on purpose.
- Playwright waits automatically before every action and in its assertions.
- Selenium makes you ask. You write an explicit wait for a condition, and skipping it is the classic Selenium bug.
You will learn your framework's version in its own track. The idea is the same in all three, and so is the mistake.
One more thing worth knowing now: waiting for the network is not the same as waiting for the screen. The request finishing does not mean the app has drawn the result. When you can, wait for what the user would see.
Remember this: wait for a condition, never for a number.
Compare all three approaches side by side in waiting in Cypress, Playwright and Selenium.