Almost every developer I have taught Cypress to gets caught by this in their first hour. It is not your fault. Cypress works differently from normal JavaScript, and nobody tells you at the start.
Here is the surprise. This code looks like it finds an element:
const button = cy.get('[data-testid="login-button"]');
button.click(); // not what you think
It does not. cy.get does not search the page when that line runs. It adds a command to a queue, and Cypress runs the queue in order after your test function finishes. button is not the DOM element. It is a handle on the queue.
This is the one mental model that explains almost everything else about Cypress:
- Commands queue, then run in order.
cy.visit('/'),cy.get(...),.click(). Each line adds a step. Cypress executes them one by one, waiting for each to succeed. - You chain, you don't await.
await cy.get(...)does not work, because cy commands are not promises. To use a value a command produces, chain.then():
cy.get('[data-testid="cart-count"]').then((el) => {
// here el is the real element
});
- Cypress runs inside the browser, next to your app. The same window, the same run loop. That is why it can see the DOM directly and retry commands cheaply. (Playwright and Selenium sit outside the browser and send it commands; Cypress lives inside. Neither is "better", but they behave differently because of it.)
The two most common beginner mistakes both come from this: storing a command's result in a variable, and using async/await with cy commands. Both feel completely natural if you know JavaScript. Both go against the queue.
So write your tests as a chain of steps, top to bottom, and let Cypress run them. When you need a real value, .then() is how you get inside.
The official introduction walks through this model with diagrams: Introduction to Cypress.
No problem for this one. It is the mental model the rest of the track stands on. The next lecture writes your first real test.