Selectors, Dialogs and Controls That Only Look Like Controls

Automating a GUI you can read but cannot change: the command queue and retry-ability, building selectors that survive a redesign, finding what a control actually listens for in Chrome DevTools, driving a div that pretends to be a checkbox, and handling native dialogs and application modals correctly.

Subject: Cypress Test Automation · 65 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Cypress: Selectors, Dialogs and Fake Controls

Title

Session 1

Automating a GUI you can read but cannot change

2. What this session gives you

Objectives

You are writing against someone else's framework, on a machine where the app source is not visible. So everything here works from the rendered DOM and the DevTools panels, which is all you actually have.

Cypress Documentation — the reference for every command in this deck

3. How Cypress Actually Runs

Section

Section 1

4. Start with what is already biting you

Warm-up

Two minutes before any explanation.

Discussion prompt

Have you written a test where a value you assigned came out undefined, or where a click seemed to happen before the page was ready? Describe one.

Hint: When exactly did your assignment statement run, relative to the click?

Answer:

Both symptoms have the same cause: your test function finishes running before any Cypress command has executed.

The function body only enqueues work. Cypress runs the queue afterwards. Everything confusing about Cypress traces back to that one fact.

Cypress Documentation — the Introduction to Cypress guide states this explicitly

5. Commands are enqueued, not executed

Concept

Writing a cy command does not do anything. It appends an instruction to a queue. When your test function returns, Cypress starts working through that queue.

command queue — The ordered list of instructions your test body builds. Cypress executes it after the body has finished running.

Cypress Documentation — Introduction to Cypress

6. Two passes, not one

Picture it

Your code runs first and enqueues. Cypress runs second and executes.

Figure (svg): A pipeline of four Cypress commands being enqueued, with a note that the test body finishes before any of them execute

The gap between enqueue time and run time is the source of most Cypress confusion.

7. Which is why a plain variable is empty

Concept

The classic first bug. The assignment runs during the enqueue pass, when nothing has been fetched yet.

let count = 0;

cy.get('tbody tr').then(($rows) => {
  count = $rows.length;   // runs later, during the queue
});

cy.log(count);            // enqueued now, but reads the old value

// works: keep the value inside the callback, or use an alias
cy.get('tbody tr').its('length').as('rowCount');
cy.get('@rowCount').should('eq', 3);
linewhen it runsvalue of count
let count = 0enqueue pass0
cy.get(...).then(...)enqueued0
cy.log(count)enqueued, argument evaluated now0
the then callback bodyqueue pass3

Cypress Documentation — Variables and Aliases

8. What does that log print?

Prediction

Three rows exist in the table.

Predict first

What number appears in the Cypress log?

  • 3
  • 0
  • undefined
  • It throws an error

Correct: 0.

Why: The argument to cy.log was evaluated during the enqueue pass, when count was still 0. The then callback did set it to 3, but that happened later, during the queue pass, and the log had already captured the old value.

9. Queries retry; actions do not

Concept

Cypress retries the last query in a chain until the assertion after it passes, or the timeout runs out. The default is four seconds.

Actions and callbacks do not retry. Neither does a value you already pulled out of the DOM. This is the rule that decides where waiting belongs.

Cypress Documentation — Retry-ability

10. Where the retrying happens

Picture it

Left column retries. Right column fires once and moves on.

Figure (svg): Two panels listing Cypress commands that retry, such as get, contains, find and should, against commands that run once, such as click, type, then and wait

Waiting belongs in an assertion, never in a fixed sleep.

This is why a well-written Cypress test almost never needs an explicit wait: the assertion is the wait.

11. Trap: waiting with a fixed number of milliseconds

Trap

The trap

The modal takes a moment to appear, so the test sleeps.

Annotate

  • A guess about how long the app takes, frozen into the test.
  • This assertion would have waited on its own, so the sleep adds nothing but time.
machinemodal appears afterresult
your laptop300mspasses, wastes 1.7s
loaded CI runner2400msfails
after a slow deploy3100msfails

Ship a test that is both slow and flaky

Why: The fixed wait is simultaneously too long on a fast machine and too short on a slow one.

The fix

Let the assertion do the waiting.

cy.get('[data-cy=delete]').click();
cy.get('[data-cy=confirm-modal]').should('be.visible');
machinemodal appears afterresult
your laptop300mspasses in 300ms
loaded CI runner2400mspasses in 2.4s
after a slow deploy3100mspasses in 3.1s

Get a test that is as fast as the app and as patient as it needs to be

Why: The assertion retries until it is true, so the test takes exactly as long as reality requires.

12. One of these actually waits correctly

Elimination

The row status changes from pending to healthy some time after a click.

Eliminate the wrong options

Which line waits properly?

  • A. cy.get('[data-cy=row-7]').should('contain', 'healthy')
  • B. cy.wait(3000); cy.get('[data-cy=row-7]').should('contain', 'healthy')
  • C. cy.get('[data-cy=row-7]').then(($el) => expect($el.text()).to.contain('healthy'))
  • D. cy.get('[data-cy=row-7]').invoke('text').should('contain', 'healthy')

Survives elimination: A

Why: The should assertion makes Cypress re-run the preceding query until the text matches or the timeout expires. That is the whole waiting mechanism, and reaching past it is what makes suites flaky.

13. Pattern: the shape of a reliable command

Pattern

Every command in this deck follows the same three-part shape.

  1. Query for the element with a selector that describes what it is, not where it sits.
  2. Assert something about it, which is what gives Cypress permission to retry.
  3. Act on it, once you know it is really there and really ready.

Query, assert, act. If a test is flaky, the missing piece is almost always the middle one.

Cypress Documentation — Retry-ability and Best Practices

14. Check: where does the waiting go?

Check

Solve it on paper before you click.

Check your understanding

A button becomes enabled a second or two after the page loads. Which line handles that correctly?

  • A. cy.get('[data-cy=save]').should('be.enabled').click() (correct)
  • B. cy.wait(2000).get('[data-cy=save]').click()
  • C. cy.get('[data-cy=save]').click({ force: true })
  • D. cy.get('[data-cy=save]').then(($b) => $b.click())

Answer: A

Why: The assertion retries the query until the button is enabled, then the click happens on an element that is genuinely ready. The waiting and the readiness check are the same line.

Why B tempts people
A fixed sleep is a guess. It is too long when the app is fast and too short when it is slow, and it hides the real timing from you.
Why C tempts people
Forcing the click bypasses the readiness check entirely. The click lands on a disabled button and the app ignores it, so the test passes while doing nothing.
Why D tempts people
Calling the underlying jQuery click skips Cypress's own event simulation and its retry logic, and it will not wait for the button to become enabled.

15. Finding Things in the DOM

Section

Section 2

16. Select by what a thing is, not where it sits

Concept

A selector copied from DevTools describes a position in the tree. Add a row above it and the selector points at something else.

A selector built from a dedicated test attribute describes identity. It survives restyling, reordering and refactoring, because none of those change what the element is.

Cypress Best Practices — selecting elements, and why UI-coupled selectors break — the Cypress team's own ranking of selector strategies

17. Selector stability, ranked

Picture it

Longer bar means it survives more kinds of change.

Figure (svg): Five bars ranking selector strategies by stability, with data-cy attributes highest and nth-child paths lowest

The bottom two are what the Copy Selector menu item gives you.

If the team will not add test attributes, role and accessible name is the next best thing, and it has the side benefit of failing when accessibility breaks.

18. Which of these selectors will survive a restyle?

Sorting

The team is about to migrate the CSS framework.

Sort into buckets

Stable, or brittle?

stable
[data-cy=asset-row]; [role=checkbox][aria-label='Select relay-07']; #asset-table
brittle
.MuiTableRow-root:nth-child(3); div > div > table > tbody > tr:nth-child(2) > td
stable
These name the element by purpose or identity. A test attribute, an accessible role with a name, or a real id all keep pointing at the same thing after a redesign.
brittle
These name a position or a generated class. Adding a row, wrapping a div, or switching component libraries silently repoints them at the wrong element or at nothing.

Note the third one: it is stable and it doubles as an accessibility check, which is often the easiest way to get test attributes approved.

19. Searching the Elements panel

Concept

Open DevTools, focus the Elements panel, and press the find shortcut. The box accepts a plain string, a CSS selector, or an XPath expression, and it walks the matches.

This is the fastest way to answer the only question that matters before writing a command: does my selector match exactly one thing?

Chrome DevTools — Console Utilities API reference — Chrome's DevTools reference for the panels and the console helpers

20. Confirming the count in the Console

Concept

The Console has selector helpers built in. They are the quickest possible check that a selector is neither empty nor over-broad.

$$('[role=checkbox]').length          // 3
$$('[data-cy=asset-row]').length      // 3
$0                                     // the node selected in Elements
getEventListeners($0)                  // what the app is listening for
helperwhat it returnsuse it to
$$(selector)an array of matching nodescheck the count before writing cy.get
$(selector)the first matching nodegrab one node to poke at
$0the node currently selected in Elementschain off whatever you just inspected
getEventListeners($0)the listeners attached to that nodelearn which event actually drives the control

The last one is the important one for this session, and it is Chrome-only.

Chrome DevTools — Console Utilities API reference — the full list of console utilities

21. The five-step DevTools routine

Picture it

This is the loop you run for every unfamiliar control.

Figure (svg): A five step vertical flow from inspecting the control, through searching the Elements panel and confirming the count in the Console, to reading the listeners and writing the command

Never write a cy command before step three.

22. Worked example: from a click in Elements to a confirmed selector

Worked example

You need the control in the first cell of the row for relay-07.

Inspect it, then read upwards

Why: The node itself may be anonymous. Its ancestors usually are not, and the row almost always carries an identifier.

Search the Elements panel for a candidate

Why: Type the candidate selector into the find box and read the match counter.

// in the Console, confirm what the panel showed
$$('tr[data-row-id] [role=checkbox]').length   // 3, one per row
$$('[data-row-id="relay-07"] [role=checkbox]').length   // 1
candidate selectormatchesverdict
[role=checkbox]3too broad on its own
tr [role=checkbox]3same three, no narrower
[data-row-id=relay-07] [role=checkbox]1exactly one, and named by data

Write it as a cy command only now

Why: One match confirmed in the live DOM means the command cannot be ambiguous.

Verify: that the count is exactly one

Why: Zero means the selector is wrong. More than one means Cypress will throw as soon as you try to act on it, because actions require a single element.

23. The match counter is the whole answer

Picture it

Type the candidate into the Elements find box and read the number on the right.

Figure (svg): The DevTools Elements panel showing a DOM tree with three highlighted matches for the selector role equals checkbox and a counter reading three of three

Three matches means the selector is too broad to act on.

Narrow it until the counter says one of one. Only then write the cy command.

24. Which tool answers which question?

Discrimination

Matching the question to the panel saves most of the time you currently spend hunting.

Sort into buckets

Elements panel, or Console?

Elements panel
What is the full ancestor chain above this control?; What changes in the DOM when I press Space?
Console
Does my selector match exactly one node?; Which events does this element listen for?
elem
The tree view and the breakpoints live here. Reading ancestors is a scroll, and a break-on-attribute-modification breakpoint shows you exactly what a keystroke changed.
cons
Anything you want a count or an object back from. The selector helpers and the listener inspector both return values you can read.

25. Check: the selector matched four things

Check

Solve it on paper before you click.

Check your understanding

Your selector matches four elements and you call .click() on it. What happens?

  • A. Cypress throws, because an action requires exactly one element (correct)
  • B. Cypress clicks all four in order
  • C. Cypress clicks the first one
  • D. Cypress clicks the last one

Answer: A

Why: Action commands require a single subject. Cypress raises an error naming the count, which is a much better failure than silently picking one. Use .first(), .eq(n) or a narrower selector to resolve it.

Why B tempts people
Cypress deliberately does not do this, because acting on an unknown number of elements makes a test whose meaning depends on the data.
Why C tempts people
That is what jQuery would do, and the silent guessing is exactly the behaviour Cypress refuses to inherit.
Why D tempts people
Nothing in Cypress prefers the last match. If you want it, ask for it with .last().

26. The Control That Is Not a Checkbox

Section

Section 3

27. A styled div wearing a checkbox costume

Concept

Component libraries frequently render an interactive control as a plain element carrying a role attribute, a tabindex, and an aria-checked state. It looks like a checkbox and it behaves like one for a keyboard user, but it is not an input.

That means it has no checked property, no change event, and nothing for Cypress's own check command to work with.

W3C WAI-ARIA Authoring Practices Guide — the Checkbox pattern Checkbox Pattern — the spec these components are implementing

28. What you see

Picture it

Three rows, each with a box in the first column. The middle one has keyboard focus.

Figure (svg): A browser window showing a table of three assets, each row beginning with a checkbox-styled control, with the second row focused

Looks like three checkboxes. Is not three checkboxes.

29. What is actually there

Picture it

The same control, in the DOM.

Figure (svg): A DOM tree from table down through tbody and a row with a data-row-id attribute to a table cell containing a div with role checkbox

A div with a role attribute. No input element anywhere in the chain.

That highlighted node is what every command in this section targets.

30. What does cy.check() do here?

Prediction

The control is a div with role of checkbox.

Predict first

What happens when you call cy.check() on it?

  • It toggles the control
  • Cypress throws, because check only works on inputs
  • It silently does nothing
  • It clicks the element instead

Correct: Cypress throws.

Why: The check command is defined only for input elements of type checkbox or radio. Given anything else it raises an error saying so. That error is helpful: it tells you immediately that you are dealing with a custom control rather than leaving you to wonder why nothing toggled.

31. Find out what the app is listening for

Concept

Do not guess between click, keydown, keyup and keypress. Ask the DOM.

// select the control in Elements, then in the Console:
getEventListeners($0)

// a typical answer for a keyboard-driven custom control:
// { keydown: [ { useCapture: false, ... } ],
//   focus:   [ ... ],
//   blur:    [ ... ] }
// note: no click listener at all
listener presentwhat to sendcypress command
clicka real click.click()
keydown onlya keydown carrying the key.trigger('keydown', { key: ' ' })
keyup onlya keyup.trigger('keyup', { key: ' ' })
none on the nodecheck the ancestors: the row may be delegatingquery the ancestor instead

If the listener list is empty, the handler is bound higher up and the events are bubbling to it. Walk up the tree and check the row.

Chrome DevTools — Console Utilities API reference — getEventListeners is a Chrome console utility, not a JavaScript API

32. Worked example: toggling the control with the keyboard

Worked example

The listeners show keydown and nothing else, so a click will never reach the handler.

Query the row by its data attribute, then the control inside it

Why: The row identifier is the stable part; the control is found relative to it.

Focus the control first

Why: The application's handler is on the control, so it has to be the active element for a keyboard event to make sense.

Send the exact event the listener expects

Why: The trigger command dispatches a real DOM event with whatever properties you give it.

cy.get('[data-row-id="relay-07"]')
  .find('[role=checkbox]')
  .should('have.attr', 'aria-checked', 'false')
  .focus()
  .trigger('keydown', { key: ' ', code: 'Space', keyCode: 32, which: 32 })
  .should('have.attr', 'aria-checked', 'true');
stepstate beforestate after
query and assertaria-checked is falseunchanged, but confirmed
focusnot the active elementis the active element
trigger keydownaria-checked is falsehandler runs
asserthandler has runaria-checked is true

Verify: with the aria-checked assertion at the end

Why: This is the only real proof the toggle worked. A passing trigger proves an event was dispatched, not that anything listened to it.

33. Step through what each command changed

Invariant

Four commands, and only one of them touches the application state.

Step through it

Which single frame would look different if the app removed its keydown listener?

  1. The element exists and reports itself unchecked.
  2. Focus moves to it. Application state is still untouched.
  3. The keydown reaches the handler and the state attribute flips.
  4. The assertion reads the new state and the test passes for a real reason.

Only the third. Everything else would carry on exactly as before, which is precisely why the final assertion is not optional.

34. Why does this pass but change nothing?

Error analysis

A first attempt at the same control. It goes green, and it is worthless.

Annotate

  • first() picks whichever row happens to be first, so the test does not say which asset it toggled.
  • trigger dispatches the event and reports success whether or not anything handled it.
  • Nothing checks aria-checked, so the test would keep passing after the feature broke.

The fix is two lines: name the row, and assert the state afterwards.

This is the most dangerous failure mode in UI automation: a green test that verifies nothing, quietly protecting nothing at all.

35. Trap: forcing a click at a control that ignores clicks

Trap

The trap

Clicking did nothing, so the next instinct is to force it.

Annotate

  • force skips the actionability checks, so nothing can report that the click was pointless.
  • And with no assertion afterwards, nothing checks the state either.
what happenedwhat the test reported
a click event was dispatchedpassed
no listener existed for clickpassed
aria-checked never changedpassed

Ship a test that can never fail

Why: Forcing removes the checks that would have told you the click was pointless.

The fix

Find the event the app listens for, send that, and assert the state.

cy.get('[data-row-id="relay-07"]').find('[role=checkbox]')
  .focus()
  .trigger('keydown', { key: ' ', code: 'Space', keyCode: 32 })
  .should('have.attr', 'aria-checked', 'true');
what happenedwhat the test reported
a keydown was dispatchedcontinues
the app's handler rancontinues
aria-checked became truepassed, and meant it

Ship a test that fails when the feature breaks

Why: Which is the only kind worth having.

36. Real input versus custom control

Comparison

Same appearance, different everything. Fill in the gaps.

Comparison matrix

input type=checkboxdiv with role=checkbox
how state is storedthe checked propertythe aria-checked attribute
cypress command.check().trigger() with the right event
assertion.should('be.checked').should('have.attr', 'aria-checked', 'true')
responds to a plain clickalwaysonly if the app added a click listener

When you meet an unfamiliar control, filling in this table for it is the fastest way to know what to write.

37. Why would anyone build it this way?

Socratic

Worth understanding, because it tells you what else to expect from the same codebase.

Discussion prompt

Why do component libraries replace a native checkbox with a styled div, and what does that choice cost them?

Hint: What can you do to a div that you cannot do to a native checkbox?

Answer:

Because native form controls are famously hard to style consistently across browsers. Replacing them buys complete visual control.

What it costs is everything the native element gave away for free: keyboard handling, focus behaviour, the accessibility tree, and form submission. The library has to reimplement all of it, and the ARIA attributes are that reimplementation.

For you this predicts the rest of the app. If the checkbox is custom, the dropdowns, tabs and dialogs almost certainly are too, and they will all need the same treatment.

W3C WAI-ARIA Authoring Practices Guide — the Checkbox pattern — the pattern documents exactly which keys and attributes a custom control owes

38. Write the assertion

Fill the middle

The trigger is written. The proof is missing.

Fill in the blanks

cy.get('[role=checkbox]').first()
.trigger('keydown', { key: ' ' })
.should('have.attr', 'aria-checked', 'true');

Why: Space is the toggle key in the ARIA checkbox pattern, and aria-checked is where the state lives. Without the assertion the test proves only that an event was dispatched, which is never the thing you wanted to know.

39. Dialogs: Two Completely Different Things

Section

Section 4

40. One of them is not in the page at all

Concept

A native dialog produced by the browser is chrome, not content. It has no DOM nodes, so no selector can ever reach it.

A modal rendered by the application is ordinary DOM. Every normal command works on it.

The first question about any dialog is therefore which kind it is, and the DevTools Elements panel answers it in one look.

Cypress Documentation — Cypress catalog of events, window:alert and window:confirm

41. Native versus app-rendered

Picture it

They can look nearly identical on screen and behave nothing alike under test.

Figure (svg): Two panels comparing a native browser dialog which is not in the DOM against an application rendered modal which is real DOM

If it appears in the Elements panel, it is yours to query.

42. Cypress already handles the native ones

Concept

Cypress automatically accepts a confirm and dismisses an alert, so a native dialog will not hang your test. What you usually want is to assert on its text.

// assert on the text of a native confirm, which Cypress auto-accepts
cy.on('window:confirm', (text) => {
  expect(text).to.equal('Delete this asset?');
});

cy.get('[data-cy=delete]').click();

// to make it cancel instead, return false
cy.on('window:confirm', () => false);
dialogcypress defaulthow to control it
window.alertdismissed automaticallylisten on window:alert to read the text
window.confirmaccepted automaticallylisten on window:confirm; return false to cancel
window.promptnot handledstub it in onBeforeLoad before the page runs

Cypress Documentation — the window:alert and window:confirm events

43. The test hangs on a prompt. Why?

Prediction

Alerts and confirms work fine, but a page that calls window.prompt freezes the run.

Predict first

What is different about prompt?

  • Cypress handles it but slowly
  • Cypress does not handle prompt automatically
  • Prompts are not allowed in tests
  • The prompt is in an iframe

Correct: Cypress does not handle window.prompt automatically.

Why: Alerts are dismissed and confirms are accepted for you, but prompt needs a return value that Cypress cannot invent. You stub it on the window object before the application code runs, using the onBeforeLoad hook of cy.visit.

44. Worked example: stubbing a prompt

Worked example

The page asks for a reason before deleting. Cypress will not answer that for you.

Stub the function before the app can call it

Why: The onBeforeLoad hook runs after the window exists and before any page script executes, which is the only window in which this works.

Give the stub the value the test needs

Why: Now the application receives a real answer and carries on.

cy.visit('/assets', {
  onBeforeLoad(win) {
    cy.stub(win, 'prompt').returns('decommissioned').as('prompt');
  },
});

cy.get('[data-cy=delete]').click();
cy.get('@prompt').should('have.been.calledWith', 'Reason for deletion?');
momentwindow.prompt iseffect
before onBeforeLoadthe real browser functionwould block the run
after onBeforeLoada stub returning a stringreturns immediately
when the app calls itthe stubapp receives the reason
at the assertionthe stub, with recorded callsarguments can be checked

Verify: by asserting on the stub's arguments

Why: Aliasing the stub means you can prove the application asked the question you expected, not just that it asked something.

45. The one moment the stub can be installed

Picture it

Before the window exists there is nothing to stub. After the page scripts run it is too late.

Figure (svg): A timeline from cy.visit through window creation and the onBeforeLoad hook to the page scripts running, with the stub window marked

onBeforeLoad is that window, and it is the only one.

The same hook is how you stub anything else the app reads off the window at startup: a feature flag, a clock, a geolocation call.

46. App modals are just elements

Concept

For a modal rendered by the application, there is nothing special to learn. Query it, assert it is visible, act inside it.

cy.get('[data-cy=delete]').click();

cy.get('[data-cy=confirm-modal]')
  .should('be.visible')
  .within(() => {
    cy.contains('Delete this asset?');
    cy.get('[data-cy=confirm]').click();
  });

cy.get('[data-cy=confirm-modal]').should('not.exist');
linewhy it is there
should be.visiblethe retry window that waits for the animation to finish
withinscopes every command inside it to the modal, so a stray match elsewhere cannot be picked up
containsasserts the message, which is usually the actual requirement
should not.existproves the modal closed, which is the half people forget

Cypress Documentation — the within command

47. Which statement about modals is right?

Two truths and a lie

Three claims about the app-rendered modal above.

Eliminate the wrong options

Which one holds?

  • A. Asserting should('not.exist') after confirming is worth a line, because a modal that never closes is a real bug.
  • B. You need cy.wait before querying the modal so the animation can finish.
  • C. cy.on('window:confirm') will catch an app-rendered modal too.
  • D. Using within is optional and makes no difference.

Survives elimination: A

Why: Checking that the modal disappeared is the assertion most people skip, and modals that stay open after confirming are one of the most common real defects in this kind of interface.

48. Match the dialog to the technique

Matching

Four situations you will hit in this app.

Match the pairs

  • l1. a native confirm you want to accept
  • l2. a native confirm you want to cancel
  • l3. a native prompt needing a value
  • l4. an application modal
  • r1. do nothing; Cypress accepts it for you
  • r2. cy.on('window:confirm', () => false)
  • r3. cy.stub on the window in onBeforeLoad
  • r4. cy.get, should be.visible, and within

Why: Three of the four need no selector at all, because native dialogs are not part of the page. Only the last one is ordinary DOM work, and that is exactly why telling the two kinds apart is the first step.

49. Waiting on the Network

Section

Section 5

50. Sometimes the DOM is not the thing you are waiting for

Concept

Assertions cover most waiting. The exception is when you need to know that a request finished, or what it returned, rather than what it eventually drew.

Intercept names the request, and then you can wait on that name instead of on a duration.

Cypress Documentation — the intercept command

51. Intercept, alias, wait

Concept

Three lines, and the fixed wait disappears from the spec entirely.

cy.intercept('GET', '/api/assets').as('assets');
cy.visit('/assets');

cy.wait('@assets').its('response.statusCode').should('eq', 200);
cy.get('[data-cy=asset-row]').should('have.length', 3);
linewhat it doeswhy not a sleep
intercept and asnames the request before it is madethe name is stable; the duration is not
visitloads the page, firing the request-
wait on the aliasblocks until that request completesas fast as the network actually is
its response.statusCodereads the real responsea sleep could never tell you this
should have.lengthasserts what got renderedretries on top of the completed request

Note the ordering: the intercept has to be registered before the request is made, so it goes above the visit.

Cypress Documentation — intercept and aliases

52. Why does this intercept never fire?

Prediction

A test loads the page first and registers the intercept afterwards.

Predict first

What happens to the wait on that alias?

  • It resolves immediately
  • It times out, because the request already happened
  • It catches the next matching request
  • Cypress reorders it automatically

Correct: It times out.

Why: An intercept only sees requests made after it is registered. Putting it below the visit means the request fired unwatched, and the wait sits there until the timeout looking for a request that will never come again.

53. Three ways to drive a stubborn control

Trade off

Not one right answer. Fill in what each one costs.

Comparison matrix

approachwhat it gives youwhat it costs
click()the most realistic interactiondoes nothing if there is no click listener
trigger()exact control over the event and its propertiesit is a synthetic event, so it can pass where a real user would fail
cypress-real-eventsgenuine browser-level input through the CDPan extra dependency, and Chromium browsers only

Start with click, because it is honest. Move to trigger when the listeners tell you the app is not listening for clicks. Reach for real events only when a synthetic one demonstrably is not enough.

54. What does the ticket not tell you?

Missing information

The ticket says: automate selecting a row and deleting it, and verify the confirmation.

Discussion prompt

Before writing a line, what three things do you need that the ticket does not say?

Hint: Every one of them is a question about the DOM, not about the requirement.

Answer:

Which event the row control listens for, and therefore whether a click will do anything at all.

Whether the confirmation is a native browser dialog or an application modal, because the two need completely different code.

What identifies the row and the control stably, since a copied selector will break the first time the table changes.

All three are answerable in about two minutes in DevTools, and none of them is answerable by reading the ticket again.

55. Explain retry-ability to the newest person on the team

Explain it

Two sentences, out loud.

Discussion prompt

Why does a Cypress test almost never need an explicit wait?

Hint: What is Cypress doing between the moment a query fails and the moment it gives up?

Answer:

Because an assertion makes Cypress re-run the query in front of it, over and over, until the assertion passes or the timeout expires.

So the assertion is the wait. Adding a sleep on top of it only makes the test slower on fast machines without making it any safer on slow ones.

Cypress Documentation — Retry-ability

56. How sure are you about this one?

Commit first

Answer, then rate your confidence. Confident and wrong is the pair worth finding before it costs you a day.

Predict first

A test asserts should('not.exist') on a modal immediately after clicking confirm. The modal has a 400ms close animation. Does the test pass?

  • Yes, the assertion retries through the animation
  • No, it fails because the modal is still there
  • Only if you add a wait
  • It depends on the browser

Correct: Yes.

Why: The not.exist assertion retries just like every other one, so Cypress keeps re-querying for up to the default four seconds and passes as soon as the element is gone. Animations are exactly the case retry-ability was built for.

57. Putting It Together

Section

Section 6

58. The whole spec

Concept

Three tests, covering the three things this session set out to solve.

describe('Asset table', () => {
  beforeEach(() => cy.visit('/assets'));

  it('toggles a row with the keyboard', () => {
    cy.get('[data-row-id="relay-07"]').find('[role=checkbox]')
      .should('have.attr', 'aria-checked', 'false')
      .focus()
      .trigger('keydown', { key: ' ', code: 'Space', keyCode: 32 })
      .should('have.attr', 'aria-checked', 'true');
  });

  it('confirms a native delete dialog', () => {
    cy.on('window:confirm', (t) => expect(t).to.equal('Delete this asset?'));
    cy.get('[data-row-id="relay-07"]').find('[data-cy=delete]').click();
    cy.contains('[data-row-id="relay-07"]').should('not.exist');
  });

  it('waits out the app modal without a fixed wait', () => {
    cy.get('[data-cy=bulk-delete]').click();
    cy.get('[data-cy=confirm-modal]').should('be.visible').within(() => {
      cy.get('[data-cy=confirm]').click();
    });
    cy.get('[data-cy=confirm-modal]').should('not.exist');
  });
});
testthe risk it coversthe assertion that proves it
keyboard togglethe control ignores clicksaria-checked flips to true
native dialogthe confirm text changedthe text equals the expected string
app modalthe modal never closesthe modal no longer exists

Cypress Documentation — every command used here

59. What a passing run looks like

Picture it

Three tests, about a second, no fixed waits anywhere.

Figure (svg): A terminal showing a Cypress run with three passing tests for the asset table completing in one second

No sleeps means the suite runs as fast as the app does.

60. Order the debugging routine

Ranking

The next time a control will not respond, run these in this order.

Put in order

  1. Inspect the element and read its role, tabindex and aria attributes
  2. Confirm the selector matches exactly one node in the Console
  3. List the event listeners on the node and on its ancestors
  4. Send that exact event with trigger
  5. Assert the state attribute changed

Why: Identify, then confirm, then listen, then act, then prove. Most wasted time comes from jumping straight to step four and guessing at the event, which is a search over a space you could have just read.

61. Pattern: the whole session on one card

Pattern

Five rules that cover everything here.

  1. Commands enqueue; they do not run. Keep values inside callbacks or aliases.
  2. Queries retry, actions do not. Put the waiting in an assertion, never in a sleep.
  3. Confirm the match count in the Console before writing any cy.get.
  4. Read the listeners, do not guess the event. Then send exactly that event with trigger.
  5. Assert the state, not the action. A dispatched event proves nothing; a changed attribute proves everything.
symptomthe cause, nearly always
value is undefinedread outside the callback
flaky on CI, fine locallya fixed wait instead of an assertion
passes but nothing happenedforced click, or no state assertion
cypress throws on the actionthe selector matched more than one node
test hangsan unstubbed prompt

Cypress Best Practices — selecting elements, and why UI-coupled selectors break — the same advice from the people who wrote the tool

62. Check: which line is the actual proof?

Check

Solve it on paper before you click.

Check your understanding

In the keyboard toggle test, which line is the one that would fail if the feature broke?

  • A. the final should('have.attr', 'aria-checked', 'true') (correct)
  • B. the trigger('keydown', ...) call
  • C. the focus() call
  • D. the cy.get for the row

Answer: A

Why: Only the final assertion looks at the outcome. Everything before it describes what the test did, and all of it would keep succeeding on a completely broken control.

Why B tempts people
Trigger dispatches the event and reports success regardless of whether anything handled it. It cannot detect a removed listener.
Why C tempts people
Focus succeeds on any focusable element. It says nothing about whether the toggle works.
Why D tempts people
The get would fail if the row vanished, which is a different bug. A broken toggle leaves the row exactly where it was.

63. Exit ticket

Exit ticket

One honest answer, and it sets the agenda for next time.

Predict first

Which of these is still the murkiest?

  • The command queue and why values come out empty
  • Building selectors that will not break
  • Driving custom controls with trigger
  • Native dialogs versus application modals
  • Reading the DOM in DevTools quickly

Correct: Whichever you named opens the next session.

Why: All five are separable and all five are drillable. If the answer is the last one, that is the best possible use of a session, because it is the skill that makes every other problem in this list solvable without help.

64. Map your own app

Connect it up

The highest-value twenty minutes you can spend before the next session.

Draw it

List every distinct control type in the screen you are testing. Beside each, write its role attribute, the event its listeners actually take, and the attribute that holds its state. Mark the ones you had to guess.

The guesses are the agenda. Bring the list.

65. What you can do now

Recap

Five things, and the third one is the one that will save you the most time this week.

you want towrite
toggle a custom checkboxfocus() then trigger('keydown', { key: ' ' })
prove it toggledshould('have.attr', 'aria-checked', 'true')
read a native confirmcy.on('window:confirm', (t) => ...)
cancel a native confirmcy.on('window:confirm', () => false)
answer a promptcy.stub(win, 'prompt') inside onBeforeLoad
work inside a modalget(modal).should('be.visible').within(...)

Cypress Documentation — keep this open in a tab while you write

Sources

  1. Cypress Documentation
  2. Cypress API reference — .trigger()
  3. Cypress Best Practices — selecting elements, and why UI-coupled selectors break — Cypress.io documentation, Guides > References > Best Practices
  4. W3C WAI-ARIA Authoring Practices Guide — the Checkbox pattern
  5. Chrome DevTools — Console Utilities API reference
  6. MDN Web Docs — KeyboardEvent

Want this taught 1-on-1? Alexander tutors Cypress Test Automation — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108