This complete guide covers all 12 Testing tutorials on TheCodeForge, organised by topic.
Every flaky test in a browser suite is the same bug: the test assumed the page was ready and the page was not. Selenium calls the result StaleElementReferenceException. Cypress calls it element detached from the DOM. Playwright calls it a timeout. Three tools, one mistake — a reference captured at one moment and used at another, across a boundary where a framework re-rendered the node.
Which is why the single highest-value skill in this track is not learning a tool's API but learning to wait on the right condition. sleep(2) is a bet that the app will be ready in two seconds; it fails on a slow CI runner and wastes two seconds on every other run. Waiting on a state — this element is visible, this request has completed, this text has changed — is both faster and deterministic.
The tools in this track sit at different points in a clear evolution, and knowing which problem each was built to solve explains their error messages.
| Tool | How it talks to the browser | What it fixed, and what it did not |
|---|---|---|
| Selenium / WebDriver | HTTP commands to a per-browser driver process | Cross-browser standardisation. Every command is a separate round trip, so a node can change between finding it and clicking it — hence stale element references and mandatory explicit waits |
| Cypress | Runs inside the browser, in the same event loop as the app | Automatic retry-ability and excellent debuggability. The in-browser position creates its own limits around cross-origin navigation and multiple tabs |
| Playwright | A persistent bidirectional protocol connection per browser | Auto-waiting on actionability, real multi-origin and multi-tab support, parallel isolated contexts. Strict-mode locator errors are the cost of it refusing to guess |
None of them removes the need to understand what you are waiting for. They differ in how much of that waiting they do on your behalf, and in how loudly they complain when your intent is ambiguous.
A WebElement is a handle to a specific DOM node. If the framework re-renders — React reconciling, Angular running change detection, a Vue list re-keying — the old node is discarded and your handle points at something no longer in the document. Selenium raises StaleElementReferenceException on the next use.
The fix is not to retry the click; it is to stop holding the reference across a state change. Find the element as late as possible, and where the framework re-renders on every keystroke, re-find it per interaction.
ElementClickInterceptedException is a different animal and often misread as staleness: the element exists and is visible, but something else is on top of it — a sticky header, a cookie banner, a toast, a modal backdrop mid-fade. Waiting for the overlay to disappear is the fix; scrolling or forcing the click hides a real user-facing problem.Strict mode violation: locator resolved to 3 elements means your selector is ambiguous and Playwright refuses to pick one. Selenium would have silently taken the first match, which is exactly how a test ends up asserting on the wrong row and passing for months.
So the correct response is to make the intent precise rather than to suppress the check. Playwright's locator API is built for this: scope to a container, filter by text, or name the element the way a user would.
A fixed wait is a guess about someone else's machine. On a fast developer laptop it is dead time; on a loaded CI runner with four suites in parallel it is too short, and the test fails for a reason that has nothing to do with the code under test. That is the definition of a flaky test, and flaky tests get muted, and muted tests stop protecting anything.
Every framework gives you a way to wait on the actual condition. Waiting on a network response is usually the strongest signal, because it ties the test to the app's real readiness rather than to a visual side effect of it.
data-test attribute and select on it. CSS classes and DOM structure are implementation details that change with every redesign; a test attribute is a contract you chose to keep. The half hour spent adding them pays for itself the first time someone refactors the markup.storageState, Cypress's session caching — and it removes the slowest, flakiest step from every test. Reserve a real UI login for the one test whose subject is the login flow itself.data-test attributes to the elements those tests touch. Those two changes address the majority of real flakiness. Retries are worth adding afterwards, but retrying first is how a genuine race condition gets hidden until it reaches production.Every tutorial starts with a plain-English analogy — then real code, then interview questions.
Browse Testing Tutorials →