QA Interview Hub · Knowledge base · Quizzes

Cypress

Extra questions from common lists

hooks, assertions, Shadow DOM, debugging

What hooks does Cypress have and how do they work?

Hooks come from Mocha: before runs once before all the tests of a block, beforeEach before every test, afterEach after each one, after once after all. Hooks in cypress/support/e2e.ts apply globally to all specs.

A tip from the Cypress team itself: it is better to do cleanup in beforeEach than in afterEach. If a test failed or you stopped it in the Test Runner, afterEach may not run, and the next test starts with a dirty state.

describe('Cart', () => {
  beforeEach(() => {
    cy.request('DELETE', '/api/cart');   // a clean state BEFORE the test
    cy.login();
    cy.visit('/cart');
  });

  it('empty cart', () => {
    cy.contains('Your cart is empty').should('be.visible');
  });
});
In an interview: Hooks come from Mocha: before and after run once per block, beforeEach and afterEach for every test. I do state preparation and cleanup in beforeEach, because afterEach may not run if the test failed, and then the next one starts with a dirty state.
And in Playwright:

There are the same hooks (test.beforeEach, test.afterAll…), but preparation and cleanup are usually moved into fixtures: the code after await use() runs even when the test fails.

What types of assertions does Cypress support?

  • Chai BDD: expect(x).to.eq(), .to.include(), .to.have.length().
  • Chai TDD: assert.equal(), assert.isTrue().
  • Chai-jQuery: assertions of DOM state: be.visible, have.class, have.text, be.checked.
  • Sinon-Chai: for spies and stubs: be.called, calledWith.

Two styles of use: implicit through .should('have.text', 'X'), which is retried; explicit through expect inside .then, which runs once, while inside .should(cb) it is retried.

In an interview: Cypress uses Chai in the BDD and TDD styles, Chai-jQuery for checking DOM state and Sinon-Chai for stubs and spies. More important than the list is the difference between implicit assertions through should, which are retried, and explicit expect in then, which run once.

How do you work with Shadow DOM?

By default cy.get doesn't go inside a shadow root. Two ways:

  • Locally: the command .shadow(), which moves into the element's shadow root.
  • Globally: includeShadowDom: true in the config, then all queries pass through shadow boundaries.
cy.get('payment-widget').shadow().find('[data-cy=pay]').click();

// or in cypress.config.ts
// e2e: { includeShadowDom: true }
In an interview: By default Cypress doesn't see the contents of a shadow root. Locally I go in with the shadow command, and if there are many components with Shadow DOM, I turn on includeShadowDom in the config.
And in Playwright:

In Playwright locators pass through open Shadow DOM by default: nothing needs to be turned on.

How do you do hover and drag-and-drop?

Cypress has no native hover: .trigger('mouseover') fires a JS event, but not the CSS :hover state. If a menu is shown only through CSS, .invoke('show') or the cypress-real-events plugin, which generates real browser events, will help.

Drag-and-drop is a chain of events through .trigger. If a library works on the HTML5 drag API, dragstart, drop and dataTransfer are needed, and here a plugin is often used too.

cy.get('[data-cy=menu]').trigger('mouseover');

cy.get('[data-cy=card]')
  .trigger('mousedown', { button: 0 })
  .trigger('mousemove', { clientX: 400, clientY: 200 })
  .trigger('mouseup');
In an interview: Hover and drag-and-drop are done through trigger with the needed mouse events. But trigger fires only JS events, not the real CSS hover state, so for such cases I use the cypress-real-events plugin.
And in Playwright:

Built in and through real browser events: locator.hover() and locator.dragTo(target).

How does cy.exec differ from cy.task?

Both commands run something outside the browser, in the Node process.

  • cy.exec('command') runs a shell command: a database seeding script, a migration, npm run. It returns the exit code, stdout and stderr.
  • cy.task('name') calls a Node function that you registered in setupNodeEvents: a DB query through a library, working with files, generating data. It returns any value.

The rule of choice: there is a ready script, use cy.exec; you need logic and returning data into the test, use cy.task.

cy.exec('npm run db:seed').its('exitCode').should('eq', 0);
cy.task('db:query', 'SELECT count(*) FROM pets').its('0.count').should('eq', '5');
In an interview: Both work in Node. cy.exec runs a shell command, for example a database seeding script, and returns the exit code and the output. cy.task calls a Node function that I registered in the config and returns data into the test, which is how I make database queries.
And in Playwright:

Equivalents aren't needed: the test is in Node anyway, so a shell command is run through child_process, and the DB is called directly.

What Node events does Cypress have, for example after:run?

These are hooks of the whole run level, not of a test. They are registered in setupNodeEvents and run in Node:

  • before:run / after:run are before and after the whole cypress run; after:run receives the results.
  • before:spec / after:spec are around each spec file; after:spec is often used to delete videos of tests that passed.
  • before:browser:launch configures the browser: flags, extensions, window size.
  • after:screenshot processes or renames a screenshot.
setupNodeEvents(on) {
  on('after:run', (results) => {
    if ('totalFailed' in results) {
      console.log('Tests failed:', results.totalFailed);
    }
  });
}
In an interview: These are run-level events that I register in setupNodeEvents. After:run fires after the whole run and receives the results, which is handy for reports or notifications. After:spec fires after each file, and before:browser:launch lets you configure the browser.
And in Playwright:

The equivalents are globalSetup / globalTeardown in the config or a setup project; run results are in your own reporter through onEnd.

How do you control the order in which specs run?

Through specPattern as an array of paths in the needed order, or through --spec in the CLI. But the right part of the answer is to explain that test dependence on order is itself a problem: such tests break with parallel splitting, rerunning a single test, or retries.

If a scenario needs data from a previous step, it is prepared through the API in beforeEach, not through another test.

specPattern: [
  'cypress/e2e/login.cy.ts',
  'cypress/e2e/checkout.cy.ts',
],
In an interview: Technically the order can be set with an array in specPattern. But I try to make tests independent of order, because such tests break with parallel runs and repeats. I create the needed data through the API before every test.
And in Playwright:

The same principles, but by default the order is even less predictable because of workers; for deliberately sequential scenarios there is test.describe.configure({ mode: 'serial' }).

Which browsers and operating systems does Cypress support?

Browsers: the Chrome family (Chrome, Chromium, Edge, the built-in Electron), Firefox and WebKit (the Safari engine), the latter experimentally. The last three major versions are officially supported. Internet Explorer, Opera and the real Safari are not, and mobile browsers and native apps aren't either.

OS: Windows, macOS and Linux with current versions; Node.js, the supported LTS versions. Check the exact minimum versions in the documentation before an interview: they change with every major release.

In an interview: Cypress works with browsers of the Chrome family, including Edge and Electron, with Firefox and experimentally with WebKit. There is no real Safari, no mobile browsers and no native apps, which is one of the main limitations compared to Playwright or Appium.
And in Playwright:

Playwright supports Chromium, Firefox and WebKit fully, plus mobile device emulation, but also without native mobile apps.

How do you debug tests in Cypress?

  • Time travel: in cypress open hover over a command in the Command Log and you will see a DOM snapshot at that moment.
  • cy.pause() stops execution; then you can step through commands.
  • .debug() stops in DevTools with the current subject (DevTools must be open).
  • cy.log() writes to the Command Log; cy.task('log', ...) to the CI terminal.
  • A click on a command in the Command Log prints details to the console: the subject, elements, the request and response.
  • On CI: screenshots, video (if turned on) and Test Replay in Cloud.
cy.get('[data-cy=total]').debug();
cy.pause();
cy.get('[data-cy=checkout]').click();
In an interview: Locally I use time travel in the Command Log, cy.pause to stop the test and go step by step, and debug to look at the subject in DevTools. A click on a command in the log shows all the details in the console. On CI I look at screenshots, video and the logs that I print through cy.task.
And in Playwright:

The equivalents: page.pause(), npx playwright test --debug with the inspector, UI mode (--ui) and Trace Viewer for CI.

How do you work with URLs, navigation and browser history?

  • cy.url() is the full URL as a string.
  • cy.location() is an object with pathname, search, hash; convenient for checking parts.
  • cy.go('back') / cy.go('forward') are the back and forward buttons.
  • cy.reload() reloads the page.

All of them are queries or commands with retries, so assertions on the URL wait for navigation themselves.

cy.location('pathname').should('eq', '/statistics');
cy.location('search').should('include', 'period=30d');
cy.go('back');
cy.url().should('include', '/dashboard');
In an interview: For URLs there are cy.url and cy.location, the second is more convenient because you can check the path or parameters separately. For history there is cy.go back and forward, and for refreshing cy.reload. Assertions on the URL are retried, so they wait for navigation themselves.
And in Playwright:

await expect(page).toHaveURL(/statistics/), page.goBack(), page.goForward(), page.reload().