QA Interview Hub · Knowledge base · Quizzes

Cypress

Framework architecture

POM, commands, config, cy.task

How do you create a custom command and type it in TypeScript?

Commands are registered in cypress/support/commands.ts through Cypress.Commands.add. For TypeScript to know about the new command, you extend the Chainable interface.

// cypress/support/commands.ts
Cypress.Commands.add('getByCy', (id: string) => {
  return cy.get('[data-cy=' + id + ']');
});

declare global {
  namespace Cypress {
    interface Chainable {
      getByCy(id: string): Chainable<JQuery<HTMLElement>>;
      login(role?: string): Chainable<void>;
    }
  }
}

// the test
cy.getByCy('submit').click();
In an interview: I register custom commands in support through Cypress.Commands.add. For TypeScript to suggest and check them, I extend the global Chainable interface in the Cypress namespace. Typical examples are getByCy for data attributes and login.
And in Playwright:

There are no custom commands. Their role is played by fixtures through test.extend<MyFixtures>() (typed with a generic), helper functions and Page Object methods.

Page Object or App Actions: which do you choose?

Page Object is classes with methods that wrap cy commands for a specific page. A test reads like a scenario, and the locators are in one place.

App Actions is an approach promoted by the Cypress team: instead of clicks through the UI, control the application's state directly, through the API or functions exposed on window. This is faster and more stable for preparing state.

In practice they are combined: Page Object for the actions the test verifies, the API for preparation.

export class StatisticsPage {
  private chart = '[data-cy=stats-chart]';
  private period = '[data-cy=period-select]';

  visit() {
    cy.visit('/statistics');
    return this;
  }

  selectPeriod(value: string) {
    cy.get(this.period).select(value);
    return this;
  }

  chartShould(...args: Parameters<Cypress.Chainable['should']>) {
    return cy.get(this.chart).should(...args);
  }
}

// the test
new StatisticsPage().visit().selectPeriod('30d').chartShould('be.visible');
In an interview: I use Page Object: classes with methods for each page, so the locators are collected in one place and a test reads like a scenario. The Cypress team also suggests App Actions: controlling the application's state directly, without the UI. I combine both: I prepare state through the API, and I use Page Object for the actions the test really verifies. This combination is a typical approach in modern frameworks.
And in Playwright:

A Page Object with async methods, and pages are often passed into a test through fixtures, without new in every test. App Actions can be done through page.evaluate() or the API.

What are fixtures in Cypress? Are they the same as in Playwright?

No, and it is easy to get confused here. In Cypress a fixture is simply a file with test data in the cypress/fixtures folder, loaded through cy.fixture() or used as a response in cy.intercept.

In Playwright a fixture is a dependency injection mechanism that prepares resources (a page, a context, a logged-in user) and cleans them up after the test. The closest equivalent in Cypress is hooks plus custom commands.

cy.fixture('users/admin.json').then((admin) => {
  cy.get('[data-cy=email]').type(admin.email);
});

cy.intercept('GET', '/api/stats', { fixture: 'stats-empty.json' });
In an interview: In Cypress a fixture is simply a file with test data, which I load through cy.fixture or use as a response in intercept. In Playwright it is something completely different: a dependency injection mechanism that prepares and cleans up resources. So if I am asked about fixtures, I clarify which tool is meant.
And in Playwright:

Here a fixture is a dependency injection mechanism with setup and teardown. Test data from JSON is simply imported or read through fs.

How do you configure different environments and environment variables?

Variables are available through Cypress.env('NAME'). Sources: the file cypress.env.json (not committed), OS environment variables with the CYPRESS_ prefix, the --env flag, the env block in the config.

For several environments, in setupNodeEvents you read the environment name and set the matching baseUrl. On CI secrets are passed through the pipeline's secrets.

// CI: CYPRESS_USER_PASSWORD=*** npx cypress run --env envName=staging

// cypress.config.ts
setupNodeEvents(on, config) {
  const urls = {
    dev: 'https://dev.example.com',
    staging: 'https://staging.example.com',
  };
  config.baseUrl = urls[config.env.envName || 'dev'];
  return config;
}
In an interview: I read variables through Cypress.env. Locally they live in cypress.env.json, which isn't committed, and on CI they are passed as environment variables with the CYPRESS_ prefix from the pipeline's secrets. I pick the environment with the env flag, and in setupNodeEvents I set the right baseUrl.
And in Playwright:

Variables are ordinary process.env, often together with dotenv. Environments are set through use.baseURL or separate projects.

What are cy.task and setupNodeEvents?

The test code runs in the browser, so it can't reach a database, the file system or Node libraries. cy.task passes the call to the Node process, where tasks are registered in setupNodeEvents.

A task must return a value or a Promise. If it returns nothing, you must return null, otherwise Cypress will give an error.

// cypress.config.ts
import { Client } from 'pg';

setupNodeEvents(on) {
  on('task', {
    async 'db:query'(sql) {
      const client = new Client({ connectionString: process.env.DB_URL });
      await client.connect();
      const res = await client.query(sql);
      await client.end();
      return res.rows;
    },
    log(message) {
      console.log(message);
      return null;
    },
  });
}

// the test
cy.task('db:query', "SELECT status FROM orders WHERE id = 1042")
  .its('0.status').should('eq', 'paid');
In an interview: Since the test runs in the browser, it can't reach the database or files directly. For this there is cy.task: I register a task in setupNodeEvents, it runs in Node and returns the result into the test. For example, I check in PostgreSQL that an order got the right status. A task must return something, at least null.
And in Playwright:

An equivalent isn't needed: the test runs in Node anyway, so you import the PostgreSQL client straight into the test or a fixture.

Fixtures in Cypress vs Playwright

In Cypress fixtures are static files with test data, usually JSON, in the cypress/fixtures folder. They are loaded through cy.fixture('user.json') or passed to cy.intercept as a response body. Essentially this is just a data store.

In Playwright fixtures are a dependency injection mechanism: functions that prepare the environment before a test and clean it up after. They have test and worker scopes, can depend on each other and are passed into a test as parameters. The built-in fixtures are page, context, browser, request.

The equivalent of Playwright fixtures in Cypress is the beforeEach and before hooks, custom commands and cy.session. This is a frequent point of confusion, so it is worth clarifying at once in which sense the term is used.

Page Object vs App Actions

A Page Object is a class that encapsulates a page's selectors and actions, so the test works with methods like loginPage.login(user). It gives reuse and one place to change selectors. The downside in Cypress is an extra layer of abstraction over chainable commands and the temptation to do everything through the UI.

App Actions is an approach recommended by the Cypress team: the application's state is prepared directly through its internal functions, available through window, or through the API. For example, instead of clicks on a task creation form, the test calls an application function and immediately gets the needed state. This is faster and more stable, but requires access to the application's code.

In practice I would combine them: a Page Object or modules with functions for UI interactions, and data preparation through the API and custom commands. Through the UI we go only through what is actually being tested.