Playwright · Cypress · AI-powered

Stop Googling.
Describe it. Get it.

A curated snippet library for QA automation engineers. Search real-world Playwright and Cypress patterns — or let AI generate exactly what you need.

AI Snippet Generator

Describe what you need — get a working snippet

Snippet Library

137 curated snippets · updated regularly

Playwrightnetwork

Intercept & mock a GET request

Mock a REST API response so your test never hits the real server.

1import { test, expect } from '@playwright/test';
2
3test('shows mocked user data', async ({ page }) => {
4  // Intercept before navigation
5  await page.route('**/api/user', async (route) => {
6    await route.fulfill({
7      status: 200,
8      contentType: 'application/json',
9      body: JSON.stringify({ id: 1, name: 'QA Engineer' }),
10    });
11  });
12
13  await page.goto('/profile');
14  await expect(page.getByText('QA Engineer')).toBeVisible();
15});
#intercept#mock#api#route
Playwrightauth

Login via API and set auth state

Authenticate through the API and reuse the session across tests — no UI login needed.

1import { test, request } from '@playwright/test';
2
3test.beforeAll(async () => {
4  const apiContext = await request.newContext();
5
6  const response = await apiContext.post('/api/login', {
7    data: { email: 'user@test.com', password: 'secret' },
8  });
9
10  // Save auth state (cookies + localStorage) to file
11  await apiContext.storageState({ path: 'auth.json' });
12  await apiContext.dispose();
13});
14
15// In playwright.config.ts, set: storageState: 'auth.json'
16// All tests in this project will reuse the session
#login#session#storageState#api
Playwrightwait

Wait for a network response

Wait for a specific API call to complete before asserting.

1test('waits for API before asserting', async ({ page }) => {
2  await page.goto('/dashboard');
3
4  // Wait for the API call triggered by the page load
5  const response = await page.waitForResponse(
6    (res) => res.url().includes('/api/dashboard') && res.status() === 200
7  );
8
9  const data = await response.json();
10  expect(data.items.length).toBeGreaterThan(0);
11});
#waitForResponse#network#async
Playwrightfile

Upload a file

Trigger a file input and upload a local file.

1test('uploads a file', async ({ page }) => {
2  await page.goto('/upload');
3
4  // Set the file directly on the input — no dialog needed
5  await page.locator('input[type="file"]').setInputFiles('tests/fixtures/sample.pdf');
6
7  await page.getByRole('button', { name: 'Submit' }).click();
8  await expect(page.getByText('Upload successful')).toBeVisible();
9});
#upload#file#input
Playwrightvisual

Screenshot on test failure

Automatically capture a screenshot whenever a test fails.

1// playwright.config.ts
2import { defineConfig } from '@playwright/test';
3
4export default defineConfig({
5  use: {
6    screenshot: 'only-on-failure', // 'on' | 'off' | 'only-on-failure'
7    video: 'retain-on-failure',
8    trace: 'retain-on-failure',
9  },
10});
11
12// Screenshots land in: test-results/<test-name>/screenshot.png
#screenshot#failure#debug
Playwrightfixtures

Custom fixture for logged-in page

Create a reusable fixture that provides a pre-authenticated page to every test.

1import { test as base, expect } from '@playwright/test';
2
3type MyFixtures = { loggedInPage: Page };
4
5export const test = base.extend<MyFixtures>({
6  loggedInPage: async ({ page }, use) => {
7    await page.goto('/login');
8    await page.fill('[name="email"]', 'user@test.com');
9    await page.fill('[name="password"]', 'secret');
10    await page.click('[type="submit"]');
11    await page.waitForURL('/dashboard');
12
13    // Hand the authenticated page to the test
14    await use(page);
15  },
16});
17
18export { expect };
19
20// Usage in any test file:
21// import { test, expect } from './fixtures';
22// test('dashboard loads', async ({ loggedInPage }) => { ... });
#fixture#auth#reusable#extend
Playwrightforms

Select from a dropdown

Select an option by label, value, or index from a native <select> element.

1test('selects an option', async ({ page }) => {
2  await page.goto('/settings');
3
4  const select = page.locator('select[name="country"]');
5
6  // By visible label
7  await select.selectOption({ label: 'Egypt' });
8
9  // By value attribute
10  // await select.selectOption({ value: 'EG' });
11
12  // By index (0-based)
13  // await select.selectOption({ index: 2 });
14
15  await expect(select).toHaveValue('EG');
16});
#select#dropdown#forms
Playwrightassertions

Assert element is visible and contains text

Basic but essential: check an element exists and shows the right content.

1test('element is visible with correct text', async ({ page }) => {
2  await page.goto('/home');
3
4  const heading = page.getByRole('heading', { level: 1 });
5
6  // Playwright auto-waits for the element and retries assertions
7  await expect(heading).toBeVisible();
8  await expect(heading).toHaveText('Welcome back');
9
10  // Partial match
11  await expect(heading).toContainText('Welcome');
12});
#visible#text#toBeVisible#toHaveText
Cypressnetwork

Intercept & stub a POST request

Intercept a POST request and return a stubbed response — no real server needed.

1describe('Form submission', () => {
2  it('shows success message after submit', () => {
3    cy.intercept('POST', '/api/contact', {
4      statusCode: 200,
5      body: { message: 'Sent!' },
6    }).as('submitForm');
7
8    cy.visit('/contact');
9    cy.get('[name="email"]').type('test@test.com');
10    cy.get('[name="message"]').type('Hello');
11    cy.get('[type="submit"]').click();
12
13    cy.wait('@submitForm');
14    cy.contains('Sent!').should('be.visible');
15  });
16});
#intercept#stub#mock#api
Cypressauth

Login via cy.request (skip UI)

Authenticate by hitting the API directly — much faster than typing through the login form.

1// cypress/support/commands.js
2Cypress.Commands.add('loginByApi', (email, password) => {
3  cy.request({
4    method: 'POST',
5    url: '/api/login',
6    body: { email, password },
7  }).then(({ body }) => {
8    // Store the token however your app uses it
9    window.localStorage.setItem('auth_token', body.token);
10  });
11});
12
13// Usage in tests:
14// cy.loginByApi('user@test.com', 'secret');
15// cy.visit('/dashboard'); // already authenticated
#login#api#cy.request#session
Cypresswait

Wait for a network response

Use cy.intercept + cy.wait to pause until an API call completes before asserting.

1it('waits for API before asserting', () => {
2  // Set up the intercept BEFORE the action that triggers it
3  cy.intercept('GET', '/api/items').as('getItems');
4
5  cy.visit('/items');
6
7  // Wait for the aliased request to complete
8  cy.wait('@getItems').then(({ response }) => {
9    expect(response.statusCode).to.eq(200);
10    expect(response.body).to.have.length.greaterThan(0);
11  });
12
13  cy.contains('Item list loaded').should('be.visible');
14});
#wait#intercept#alias#network
Cypressfixtures

Custom command with TypeScript types

Create a typed custom Cypress command and register it so autocomplete works.

1// cypress/support/commands.ts
2Cypress.Commands.add('getByTestId', (testId: string) => {
3  return cy.get(`[data-testid="${testId}"]`);
4});
5
6// cypress/support/index.d.ts
7declare namespace Cypress {
8  interface Chainable {
9    getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
10  }
11}
12
13// Usage:
14// cy.getByTestId('submit-button').click();
15// cy.getByTestId('error-message').should('be.visible');
#custom command#typescript#extend#reusable
Cypressfile

Upload a file with cy.fixture

Upload a file stored in your fixtures folder using selectFile.

1it('uploads a file', () => {
2  cy.visit('/upload');
3
4  // File must be in cypress/fixtures/
5  cy.get('input[type="file"]')
6    .selectFile('cypress/fixtures/sample.pdf');
7
8  cy.get('[type="submit"]').click();
9  cy.contains('Upload successful').should('be.visible');
10});
#upload#fixture#selectFile#file
Cypressassertions

Assert text, visibility, and value

Core assertion patterns you'll use in almost every test.

1it('covers core assertions', () => {
2  cy.visit('/profile');
3
4  // Text content
5  cy.get('h1').should('contain.text', 'Welcome');
6
7  // Visibility
8  cy.get('.error-banner').should('not.exist');
9  cy.get('.success-msg').should('be.visible');
10
11  // Input value
12  cy.get('[name="email"]').should('have.value', 'user@test.com');
13
14  // Attribute
15  cy.get('img.avatar').should('have.attr', 'alt', 'User avatar');
16
17  // CSS class
18  cy.get('.btn-primary').should('have.class', 'active');
19});
#should#contain#visible#value#assert
Cypressnavigation

Navigate and assert URL

Go to a page and verify the resulting URL.

1it('navigates and checks URL', () => {
2  cy.visit('/');
3
4  cy.get('nav a[href="/about"]').click();
5
6  // Assert full URL
7  cy.url().should('include', '/about');
8
9  // Or check just the pathname
10  cy.location('pathname').should('eq', '/about');
11});
#visit#url#location#navigate
Cypressforms

Select from a dropdown

Select a <select> option by text, value, or index.

1it('selects a dropdown option', () => {
2  cy.visit('/settings');
3
4  // By visible text
5  cy.get('select[name="country"]').select('Egypt');
6
7  // By value
8  // cy.get('select[name="country"]').select('EG');
9
10  // By index (0-based)
11  // cy.get('select[name="country"]').select(2);
12
13  cy.get('select[name="country"]').should('have.value', 'EG');
14});
#select#dropdown#forms
Playwrightauth

Handle OAuth popup / new page

Intercept a new browser tab that opens during OAuth and authenticate inside it.

1test('OAuth login via popup', async ({ page, context }) => {
2  await page.goto('/login');
3
4  // Listen for the new tab BEFORE clicking — race-condition safe
5  const [popup] = await Promise.all([
6    context.waitForEvent('page'),
7    page.getByRole('button', { name: 'Sign in with Google' }).click(),
8  ]);
9
10  await popup.waitForLoadState('domcontentloaded');
11  await popup.fill('[name="email"]', 'user@gmail.com');
12  await popup.fill('[name="password"]', 'secret');
13  await popup.getByRole('button', { name: 'Next' }).click();
14
15  // Wait for OAuth to complete and the popup to close
16  await popup.waitForEvent('close');
17
18  await expect(page.getByText('Welcome')).toBeVisible();
19});
#oauth#popup#new page#multi-tab
Playwrightauth

Simulate token expiry and refresh

Return a 401 on the first API call, then a valid response on the retry to verify token-refresh logic.

1test('app refreshes token on 401', async ({ page }) => {
2  let callCount = 0;
3
4  await page.route('**/api/data', async (route) => {
5    callCount++;
6    if (callCount === 1) {
7      // First call returns Unauthorized
8      await route.fulfill({ status: 401, body: JSON.stringify({ error: 'Unauthorized' }) });
9    } else {
10      // Second call succeeds after the app has refreshed the token
11      await route.fulfill({ status: 200, body: JSON.stringify({ items: ['a', 'b'] }) });
12    }
13  });
14
15  await page.goto('/dashboard');
16
17  await expect(page.getByText('a')).toBeVisible();
18  expect(callCount).toBe(2); // confirm the retry happened
19});
#401#token refresh#intercept#retry
Playwrightauth

Assert redirect for unauthenticated user

Verify that a protected route redirects to /login when no session is present.

1test('redirects unauthenticated user to /login', async ({ page }) => {
2  // Navigate without any auth cookies or localStorage session
3  await page.goto('/dashboard');
4
5  // Playwright waits for navigation to settle before asserting
6  await expect(page).toHaveURL(//login/);
7  await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
8});
#redirect#protected route#unauthenticated#url
Playwrightnetwork

Simulate slow network response

Add artificial latency to an API route to test loading states and spinners.

1test('shows loading spinner during slow API', async ({ page }) => {
2  await page.route('**/api/reports', async (route) => {
3    // Pause 2 seconds before responding
4    await new Promise((resolve) => setTimeout(resolve, 2000));
5    await route.fulfill({ status: 200, body: JSON.stringify({ rows: [] }) });
6  });
7
8  await page.goto('/reports');
9
10  // Spinner visible while waiting for the delayed response
11  await expect(page.getByTestId('loading-spinner')).toBeVisible();
12  await expect(page.getByTestId('loading-spinner')).toBeHidden();
13});
#delay#slow network#loading#intercept
Playwrightnetwork

Block third-party analytics scripts

Abort requests to analytics domains so tests run faster and don't fire real tracking events.

1// Run before each test — add to a beforeEach or global setup
2test.beforeEach(async ({ page }) => {
3  await page.route(
4    /googletagmanager|segment.io|hotjar|mixpanel|intercom/,
5    (route) => route.abort()  // silently drop the request
6  );
7});
8
9test('page loads without analytics noise', async ({ page }) => {
10  await page.goto('/home');
11  await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
12});
#block#analytics#third-party#abort
Playwrightnetwork

Inject custom request headers

Add feature-flag or API-version headers to every outgoing request.

1test('sends feature-flag header with every request', async ({ page }) => {
2  await page.route('**/api/**', async (route) => {
3    await route.continue({
4      headers: {
5        ...route.request().headers(), // preserve existing headers
6        'x-feature-flag': 'new-dashboard',
7        'x-api-version': '2',
8      },
9    });
10  });
11
12  await page.goto('/dashboard');
13  await expect(page.getByTestId('new-dashboard-banner')).toBeVisible();
14});
#headers#request#modify#feature flag
Playwrightassertions

Assert table row count

Count rows in an HTML table and assert the exact number.

1test('table renders the correct number of rows', async ({ page }) => {
2  await page.goto('/users');
3
4  const rows = page.locator('table tbody tr');
5
6  // Playwright retries the assertion until the count matches
7  await expect(rows).toHaveCount(10);
8
9  // Or assert a minimum
10  const count = await rows.count();
11  expect(count).toBeGreaterThanOrEqual(5);
12});
#table#rows#count#toHaveCount
Playwrightassertions

Assert all list items match a condition

Iterate over every matched element using locator.all() and assert each one.

1test('every search result contains the keyword', async ({ page }) => {
2  await page.goto('/search?q=cypress');
3
4  const items = page.locator('[data-testid="result-item"]');
5  await expect(items).not.toHaveCount(0);
6
7  // locator.all() resolves the locator to an array of elements
8  for (const item of await items.all()) {
9    await expect(item).toContainText(/cypress/i);
10  }
11});
#list#all#each#contains#loop
Playwrightassertions

Assert element attributes and CSS

Check href, aria-label, disabled state, and computed CSS on elements.

1test('button and link attributes are correct', async ({ page }) => {
2  await page.goto('/profile');
3
4  const saveBtn = page.getByRole('button', { name: 'Save' });
5  await expect(saveBtn).toHaveAttribute('type', 'submit');
6  await expect(saveBtn).not.toBeDisabled();
7  await expect(saveBtn).toHaveCSS('cursor', 'pointer');
8
9  // ARIA label on an image
10  const avatar = page.getByRole('img', { name: /avatar/i });
11  await expect(avatar).toHaveAttribute('alt', /avatar/i);
12
13  // Data attribute
14  const badge = page.getByTestId('plan-badge');
15  await expect(badge).toHaveAttribute('data-plan', 'pro');
16});
#attribute#aria#disabled#css#toHaveAttribute
Playwrightnavigation

Browser back and forward navigation

Programmatically navigate browser history and assert the URL at each step.

1test('back and forward navigation works', async ({ page }) => {
2  await page.goto('/page-a');
3  await page.getByRole('link', { name: 'Go to B' }).click();
4  await expect(page).toHaveURL(/page-b/);
5
6  await page.goBack();
7  await expect(page).toHaveURL(/page-a/);
8
9  await page.goForward();
10  await expect(page).toHaveURL(/page-b/);
11});
#goBack#goForward#history#url
Playwrightnavigation

Open link in new tab and interact

Capture a new tab opened by target=_blank, interact with it, and close it.

1test('link opens in new tab with correct content', async ({ page, context }) => {
2  await page.goto('/docs');
3
4  const [newPage] = await Promise.all([
5    context.waitForEvent('page'),
6    page.getByRole('link', { name: 'Open Guide' }).click(),
7  ]);
8
9  await newPage.waitForLoadState('domcontentloaded');
10  await expect(newPage).toHaveURL(/guide/);
11  await expect(newPage.getByRole('heading', { level: 1 })).toBeVisible();
12
13  await newPage.close();
14});
#new tab#popup#target blank#context
Playwrightnavigation

Reload page and assert persistence

Reload and confirm that localStorage-backed state (e.g. theme) survives.

1test('theme preference persists after reload', async ({ page }) => {
2  await page.goto('/settings');
3  await page.getByRole('button', { name: 'Dark mode' }).click();
4  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
5
6  // reload() keeps cookies and localStorage intact
7  await page.reload();
8
9  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
10});
#reload#refresh#localStorage#persist
Playwrightforms

Check and uncheck a checkbox

Toggle a checkbox on and off, asserting state after each interaction.

1test('checkbox toggles correctly', async ({ page }) => {
2  await page.goto('/preferences');
3
4  const checkbox = page.getByRole('checkbox', { name: 'Email notifications' });
5
6  await expect(checkbox).not.toBeChecked();
7
8  await checkbox.check();
9  await expect(checkbox).toBeChecked();
10
11  await checkbox.uncheck();
12  await expect(checkbox).not.toBeChecked();
13});
#checkbox#check#uncheck#isChecked
Playwrightforms

Select a radio button

Pick a radio option by accessible name and confirm mutually exclusive selection.

1test('selects express shipping radio', async ({ page }) => {
2  await page.goto('/checkout');
3
4  await page.getByRole('radio', { name: 'Express shipping' }).check();
5  await expect(page.getByRole('radio', { name: 'Express shipping' })).toBeChecked();
6
7  // Standard option should be deselected — radios are mutually exclusive
8  await expect(page.getByRole('radio', { name: 'Standard shipping' })).not.toBeChecked();
9});
#radio#check#getByRole#exclusive
Playwrightforms

Fill a date input

Set a value on a native <input type='date'> using ISO format.

1test('sets a date on a date input', async ({ page }) => {
2  await page.goto('/booking');
3
4  const dateInput = page.locator('input[type="date"][name="check-in"]');
5
6  // Native date inputs require ISO 8601 format: YYYY-MM-DD
7  await dateInput.fill('2025-08-15');
8  await expect(dateInput).toHaveValue('2025-08-15');
9
10  await page.getByRole('button', { name: 'Continue' }).click();
11  await expect(page.getByText('Aug 15, 2025')).toBeVisible();
12});
#date#input#fill#ISO
Playwrightforms

Clear a field and retype

Clear an existing input value and type a replacement — the edit-form pattern.

1test('clears and updates the display name', async ({ page }) => {
2  await page.goto('/edit-profile');
3
4  const nameInput = page.getByLabel('Display name');
5
6  // Triple-click selects all, then fill replaces the selection
7  await nameInput.click({ clickCount: 3 });
8  await nameInput.fill('Jane Doe');
9  await expect(nameInput).toHaveValue('Jane Doe');
10
11  // Alternative: explicit clear then fill
12  // await nameInput.clear();
13  // await nameInput.fill('Jane Doe');
14});
#clear#fill#triple-click#retype
Playwrightfixtures

Parametrize tests with a data loop

Run the same assertion logic for multiple roles using a for...of loop over test data.

1import { test, expect } from '@playwright/test';
2
3const roles = [
4  { name: 'admin',  email: 'admin@test.com',  canDelete: true  },
5  { name: 'editor', email: 'editor@test.com', canDelete: false },
6  { name: 'viewer', email: 'viewer@test.com', canDelete: false },
7];
8
9for (const { name, email, canDelete } of roles) {
10  test(`${name} sees correct actions`, async ({ page }) => {
11    await page.goto(`/login?email=${email}`);
12
13    const deleteBtn = page.getByRole('button', { name: 'Delete' });
14
15    if (canDelete) {
16      await expect(deleteBtn).toBeVisible();
17    } else {
18      await expect(deleteBtn).not.toBeVisible();
19    }
20  });
21}
#parametrize#data-driven#loop#roles
Playwrightfixtures

Load a JSON fixture from disk

Read a JSON file using Node's fs and use the data to drive test inputs.

1import { test, expect } from '@playwright/test';
2import fs from 'fs';
3import path from 'path';
4
5// tests/fixtures/user.json → { "name": "Alice", "email": "alice@test.com" }
6const user = JSON.parse(
7  fs.readFileSync(path.join(__dirname, 'fixtures/user.json'), 'utf-8')
8);
9
10test('fills form from fixture data', async ({ page }) => {
11  await page.goto('/create-user');
12
13  await page.getByLabel('Name').fill(user.name);
14  await page.getByLabel('Email').fill(user.email);
15  await page.getByRole('button', { name: 'Submit' }).click();
16
17  await expect(page.getByText(`Welcome, ${user.name}`)).toBeVisible();
18});
#fixture#json#readFileSync#data-driven
Playwrightvisual

Full-page screenshot comparison

Capture a full-page screenshot and compare it against a stored baseline.

1test('home page matches visual baseline', async ({ page }) => {
2  await page.goto('/home');
3
4  // Wait for images, fonts, and animations to settle
5  await page.waitForLoadState('networkidle');
6
7  // First run creates the baseline PNG; subsequent runs diff against it
8  await expect(page).toHaveScreenshot('home-page.png', {
9    fullPage: true,
10    maxDiffPixelRatio: 0.02, // tolerate 2% pixel difference (anti-aliasing)
11  });
12});
#screenshot#visual regression#baseline#toHaveScreenshot
Playwrightvisual

Screenshot a single element

Capture a specific component rather than the whole page for focused visual tests.

1test('hero banner matches baseline', async ({ page }) => {
2  await page.goto('/');
3
4  const hero = page.getByTestId('hero-banner');
5  await hero.scrollIntoViewIfNeeded();
6
7  await expect(hero).toHaveScreenshot('hero-banner.png', {
8    maxDiffPixelRatio: 0.01,
9  });
10});
#screenshot#element#component#locator
Playwrightfile

Download a file and verify content

Trigger a download, save it locally, and assert the filename and file content.

1import path from 'path';
2import fs from 'fs';
3
4test('downloads a CSV report', async ({ page }) => {
5  await page.goto('/reports');
6
7  const [download] = await Promise.all([
8    page.waitForEvent('download'),
9    page.getByRole('button', { name: 'Export CSV' }).click(),
10  ]);
11
12  const filePath = path.join(__dirname, 'downloads', download.suggestedFilename());
13  await download.saveAs(filePath);
14
15  expect(download.suggestedFilename()).toMatch(/\.csv$/);
16
17  const content = fs.readFileSync(filePath, 'utf-8');
18  expect(content).toContain('email,name,role'); // assert CSV header row
19});
#download#csv#save#readFileSync
Playwrightwait

Wait for element to disappear

Assert that a loading spinner or toast notification becomes hidden.

1test('spinner disappears after data loads', async ({ page }) => {
2  await page.goto('/dashboard');
3
4  const spinner = page.getByTestId('loading-spinner');
5
6  // Playwright retries until hidden — default timeout 30 s
7  await expect(spinner).toBeHidden();
8
9  // Data table should now be fully rendered
10  await expect(page.getByRole('table')).toBeVisible();
11});
#toBeHidden#disappear#loading#spinner
Playwrightwait

Poll until a JS condition is true

Use waitForFunction to keep polling the DOM until a dynamic condition resolves.

1test('waits until feed has at least 5 items', async ({ page }) => {
2  await page.goto('/live-feed');
3
4  // Evaluate runs inside the browser; Playwright polls until it returns truthy
5  await page.waitForFunction(
6    () => document.querySelectorAll('[data-testid="feed-item"]').length >= 5,
7    { timeout: 15_000, polling: 500 }
8  );
9
10  await expect(page.locator('[data-testid="feed-item"]')).toHaveCount(5);
11});
#waitForFunction#poll#condition#live feed
Playwrightselectors

Select nth element and filter by text

Use nth() to pick by position and filter() to narrow by text content.

1test('targets specific cards by position and text', async ({ page }) => {
2  await page.goto('/products');
3
4  // nth() is 0-indexed — picks the third card
5  const thirdCard = page.locator('[data-testid="product-card"]').nth(2);
6  await expect(thirdCard).toBeVisible();
7
8  // filter() narrows the locator set by inner text
9  const saleCard = page
10    .locator('[data-testid="product-card"]')
11    .filter({ hasText: 'Sale' })
12    .first();
13
14  await expect(saleCard.getByTestId('sale-badge')).toBeVisible();
15});
#nth#filter#hasText#locator
Playwrightselectors

Interact with Shadow DOM elements

Pierce a shadow root to fill inputs inside web components.

1test('fills input inside a shadow DOM component', async ({ page }) => {
2  await page.goto('/web-components-demo');
3
4  // Playwright's locator API automatically pierces open shadow roots
5  const shadowInput = page.locator('my-input-component input');
6  await shadowInput.fill('hello');
7
8  await expect(shadowInput).toHaveValue('hello');
9});
#shadow DOM#web component#pierce#locator
Cypressauth

OAuth cross-origin flow with cy.origin

Handle an OAuth provider on a separate domain using cy.origin.

1it('logs in via Google OAuth', () => {
2  cy.visit('/login');
3  cy.get('[data-testid="google-login"]').click();
4
5  // cy.origin enables commands to run on a different domain
6  cy.origin('https://accounts.google.com', () => {
7    cy.get('input[type="email"]').type('user@gmail.com');
8    cy.get('#identifierNext').click();
9    cy.get('input[type="password"]', { timeout: 8000 }).type('secret');
10    cy.get('#passwordNext').click();
11  });
12
13  cy.url().should('include', '/dashboard');
14  cy.contains('Welcome').should('be.visible');
15});
#oauth#cy.origin#cross-origin#google
Cypressauth

Cache auth session with cy.session

Persist login state across tests so the API login runs once per spec, not per test.

1Cypress.Commands.add('loginWithSession', (email, password) => {
2  cy.session(
3    [email, password], // cache key — change it to invalidate the session
4    () => {
5      cy.request({ method: 'POST', url: '/api/login', body: { email, password } })
6        .then(({ body }) => {
7          window.localStorage.setItem('auth_token', body.token);
8        });
9    },
10    { cacheAcrossSpecs: true } // reuse across multiple spec files
11  );
12});
13
14// Usage in any test:
15// cy.loginWithSession('user@test.com', 'password');
16// cy.visit('/dashboard');
#cy.session#cache#performance#auth
Cypressauth

Assert unauthenticated redirect

Clear session data and confirm that protected routes redirect to /login.

1it('redirects to /login when not authenticated', () => {
2  // Wipe any existing auth state
3  cy.clearLocalStorage();
4  cy.clearCookies();
5
6  cy.visit('/dashboard', { failOnStatusCode: false });
7
8  cy.url().should('include', '/login');
9  cy.get('h1').should('contain.text', 'Sign in');
10});
#redirect#protected#unauthenticated#clearLocalStorage
Cypressnetwork

Simulate slow API response

Delay a network response to test loading spinners and skeleton screens.

1it('shows spinner during slow API response', () => {
2  cy.intercept('GET', '/api/reports', (req) => {
3    req.on('response', (res) => {
4      res.setDelay(2000); // 2-second artificial delay
5    });
6  }).as('slowReports');
7
8  cy.visit('/reports');
9
10  cy.get('[data-testid="spinner"]').should('be.visible');
11  cy.wait('@slowReports');
12  cy.get('[data-testid="spinner"]').should('not.exist');
13});
#delay#loading#intercept#slow network
Cypressnetwork

Block third-party scripts

Destroy analytics/tracking requests globally so they don't pollute test runs.

1// cypress/support/e2e.js — applied before every test
2beforeEach(() => {
3  cy.intercept(/googletagmanager|segment.io|hotjar|intercom/, (req) => {
4    req.destroy(); // abort — prevents real events from firing
5  });
6});
7
8it('page loads without analytics noise', () => {
9  cy.visit('/home');
10  cy.get('h1').should('be.visible');
11});
#block#abort#analytics#third-party
Cypressnetwork

Inject custom request headers

Modify outgoing requests to carry feature-flag or API-version headers.

1it('sends feature-flag header to enable new dashboard', () => {
2  cy.intercept('GET', '/api/**', (req) => {
3    req.headers['x-feature-flag'] = 'new-dashboard';
4    req.headers['x-api-version'] = '2';
5  });
6
7  cy.visit('/dashboard');
8  cy.get('[data-testid="new-dashboard-banner"]').should('be.visible');
9});
#headers#request#intercept#feature flag
Cypressassertions

Assert table row count

Count table rows and assert an exact or minimum number.

1it('table renders the correct number of rows', () => {
2  cy.visit('/users');
3
4  cy.get('table tbody tr').should('have.length', 10);
5
6  // Or assert a minimum
7  cy.get('table tbody tr')
8    .its('length')
9    .should('be.gte', 5);
10});
#table#rows#have.length#count
Cypressassertions

Assert every list item contains text

Use .each() to iterate over results and assert each one matches a condition.

1it('every search result mentions the keyword', () => {
2  cy.visit('/search?q=playwright');
3
4  cy.get('[data-testid="result-item"]')
5    .should('have.length.greaterThan', 0)
6    .each(($el) => {
7      // cy.wrap() gives you Cypress commands on the raw jQuery element
8      cy.wrap($el).should('contain.text', 'playwright');
9    });
10});
#each#contain#list#loop
Cypressassertions

Assert element attributes

Check href, aria-label, disabled state, and data attributes on elements.

1it('link and button attributes are correct', () => {
2  cy.visit('/profile');
3
4  cy.get('a[data-testid="docs-link"]')
5    .should('have.attr', 'href')
6    .and('include', '/docs');
7
8  cy.get('[data-testid="avatar"]')
9    .should('have.attr', 'aria-label', 'User avatar');
10
11  cy.get('[data-testid="submit-btn"]')
12    .should('not.be.disabled');
13
14  cy.get('[data-testid="plan-badge"]')
15    .should('have.attr', 'data-plan', 'pro');
16});
#attribute#aria#disabled#href#have.attr
Cypressnavigation

Browser back and forward navigation

Use cy.go() to navigate history and assert the URL at each step.

1it('back and forward navigation works', () => {
2  cy.visit('/page-a');
3  cy.get('a[href="/page-b"]').click();
4  cy.url().should('include', '/page-b');
5
6  cy.go('back');
7  cy.url().should('include', '/page-a');
8
9  cy.go('forward');
10  cy.url().should('include', '/page-b');
11});
#go back#go forward#history#url
Cypressnavigation

Reload page and assert persistence

Confirm localStorage-backed state (e.g. theme) survives a page reload.

1it('dark mode setting persists after reload', () => {
2  cy.visit('/settings');
3
4  cy.get('[data-testid="dark-mode-toggle"]').click();
5  cy.get('html').should('have.attr', 'data-theme', 'dark');
6
7  // cy.reload() keeps localStorage intact within the same test
8  cy.reload();
9
10  cy.get('html').should('have.attr', 'data-theme', 'dark');
11});
#reload#refresh#localStorage#persist
Cypressforms

Check and uncheck a checkbox

Toggle a checkbox and chain assertions on its state.

1it('checkbox toggles correctly', () => {
2  cy.visit('/preferences');
3
4  cy.get('[name="email-notifications"]')
5    .should('not.be.checked')
6    .check()
7    .should('be.checked')
8    .uncheck()
9    .should('not.be.checked');
10});
#checkbox#check#uncheck#be.checked
Cypressforms

Select a radio button

Choose a radio option by value and confirm mutual exclusivity.

1it('selects the express shipping radio', () => {
2  cy.visit('/checkout');
3
4  cy.get('[name="shipping"][value="express"]')
5    .check()
6    .should('be.checked');
7
8  // Other options must not be selected
9  cy.get('[name="shipping"][value="standard"]')
10    .should('not.be.checked');
11});
#radio#check#be.checked#exclusive
Cypressforms

Fill a date input

Type an ISO date value into a native <input type='date'> field.

1it('fills a native date input', () => {
2  cy.visit('/booking');
3
4  // ISO 8601 format is required by native date inputs
5  cy.get('input[type="date"][name="check-in"]')
6    .type('2025-08-15')
7    .should('have.value', '2025-08-15');
8
9  cy.get('[type="submit"]').click();
10  cy.contains('Aug 15, 2025').should('be.visible');
11});
#date#input#type#ISO
Cypressforms

Clear a field and retype

Clear an existing input value and replace it — the standard edit-form pattern.

1it('updates the display name field', () => {
2  cy.visit('/edit-profile');
3
4  cy.get('[name="display-name"]')
5    .should('have.value', 'John Doe')
6    .clear()
7    .type('Jane Doe')
8    .should('have.value', 'Jane Doe');
9
10  cy.get('[type="submit"]').click();
11  cy.contains('Profile updated').should('be.visible');
12});
#clear#type#retype#edit form
Cypressfixtures

Load and use a JSON fixture

Read test data from a fixture file and use it to fill form fields.

1// cypress/fixtures/user.json → { "name": "Alice", "email": "alice@test.com", "role": "admin" }
2
3it('fills form from fixture data', () => {
4  cy.fixture('user').then((user) => {
5    cy.visit('/create-user');
6
7    cy.get('[name="name"]').type(user.name);
8    cy.get('[name="email"]').type(user.email);
9    cy.get('select[name="role"]').select(user.role);
10
11    cy.get('[type="submit"]').click();
12    cy.contains(`Welcome, ${user.name}`).should('be.visible');
13  });
14});
#fixture#json#cy.fixture#data-driven
Cypressfixtures

Parametrize tests with forEach

Loop over a data array to run the same test body for multiple input sets.

1const roles = [
2  { role: 'admin',  canDelete: true  },
3  { role: 'editor', canDelete: false },
4  { role: 'viewer', canDelete: false },
5];
6
7roles.forEach(({ role, canDelete }) => {
8  it(`${role} sees correct actions`, () => {
9    cy.request('POST', '/api/login', { role }).then(({ body }) => {
10      window.localStorage.setItem('auth_token', body.token);
11    });
12
13    cy.visit('/dashboard');
14
15    if (canDelete) {
16      cy.get('[data-testid="delete-btn"]').should('be.visible');
17    } else {
18      cy.get('[data-testid="delete-btn"]').should('not.exist');
19    }
20  });
21});
#parametrize#forEach#data-driven#roles
Cypressvisual

Full-page screenshot

Capture the entire page as a PNG for documentation or manual visual review.

1it('captures full-page screenshot of dashboard', () => {
2  cy.visit('/dashboard');
3
4  // Wait for async data before capturing
5  cy.get('table tbody tr').should('have.length.greaterThan', 0);
6
7  cy.screenshot('dashboard-full', {
8    capture: 'fullPage', // 'viewport' | 'fullPage' | 'runner'
9  });
10  // Saved to: cypress/screenshots/dashboard-full.png
11});
#screenshot#full page#cy.screenshot#capture
Cypressvisual

Element screenshot for visual diff

Screenshot a specific component for targeted visual regression testing.

1it('captures the nav bar component', () => {
2  cy.visit('/');
3
4  cy.get('[data-testid="main-nav"]')
5    .scrollIntoView()
6    .screenshot('main-nav', { padding: 8 });
7
8  // Pair with cypress-image-diff or cypress-plugin-snapshots
9  // for automated pixel-level comparison against a baseline
10});
#screenshot#element#component#visual regression
Cypressfile

Download a file and verify content

Trigger a CSV download and assert the file exists with expected content.

1it('downloads the CSV report', () => {
2  cy.visit('/reports');
3
4  cy.get('[data-testid="export-btn"]').click();
5
6  // cy.readFile polls until the file appears (default timeout 4 s)
7  cy.readFile('cypress/downloads/report.csv', { timeout: 8000 })
8    .should('exist')
9    .and('include', 'email,name,role'); // assert the CSV header row
10});
#download#csv#readFile#verify
Cypresswait

Wait for element to disappear

Assert that a loading spinner or toast notification leaves the DOM.

1it('spinner disappears once data loads', () => {
2  cy.visit('/dashboard');
3
4  cy.get('[data-testid="spinner"]').should('exist');
5
6  // Cypress retries until the element is gone
7  cy.get('[data-testid="spinner"]').should('not.exist');
8
9  cy.get('table').should('be.visible');
10});
#not.exist#disappear#spinner#loading
Cypressselectors

Select elements by position and text filter

Target the nth item with .eq() and narrow a set with .filter(':contains(...)').

1it('selects cards by position and text', () => {
2  cy.visit('/products');
3
4  // eq() is 0-indexed — gets the third card
5  cy.get('[data-testid="product-card"]').eq(2).should('be.visible');
6
7  // filter() narrows to only elements containing "Sale"
8  cy.get('[data-testid="product-card"]')
9    .filter(':contains("Sale")')
10    .first()
11    .find('[data-testid="sale-badge"]')
12    .should('be.visible');
13});
#eq#nth#filter#contains
Playwrightselectors

Drag and drop

Drag an element to a drop target using Playwright's built-in dragTo API.

1test('drags a card to the done column', async ({ page }) => {
2  await page.goto('/kanban');
3
4  const card = page.locator('[data-testid="card-1"]');
5  const dropZone = page.locator('[data-testid="column-done"]');
6
7  // dragTo handles the full mousedown → mousemove → mouseup sequence
8  await card.dragTo(dropZone);
9
10  // Assert the card now lives inside the target column
11  await expect(dropZone.locator('[data-testid="card-1"]')).toBeVisible();
12});
#drag#drop#dragTo#mouse
Playwrightselectors

Hover over an element

Trigger a hover to reveal a tooltip or dropdown menu.

1test('tooltip appears on hover', async ({ page }) => {
2  await page.goto('/dashboard');
3
4  // hover() dispatches the full mousemove → mouseenter → mouseover sequence
5  await page.locator('[data-testid="info-icon"]').hover();
6
7  await expect(page.getByRole('tooltip')).toBeVisible();
8  await expect(page.getByRole('tooltip')).toContainText('Last updated');
9});
#hover#tooltip#mouseover#mouseenter
Playwrightnavigation

Handle browser dialog (alert / confirm)

Listen for alert and confirm dialogs and accept or dismiss them programmatically.

1test('accepts a confirm dialog before deleting', async ({ page }) => {
2  // Register the handler BEFORE the action that triggers the dialog
3  page.on('dialog', async (dialog) => {
4    expect(dialog.message()).toContain('Are you sure?');
5    await dialog.accept(); // use dialog.dismiss() to click Cancel
6  });
7
8  await page.goto('/settings');
9  await page.getByRole('button', { name: 'Delete account' }).click();
10
11  await expect(page.getByText('Account deleted')).toBeVisible();
12});
#dialog#alert#confirm#dismiss#accept
Playwrightnavigation

Switch between browser tabs

Open a new tab, interact with it, then return focus to the original tab.

1test('switches between tabs', async ({ page, context }) => {
2  await page.goto('/home');
3
4  // Capture the new tab at the same time as the click that opens it
5  const [newTab] = await Promise.all([
6    context.waitForEvent('page'),
7    page.getByRole('link', { name: 'Open in new tab' }).click(),
8  ]);
9
10  await newTab.waitForLoadState('domcontentloaded');
11  await expect(newTab).toHaveURL(//guide/);
12  await newTab.getByRole('button', { name: 'Accept cookies' }).click();
13
14  // Return focus to the original tab
15  await page.bringToFront();
16  await expect(page).toHaveURL(//home/);
17});
#tab#new tab#context#bringToFront#popup
Playwrightnavigation

Scroll to an element

Scroll an off-screen element into the viewport before asserting or interacting.

1test('scrolls to the pricing section', async ({ page }) => {
2  await page.goto('/landing');
3
4  const pricing = page.getByTestId('pricing-section');
5
6  // scrollIntoViewIfNeeded does nothing if the element is already visible
7  await pricing.scrollIntoViewIfNeeded();
8
9  // toBeInViewport() asserts the element is actually in the visible area
10  await expect(pricing).toBeInViewport();
11  await expect(pricing.getByRole('heading', { name: /pricing/i })).toBeVisible();
12});
#scroll#scrollIntoView#viewport#toBeInViewport
Playwrightassertions

Get all text from a list of elements

Collect every matched element's text into a string array and assert the collection.

1test('blog tags include "playwright"', async ({ page }) => {
2  await page.goto('/blog');
3
4  const tags = page.locator('[data-testid="tag"]');
5
6  // allTextContents() resolves the full locator set to a string[]
7  const texts = await tags.allTextContents();
8
9  expect(texts.length).toBeGreaterThan(0);
10  expect(texts).toContain('playwright');
11
12  // Assert no duplicate tags
13  const unique = new Set(texts);
14  expect(unique.size).toBe(texts.length);
15});
#allTextContents#text#list#array
Playwrightvisual

Test mobile viewport with device emulation

Emulate a real device profile to verify responsive layout in a single test.

1import { test, expect, devices } from '@playwright/test';
2
3test('mobile nav is shown on iPhone 14', async ({ browser }) => {
4  // Spread the full device profile: viewport, UA, deviceScaleFactor, etc.
5  const ctx = await browser.newContext({ ...devices['iPhone 14'] });
6  const page = await ctx.newPage();
7
8  await page.goto('/');
9
10  // Hamburger visible on mobile, desktop nav hidden
11  await expect(page.getByTestId('mobile-menu-button')).toBeVisible();
12  await expect(page.getByTestId('desktop-nav')).toBeHidden();
13
14  await ctx.close();
15});
#mobile#viewport#devices#responsive#emulation
Playwrightfixtures

Run tests across multiple browsers

Configure projects in playwright.config.ts so every test runs on Chromium, Firefox, and WebKit.

1// playwright.config.ts
2import { defineConfig, devices } from '@playwright/test';
3
4export default defineConfig({
5  projects: [
6    { name: 'chromium', use: { ...devices['Desktop Chrome']  } },
7    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
8    { name: 'webkit',   use: { ...devices['Desktop Safari']  } },
9  ],
10});
11
12// Run all browsers:     npx playwright test
13// Run one browser only: npx playwright test --project=firefox
#projects#cross-browser#firefox#webkit#config
Playwrightnetwork

Mock browser geolocation

Inject fake GPS coordinates before the page loads to test location-aware features.

1test('shows the correct city for mocked coordinates', async ({ browser }) => {
2  const context = await browser.newContext({
3    geolocation: { latitude: 48.8566, longitude: 2.3522 }, // Paris
4    permissions: ['geolocation'], // grant without a browser prompt
5  });
6  const page = await context.newPage();
7
8  await page.goto('/store-finder');
9  await page.getByRole('button', { name: 'Use my location' }).click();
10
11  // The app should resolve the mocked coordinates to Paris
12  await expect(page.getByTestId('detected-city')).toHaveText('Paris');
13  await context.close();
14});
#geolocation#mock#context#permissions#GPS
Playwrightselectors

Interact with content inside an iframe

Use frameLocator() to scope all locator calls inside an embedded iframe.

1test('fills a payment form inside an iframe', async ({ page }) => {
2  await page.goto('/checkout');
3
4  // frameLocator() switches the locator context into the iframe's DOM
5  const frame = page.frameLocator('[data-testid="payment-iframe"]');
6
7  // All commands inside the frame use the same Playwright locator API
8  await frame.locator('[placeholder="Card number"]').fill('4242 4242 4242 4242');
9  await frame.locator('[placeholder="MM / YY"]').fill('12 / 28');
10  await frame.locator('[placeholder="CVC"]').fill('123');
11
12  await page.getByRole('button', { name: 'Pay now' }).click();
13  await expect(page.getByText('Payment successful')).toBeVisible();
14});
#iframe#frameLocator#embed#payment#cross-frame
Playwrightforms

Trigger keyboard shortcuts

Press modifier key combinations like Ctrl+A and Ctrl+C on a focused element.

1test('selects all text and copies with keyboard shortcuts', async ({ page }) => {
2  await page.goto('/editor');
3
4  const editor = page.getByRole('textbox', { name: 'Content' });
5  await editor.fill('Hello World');
6
7  // press() accepts KeyboardEvent.key names and modifier combos
8  await editor.press('Control+a'); // select all text
9  await editor.press('Control+c'); // copy to clipboard
10
11  // Paste into a second field to verify the clipboard content
12  const preview = page.getByRole('textbox', { name: 'Preview' });
13  await preview.click();
14  await preview.press('Control+v');
15
16  await expect(preview).toHaveValue('Hello World');
17});
#keyboard#ctrl#shortcut#press#hotkey
Playwrightassertions

Count elements on the page

Assert an exact element count with toHaveCount(), or read the count for conditional logic.

1test('product grid renders the right number of cards', async ({ page }) => {
2  await page.goto('/products');
3
4  const cards = page.locator('[data-testid="product-card"]');
5
6  // toHaveCount() retries — prefer it over count() for assertions
7  await expect(cards).toHaveCount(12);
8
9  // Use count() when the number drives conditional logic
10  const n = await cards.count();
11  if (n > 6) {
12    await expect(page.getByTestId('pagination')).toBeVisible();
13  }
14});
#count#toHaveCount#length#elements
Playwrightauth

Read and assert a cookie

Retrieve cookies from the browser context after login and assert their security flags.

1test('auth cookie is set with correct security flags', async ({ page, context }) => {
2  await page.goto('/login');
3  await page.getByLabel('Email').fill('user@test.com');
4  await page.getByLabel('Password').fill('secret');
5  await page.getByRole('button', { name: 'Sign in' }).click();
6  await page.waitForURL('/dashboard');
7
8  // context.cookies() returns every cookie for the current browser context
9  const cookies = await context.cookies();
10  const auth = cookies.find((c) => c.name === 'auth_token');
11
12  expect(auth).toBeDefined();
13  expect(auth!.httpOnly).toBe(true); // JS must not be able to read it
14  expect(auth!.secure).toBe(true);   // must only transmit over HTTPS
15});
#cookie#cookies#httpOnly#secure#assert
Playwrightfixtures

Pre-set localStorage before navigation

Use addInitScript to inject localStorage values before the page boots — ideal for themes, feature flags, and onboarding state.

1test('dashboard loads with pre-set dark mode', async ({ page }) => {
2  // addInitScript runs in the browser before every page.goto() in this test
3  await page.addInitScript(() => {
4    window.localStorage.setItem('theme', 'dark');
5    window.localStorage.setItem('onboardingComplete', 'true');
6  });
7
8  await page.goto('/dashboard');
9
10  // Theme is already applied — no toggle click needed
11  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
12
13  // Onboarding modal should not appear for returning users
14  await expect(page.getByTestId('onboarding-modal')).not.toBeVisible();
15});
#localStorage#addInitScript#theme#feature flag#setup
Playwrightwait

Retry assertions with a custom timeout

Override the per-assertion timeout and use toPass() for complex retry scenarios.

1test('toast auto-dismisses and status becomes Ready', async ({ page }) => {
2  await page.goto('/notifications');
3  await page.getByRole('button', { name: 'Trigger notification' }).click();
4
5  // Extend the default timeout for this specific assertion
6  await expect(page.getByTestId('toast')).toBeHidden({ timeout: 10_000 });
7
8  // toPass() retries the callback until it stops throwing — great for
9  // composite assertions that can't use a single locator
10  await expect(async () => {
11    const text = await page.locator('[data-testid="status"]').textContent();
12    expect(text).toBe('Ready');
13  }).toPass({ timeout: 15_000, intervals: [500, 1000, 2000] });
14});
#timeout#retry#toPass#custom wait#polling
Playwrightfixtures

Generate random test data inline

Create unique email and username values per run to prevent conflicts in a shared test database.

1test('registers a new user with unique credentials', async ({ page }) => {
2  // Generate a short unique suffix — avoids conflicts in a shared DB
3  const uid = Math.random().toString(36).slice(2, 10);
4  const email = `qa+${uid}@example.com`;
5  const username = `user_${uid}`;
6
7  await page.goto('/register');
8  await page.getByLabel('Username').fill(username);
9  await page.getByLabel('Email').fill(email);
10  await page.getByLabel('Password').fill('Str0ng!Pass');
11  await page.getByRole('button', { name: 'Create account' }).click();
12
13  await expect(page.getByText(`Welcome, ${username}`)).toBeVisible();
14});
#random#test data#unique#faker#Math.random
Playwrightvisual

Take an element-level screenshot

Capture a screenshot scoped to a single component for visual documentation or baseline comparison.

1test('sidebar matches visual baseline', async ({ page }) => {
2  await page.goto('/dashboard');
3
4  const sidebar = page.getByTestId('sidebar');
5  await sidebar.scrollIntoViewIfNeeded();
6
7  // For a one-off PNG saved to disk:
8  await sidebar.screenshot({ path: 'test-results/sidebar.png' });
9
10  // For automated regression, use toHaveScreenshot() —
11  // first run creates the baseline; subsequent runs diff against it
12  await expect(sidebar).toHaveScreenshot('sidebar-baseline.png', {
13    maxDiffPixelRatio: 0.02, // tolerate 2% pixel difference (anti-aliasing)
14  });
15});
#screenshot#element#component#toHaveScreenshot#visual regression
Cypressselectors

Drag and drop

Simulate HTML5 drag-and-drop by triggering the underlying mouse events manually.

1it('drags a card to the done column', () => {
2  cy.visit('/kanban');
3
4  // Cypress has no native drag API — fire the underlying pointer events
5  cy.get('[data-testid="card-1"]')
6    .trigger('mousedown', { which: 1, force: true });
7
8  cy.get('[data-testid="column-done"]')
9    .trigger('mousemove', { force: true })
10    .trigger('mouseup',   { force: true });
11
12  cy.get('[data-testid="column-done"]')
13    .find('[data-testid="card-1"]')
14    .should('exist');
15});
#drag#drop#trigger#mousedown#mousemove
Cypressselectors

Hover over an element

Dispatch a mouseover event to reveal tooltips or hover menus — Cypress has no native .hover().

1it('tooltip appears on hover', () => {
2  cy.visit('/dashboard');
3
4  // trigger('mouseover') fires the event Cypress would send on hover
5  cy.get('[data-testid="info-icon"]').trigger('mouseover');
6
7  cy.get('[role="tooltip"]')
8    .should('be.visible')
9    .and('contain.text', 'Last updated');
10});
#hover#mouseover#trigger#tooltip
Cypressnavigation

Handle a browser alert or confirm dialog

Stub window.confirm to accept or dismiss a dialog without freezing the Cypress runner.

1it('accepts a confirm dialog before deleting', () => {
2  cy.visit('/settings');
3
4  // Register the handler BEFORE the action that triggers the dialog
5  cy.on('window:confirm', (message) => {
6    expect(message).to.include('Are you sure?');
7    return true; // return false to click Cancel
8  });
9
10  cy.get('[data-testid="delete-account-btn"]').click();
11  cy.contains('Account deleted').should('be.visible');
12});
#alert#confirm#dialog#window:confirm#stub
Cypressselectors

Interact with content inside an iframe

Access a same-origin iframe's DOM by chaining through its contentDocument body.

1it('fills a payment form inside an iframe', () => {
2  cy.visit('/checkout');
3
4  // Store the iframe body as an alias to avoid repeating the chain
5  cy.get('[data-testid="payment-iframe"]')
6    .its('0.contentDocument.body')
7    .should('not.be.empty')   // wait for the iframe to finish loading
8    .then(cy.wrap)
9    .as('frame');
10
11  cy.get('@frame').find('[placeholder="Card number"]').type('4242 4242 4242 4242');
12  cy.get('@frame').find('[placeholder="CVC"]').type('123');
13
14  cy.get('[type="submit"]').click();
15  cy.contains('Payment successful').should('be.visible');
16});
#iframe#contentDocument#same-origin#embed#wrap
Cypressnavigation

Scroll to an element

Use .scrollIntoView() to bring an off-screen element into the viewport.

1it('scrolls to the pricing section', () => {
2  cy.visit('/landing');
3
4  // scrollIntoView() scrolls and then yields the element for chaining
5  cy.get('[data-testid="pricing-section"]')
6    .scrollIntoView()
7    .should('be.visible');
8
9  cy.get('[data-testid="pricing-section"] h2')
10    .should('contain.text', 'Pricing');
11});
#scroll#scrollIntoView#viewport#off-screen
Cypressassertions

Get text from multiple elements

Collect inner text from a list of elements using .each() and assert the gathered array.

1it('blog tags include "playwright"', () => {
2  cy.visit('/blog');
3
4  const texts = [];
5
6  cy.get('[data-testid="tag"]')
7    .each(($el) => {
8      texts.push($el.text().trim()); // .text() returns the raw string
9    })
10    .then(() => {
11      expect(texts.length).to.be.greaterThan(0);
12      expect(texts).to.include('playwright');
13
14      // Assert no duplicate tags
15      const unique = [...new Set(texts)];
16      expect(unique.length).to.equal(texts.length);
17    });
18});
#text#each#array#collect#list
Cypressvisual

Test mobile viewport

Switch to a mobile viewport with cy.viewport() to verify responsive layout.

1it('shows mobile navigation on a small screen', () => {
2  // Emulate an iPhone 14 — Cypress supports named presets and custom px sizes
3  cy.viewport('iphone-14');
4  cy.visit('/');
5
6  // Hamburger should be visible; desktop nav should not
7  cy.get('[data-testid="mobile-menu-button"]').should('be.visible');
8  cy.get('[data-testid="desktop-nav"]').should('not.be.visible');
9
10  // Verify the menu expands on click
11  cy.get('[data-testid="mobile-menu-button"]').click();
12  cy.get('[data-testid="mobile-nav"]').should('be.visible');
13});
#mobile#viewport#responsive#cy.viewport#iphone
Cypressnetwork

Mock browser geolocation

Stub navigator.geolocation.getCurrentPosition before page load to test location-aware UI.

1it('shows Paris when location is mocked', () => {
2  cy.visit('/store-finder', {
3    onBeforeLoad(win) {
4      // Replace the real API with a stub before the page initialises
5      cy.stub(win.navigator.geolocation, 'getCurrentPosition').callsFake((cb) => {
6        cb({ coords: { latitude: 48.8566, longitude: 2.3522 } }); // Paris
7      });
8    },
9  });
10
11  cy.get('[data-testid="use-location-btn"]').click();
12  cy.get('[data-testid="detected-city"]').should('have.text', 'Paris');
13});
#geolocation#stub#mock#navigator#location
Cypressauth

Read and assert a cookie

Use cy.getCookie() to retrieve a named cookie and assert its value and security flags.

1it('auth cookie is set with correct security flags', () => {
2  cy.request('POST', '/api/login', {
3    email: 'user@test.com',
4    password: 'secret',
5  });
6
7  // getCookie() returns the cookie object — chain assertions on properties
8  cy.getCookie('auth_token').should('exist').then((cookie) => {
9    expect(cookie.httpOnly).to.be.true; // JS must not be able to read it
10    expect(cookie.secure).to.be.true;   // must only transmit over HTTPS
11  });
12});
#cookie#getCookie#httpOnly#secure#assert
Cypressfixtures

Pre-set localStorage before navigation

Inject localStorage values via onBeforeLoad so the app reads them on first paint.

1it('dashboard loads with dark mode enabled', () => {
2  cy.visit('/dashboard', {
3    onBeforeLoad(win) {
4      // Set localStorage BEFORE the page script executes
5      win.localStorage.setItem('theme', 'dark');
6      win.localStorage.setItem('onboardingComplete', 'true');
7    },
8  });
9
10  cy.get('html').should('have.attr', 'data-theme', 'dark');
11
12  // Onboarding modal should not appear for returning users
13  cy.get('[data-testid="onboarding-modal"]').should('not.exist');
14});
#localStorage#onBeforeLoad#theme#setup#initialise
Cypressforms

Trigger keyboard shortcuts

Use Cypress key sequences like {ctrl+a} and {ctrl+v} to test editor keyboard interactions.

1it('copies text with Ctrl+A and Ctrl+C', () => {
2  cy.visit('/editor');
3
4  cy.get('[data-testid="content-editor"]')
5    .type('Hello World')
6    .type('{ctrl+a}') // select all text in the field
7    .type('{ctrl+c}'); // copy the selection to the clipboard
8
9  cy.get('[data-testid="preview-area"]')
10    .click()
11    .type('{ctrl+v}') // paste from clipboard
12    .should('have.value', 'Hello World');
13});
#keyboard#ctrl#shortcut#type#hotkey
Cypressassertions

Count elements on the page

Assert the exact number of matched elements with have.length, or read the count dynamically.

1it('product grid shows the correct number of cards', () => {
2  cy.visit('/products');
3
4  // have.length asserts the exact count; Cypress retries until it passes
5  cy.get('[data-testid="product-card"]').should('have.length', 12);
6
7  // Use .its('length') when the count drives further conditional logic
8  cy.get('[data-testid="product-card"]')
9    .its('length')
10    .should('be.gte', 1);
11});
#count#have.length#length#elements
Cypressfixtures

Generate random test data inline

Produce a unique email and username per run to prevent conflicts in a shared test environment.

1it('registers a new user with unique credentials', () => {
2  // Math.random() gives a short unique suffix per run
3  const uid = Math.random().toString(36).slice(2, 10);
4  const email = `qa+${uid}@example.com`;
5  const username = `user_${uid}`;
6
7  cy.visit('/register');
8  cy.get('[name="username"]').type(username);
9  cy.get('[name="email"]').type(email);
10  cy.get('[name="password"]').type('Str0ng!Pass');
11  cy.get('[type="submit"]').click();
12
13  cy.contains(`Welcome, ${username}`).should('be.visible');
14});
#random#test data#unique#Math.random#faker
Cypresswait

Retry an assertion with a custom timeout

Override the default 4-second retry window to wait longer for slow DOM updates.

1it('toast disappears and status becomes Ready', () => {
2  cy.visit('/notifications');
3  cy.get('[data-testid="trigger-btn"]').click();
4
5  cy.get('[data-testid="toast"]').should('be.visible');
6
7  // Pass a timeout option to extend the default 4 s retry window
8  cy.get('[data-testid="toast"]', { timeout: 10_000 }).should('not.exist');
9
10  // Use a should callback to assert on derived/computed values
11  cy.get('[data-testid="status"]').should(($el) => {
12    expect($el.text().trim()).to.equal('Ready');
13  });
14});
#timeout#retry#custom wait#should#polling
Cypressnetwork

Stub window.open to prevent new tabs

Replace window.open with a cy.stub() so no real tab opens during tests, then assert the call.

1it('share button calls window.open with the correct URL', () => {
2  cy.visit('/share');
3
4  cy.window().then((win) => {
5    // Replace window.open before the button click — no real tab will open
6    cy.stub(win, 'open').as('windowOpen');
7  });
8
9  cy.get('[data-testid="share-btn"]').click();
10
11  // Assert window.open was called with a URL matching the pattern
12  cy.get('@windowOpen').should(
13    'have.been.calledWith',
14    Cypress.sinon.match(//share?id=/),
15    '_blank'
16  );
17});
#window.open#stub#new tab#sinon#spy
Playwrightwait

Wait for network idle

Hold execution until all in-flight network requests have settled — useful before screenshots or complex assertions.

1test('waits for all requests to settle before asserting', async ({ page }) => {
2  await page.goto('/reports');
3
4  // 'networkidle' waits until there are no requests for at least 500 ms
5  await page.waitForLoadState('networkidle');
6
7  // Safe to assert — all async data has finished loading
8  await expect(page.getByTestId('report-table')).toBeVisible();
9  await expect(page.locator('table tbody tr')).not.toHaveCount(0);
10});
#networkidle#waitForLoadState#network#settle
Playwrightassertions

Assert page has no console errors

Collect browser console messages during a test and fail if any errors were logged.

1test('home page logs no console errors', async ({ page }) => {
2  const errors: string[] = [];
3
4  // Capture every console message before navigating
5  page.on('console', (msg) => {
6    if (msg.type() === 'error') {
7      errors.push(msg.text());
8    }
9  });
10
11  await page.goto('/');
12  await page.waitForLoadState('networkidle');
13
14  // Fail the test and print each offending message
15  expect(errors, `Console errors: ${errors.join(', ')}`).toHaveLength(0);
16});
#console#errors#debug#page.on#msg
Cypressnetwork

Intercept and modify a response body

Intercept a real API response and overwrite specific fields before they reach the app.

1it('shows an overdue badge when API returns an overdue status', () => {
2  cy.intercept('GET', '/api/tasks', (req) => {
3    req.reply((res) => {
4      // Mutate a single field in the real response — everything else stays
5      res.body.tasks[0].status = 'overdue';
6    });
7  }).as('getTasks');
8
9  cy.visit('/tasks');
10  cy.wait('@getTasks');
11
12  // The app should render the overdue badge based on the patched status
13  cy.get('[data-testid="task-0"] [data-testid="status-badge"]')
14    .should('have.text', 'Overdue');
15});
#intercept#modify#response#body#req.reply
Cypressassertions

Assert element is in viewport

Confirm an element is actually visible within the browser's current scroll position.

1// Add this reusable command once in cypress/support/commands.js
2Cypress.Commands.add('isInViewport', { prevSubject: true }, (subject) => {
3  const rect = subject[0].getBoundingClientRect();
4  expect(rect.top).to.be.gte(0);
5  expect(rect.bottom).to.be.lte(Cypress.config('viewportHeight'));
6  expect(rect.left).to.be.gte(0);
7  expect(rect.right).to.be.lte(Cypress.config('viewportWidth'));
8  return subject;
9});
10
11// Usage in any test:
12it('CTA button is fully visible without scrolling', () => {
13  cy.visit('/landing');
14  cy.get('[data-testid="cta-button"]').isInViewport();
15});
#viewport#visible#isInViewport#scroll#in-view
Cypressforms

Clear and type into an input field

Clear an existing value and type a replacement — the standard edit-form pattern in Cypress.

1it('updates the email address field', () => {
2  cy.visit('/account');
3
4  // .clear() empties the field, .type() then populates it fresh
5  cy.get('[name="email"]')
6    .should('have.value', 'old@example.com')
7    .clear()
8    .type('new@example.com')
9    .should('have.value', 'new@example.com');
10
11  cy.get('[type="submit"]').click();
12  cy.contains('Email updated').should('be.visible');
13});
#clear#type#input#edit#fill
Playwrightselectors

getByRole — click a button by name

Locate a button by its accessible role and visible name — the most resilient selector strategy.

1test('saves changes using getByRole', async ({ page }) => {
2  await page.goto('/settings');
3
4  // getByRole matches what screen readers see — survives CSS and text refactors
5  await page.getByRole('button', { name: 'Save changes' }).click();
6
7  await expect(page.getByText('Settings saved')).toBeVisible();
8});
#getByRole#button#accessible name#ARIA
Playwrightselectors

getByLabel — fill a form input

Find an input by its associated label text — no need to know the input's id or name attribute.

1test('fills inputs by their label text', async ({ page }) => {
2  await page.goto('/signup');
3
4  // getByLabel works with <label for="...">, aria-label, and aria-labelledby
5  await page.getByLabel('Email address').fill('qa@example.com');
6  await page.getByLabel('Password').fill('Str0ng!Pass');
7
8  await page.getByRole('button', { name: 'Sign up' }).click();
9  await expect(page.getByText('Account created')).toBeVisible();
10});
#getByLabel#label#form#input
Playwrightselectors

getByPlaceholder — fill by placeholder text

Locate an input by its placeholder attribute — useful for search bars and inline forms without visible labels.

1test('searches using the placeholder-located input', async ({ page }) => {
2  await page.goto('/library');
3
4  // getByPlaceholder is ideal when there is no visible <label>
5  await page.getByPlaceholder('Search snippets...').fill('playwright drag');
6  await page.getByPlaceholder('Search snippets...').press('Enter');
7
8  await expect(page.getByTestId('results-list')).toBeVisible();
9});
#getByPlaceholder#placeholder#search#input
Playwrightselectors

getByTestId — locate by data-testid

Select elements by a data-testid attribute — immune to text, style, and layout changes.

1test('interacts with elements via data-testid', async ({ page }) => {
2  await page.goto('/profile');
3
4  // data-testid never breaks due to copy or styling changes —
5  // the gold standard for stable selectors in production test suites
6  const avatar = page.getByTestId('user-avatar');
7  await expect(avatar).toBeVisible();
8
9  await page.getByTestId('edit-profile-btn').click();
10  await expect(page.getByTestId('edit-profile-modal')).toBeVisible();
11});
#getByTestId#data-testid#stable selector#test id
Playwrightselectors

getByText — find element by visible text

Locate any element by its visible text content — supports exact strings and regex patterns.

1test('finds elements by their text content', async ({ page }) => {
2  await page.goto('/pricing');
3
4  // Exact string match — finds the element containing exactly this text
5  await page.getByText('Get started for free').click();
6
7  // Regex — useful for dynamic text or case-insensitive matching
8  await expect(page.getByText(/welcome,/i)).toBeVisible();
9
10  // Scope to a specific element type to avoid ambiguous matches
11  await expect(page.getByRole('heading').getByText('Dashboard')).toBeVisible();
12});
#getByText#text content#regex#visible text
Playwrightselectors

Filter a locator by text

Narrow a matched set of elements down to only those containing specific text.

1test('clicks the "View" button only on Alice's card', async ({ page }) => {
2  await page.goto('/team');
3
4  const cards = page.locator('[data-testid="member-card"]');
5
6  // filter() narrows the matched set without leaving the locator API —
7  // far more reliable than CSS :has-text() pseudo-classes
8  const aliceCard = cards.filter({ hasText: 'Alice' });
9  await expect(aliceCard).toHaveCount(1);
10
11  await aliceCard.getByRole('button', { name: 'View profile' }).click();
12  await expect(page.getByTestId('profile-drawer')).toBeVisible();
13});
#filter#hasText#locator#narrow
Playwrightselectors

XPath selector — when to use it

Use XPath as a last resort for legacy HTML with no accessible attributes or test IDs.

1test('targets a table row by cell text using XPath', async ({ page }) => {
2  await page.goto('/invoices');
3
4  // XPath is a last resort — only reach for it when getByRole, getByTestId,
5  // and CSS selectors all fail (e.g. old server-rendered HTML with no hooks).
6  // Here: find the <tr> whose first <td> contains "Invoice #1042"
7  const row = page.locator('//tr[td[normalize-space()="Invoice #1042"]]');
8  await expect(row).toBeVisible();
9
10  // Relative XPath from the matched element — get the third cell
11  const statusCell = row.locator('xpath=td[3]');
12  await expect(statusCell).toHaveText('Paid');
13});
#XPath#xpath#legacy#normalize-space
Playwrightauth

Save auth state to file after login

Persist cookies and localStorage to a JSON file after login so other tests can skip the UI flow.

1test('logs in and saves session state to disk', async ({ page }) => {
2  await page.goto('/login');
3  await page.getByLabel('Email').fill('user@test.com');
4  await page.getByLabel('Password').fill('secret');
5  await page.getByRole('button', { name: 'Sign in' }).click();
6  await page.waitForURL('/dashboard');
7
8  // Saves cookies + localStorage to a JSON file.
9  // Other test files can load this file and skip the login UI entirely.
10  await page.context().storageState({ path: 'playwright/.auth/user.json' });
11});
#storageState#save auth#login#persist session
Playwrightauth

Reuse saved auth state with test.use()

Load a saved storageState file so every test in the file starts already authenticated.

1import { test, expect } from '@playwright/test';
2
3// Apply the saved auth state to every test in this file.
4// The browser context starts pre-loaded with cookies and localStorage.
5test.use({ storageState: 'playwright/.auth/user.json' });
6
7test('accesses dashboard without going through login', async ({ page }) => {
8  // No login step — the context already carries a valid session
9  await page.goto('/dashboard');
10  await expect(page.getByTestId('user-menu')).toBeVisible();
11});
12
13test('accesses settings page directly', async ({ page }) => {
14  await page.goto('/settings');
15  await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
16});
#storageState#test.use#reuse session#skip login
Playwrightauth

Login once for all tests with auth.setup.ts

Use a global setup project to authenticate once before the entire suite — the standard pattern for large test suites.

1// ── playwright.config.ts ─────────────────────────────────────
2import { defineConfig } from '@playwright/test';
3
4export default defineConfig({
5  projects: [
6    {
7      name: 'setup',
8      testMatch: /auth\.setup\.ts/, // runs this file before all others
9    },
10    {
11      name: 'chromium',
12      use: { storageState: 'playwright/.auth/user.json' },
13      dependencies: ['setup'], // waits for setup to finish first
14    },
15  ],
16});
17
18// ── auth.setup.ts ─────────────────────────────────────────────
19import { test as setup } from '@playwright/test';
20
21setup('authenticate once for the whole suite', async ({ page }) => {
22  await page.goto('/login');
23  await page.getByLabel('Email').fill('user@test.com');
24  await page.getByLabel('Password').fill('secret');
25  await page.getByRole('button', { name: 'Sign in' }).click();
26  await page.waitForURL('/dashboard');
27
28  // Write auth state — all chromium tests will load this automatically
29  await page.context().storageState({ path: 'playwright/.auth/user.json' });
30});
#auth.setup#global setup#dependencies#storageState#project
Playwrightnetwork

Abort a request to block third-party scripts

Drop matching outgoing requests before they're sent — keeps analytics and trackers out of test runs.

1test('page works with analytics requests blocked', async ({ page }) => {
2  // route.abort() drops the request — the browser never sends it.
3  // Block analytics early so they don't pollute network logs or fire events.
4  await page.route(/google-analytics\.com|segment\.io|hotjar\.com/, (route) => {
5    route.abort();
6  });
7
8  await page.goto('/home');
9
10  // The page should still render correctly without the blocked resources
11  await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
12});
#route.abort#block#abort#analytics#third-party
Playwrightnetwork

Assert intercepted response body

Capture a real API response in-flight and assert its JSON structure matches the expected contract.

1test('users API response matches expected contract', async ({ page }) => {
2  // Start listening before navigation so we don't miss the request
3  const responsePromise = page.waitForResponse(
4    (res) => res.url().includes('/api/users') && res.status() === 200
5  );
6
7  await page.goto('/users');
8  const response = await responsePromise;
9  const body = await response.json();
10
11  // Assert the shape — catches backend contract breaks before the UI does
12  expect(body).toHaveProperty('users');
13  expect(Array.isArray(body.users)).toBe(true);
14  expect(body.users[0]).toMatchObject({
15    id: expect.any(Number),
16    email: expect.stringContaining('@'),
17  });
18});
#waitForResponse#response body#json#api contract#assert
Playwrightassertions

Soft assertions — collect all failures

Use expect.soft() to continue the test after a failure and report all assertion errors together.

1test('profile page shows all user fields correctly', async ({ page }) => {
2  await page.goto('/profile');
3
4  // expect.soft() records the failure but does NOT stop execution —
5  // all soft failures are reported together at the end of the test
6  await expect.soft(page.getByTestId('username')).toHaveText('Alice');
7  await expect.soft(page.getByTestId('email')).toHaveText('alice@test.com');
8  await expect.soft(page.getByTestId('role-badge')).toHaveText('Admin');
9  await expect.soft(page.getByTestId('plan-badge')).toHaveText('Pro');
10
11  // Hard assertion — if the modal doesn't open, there's no point continuing
12  await page.getByRole('button', { name: 'Edit profile' }).click();
13  await expect(page.getByTestId('edit-modal')).toBeVisible();
14});
#soft assertions#expect.soft#non-blocking#collect failures
Playwrightassertions

Assert page title

Assert the document <title> using toHaveTitle() — retries automatically for pages with dynamic titles.

1test('page has the correct document title', async ({ page }) => {
2  await page.goto('/');
3
4  // toHaveTitle() retries until the <title> matches — handles async title updates
5  await expect(page).toHaveTitle('SnipQA — Playwright & Cypress Snippet Library');
6
7  // Regex — useful for titles with dynamic segments like counts or user names
8  await expect(page).toHaveTitle(/SnipQA/);
9
10  // After navigation, verify the title updated
11  await page.getByRole('link', { name: 'Pricing' }).click();
12  await expect(page).toHaveTitle(/Pricing/);
13});
#toHaveTitle#page title#document title#SEO
Playwrightassertions

Assert current URL

Assert the browser URL after navigation, form submission, or redirect using toHaveURL().

1test('URL is correct after navigation and form submission', async ({ page }) => {
2  await page.goto('/login');
3
4  await page.getByLabel('Email').fill('user@test.com');
5  await page.getByLabel('Password').fill('secret');
6  await page.getByRole('button', { name: 'Sign in' }).click();
7
8  // toHaveURL() retries — handles async redirects from client-side routers
9  await expect(page).toHaveURL('/dashboard');
10
11  // Regex — use for dynamic segments like /users/123 or query strings
12  await expect(page).toHaveURL(/\/dashboard/);
13
14  await page.getByRole('link', { name: 'Settings' }).click();
15  await expect(page).toHaveURL(/\/settings/);
16});
#toHaveURL#URL#navigation#redirect#assert
Playwrightassertions

Assert input is disabled or enabled

Check whether a button or input is in a disabled or enabled state — common for form validation flows.

1test('submit button reflects form validity state', async ({ page }) => {
2  await page.goto('/register');
3
4  const submitBtn = page.getByRole('button', { name: 'Create account' });
5
6  // toBeDisabled() retries — safe to call right after navigation
7  await expect(submitBtn).toBeDisabled();
8
9  await page.getByLabel('Email').fill('qa@example.com');
10  await page.getByLabel('Password').fill('Str0ng!Pass');
11
12  // Form is now valid — button should become interactive
13  await expect(submitBtn).toBeEnabled();
14
15  // Also works on inputs — e.g. assert a field is read-only after lock
16  const emailField = page.getByLabel('Email');
17  await expect(emailField).not.toBeDisabled();
18});
#toBeDisabled#toBeEnabled#disabled#form validation
Playwrightforms

Type into a contenteditable div

Interact with rich-text editors and contenteditable elements using pressSequentially().

1test('types into a rich-text editor', async ({ page }) => {
2  await page.goto('/editor');
3
4  const editor = page.locator('[contenteditable="true"]');
5
6  // .fill() does not work on contenteditable elements — use click + pressSequentially
7  await editor.click();
8  await editor.pressSequentially('Hello, Playwright!');
9
10  // To replace existing content: select all then type the replacement
11  await editor.press('Control+a');
12  await editor.pressSequentially('Replaced content');
13
14  await expect(editor).toHaveText('Replaced content');
15});
#contenteditable#rich text#editor#pressSequentially
Playwrightfixtures

beforeEach and afterEach hooks

Run setup and teardown logic before and after every test in a describe block.

1import { test, expect } from '@playwright/test';
2
3test.describe('User settings', () => {
4  test.beforeEach(async ({ page }) => {
5    // Runs before every test — log in once per test
6    await page.goto('/login');
7    await page.getByLabel('Email').fill('user@test.com');
8    await page.getByLabel('Password').fill('secret');
9    await page.getByRole('button', { name: 'Sign in' }).click();
10    await page.waitForURL('/dashboard');
11  });
12
13  test.afterEach(async ({ page }) => {
14    // Runs after every test — clean up state that could leak
15    await page.evaluate(() => window.localStorage.clear());
16  });
17
18  test('can reach the settings page', async ({ page }) => {
19    await page.goto('/settings');
20    await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
21  });
22
23  test('can update display name', async ({ page }) => {
24    await page.goto('/settings');
25    await page.getByLabel('Display name').fill('New Name');
26    await page.getByRole('button', { name: 'Save' }).click();
27    await expect(page.getByText('Saved!')).toBeVisible();
28  });
29});
#beforeEach#afterEach#hooks#setup#teardown
Playwrightfixtures

Group tests with test.describe

Use test.describe to group related tests, share hooks, and produce readable nested report output.

1import { test, expect } from '@playwright/test';
2
3// test.describe groups tests and scopes beforeEach/afterEach.
4// Report shows: "Checkout > with items > shows total price"
5test.describe('Checkout', () => {
6  test.describe('with items in the cart', () => {
7    test.beforeEach(async ({ page }) => {
8      await page.goto('/cart?prefill=true');
9    });
10
11    test('shows the total price', async ({ page }) => {
12      await expect(page.getByTestId('cart-total')).toBeVisible();
13    });
14
15    test('applying a coupon reduces the total', async ({ page }) => {
16      await page.getByLabel('Coupon code').fill('SAVE10');
17      await page.getByRole('button', { name: 'Apply' }).click();
18      await expect(page.getByTestId('discount-row')).toBeVisible();
19    });
20  });
21
22  test('empty cart shows empty-state message', async ({ page }) => {
23    await page.goto('/cart');
24    await expect(page.getByTestId('empty-cart-message')).toBeVisible();
25  });
26});
#test.describe#group#nested#organisation
Playwrightfixtures

Data-driven tests with a for loop

Generate one named test per data row by looping over an array with test() calls.

1import { test, expect } from '@playwright/test';
2
3const plans = [
4  { name: 'Free',  price: '$0',  maxUsers: '1 user'   },
5  { name: 'Pro',   price: '$12', maxUsers: '5 users'  },
6  { name: 'Team',  price: '$49', maxUsers: '25 users' },
7];
8
9// Each iteration registers a separately named test —
10// report shows: "pricing card: Free", "pricing card: Pro", etc.
11for (const { name, price, maxUsers } of plans) {
12  test(`pricing card: ${name}`, async ({ page }) => {
13    await page.goto('/pricing');
14
15    const card = page.locator('[data-testid="pricing-card"]').filter({ hasText: name });
16    await expect(card.getByTestId('plan-price')).toHaveText(price);
17    await expect(card.getByTestId('plan-users')).toHaveText(maxUsers);
18  });
19}
#data-driven#loop#parametrize#test.each#for
Playwrightwait

Wait for URL to change after an action

Use waitForURL() or toHaveURL() to wait for client-side navigation to complete after a click or submit.

1test('multi-step form advances the URL on each step', async ({ page }) => {
2  await page.goto('/onboarding/step-1');
3
4  await page.getByLabel('Company name').fill('Acme Corp');
5  await page.getByRole('button', { name: 'Next' }).click();
6
7  // waitForURL() blocks until the URL matches — safer than a fixed wait
8  await page.waitForURL('/onboarding/step-2');
9  await expect(page.getByRole('heading', { name: 'Step 2' })).toBeVisible();
10
11  await page.getByRole('button', { name: 'Next' }).click();
12
13  // toHaveURL() is the retrying assertion equivalent — use it in expect chains
14  await expect(page).toHaveURL(/step-3/);
15});
#waitForURL#toHaveURL#navigation#redirect#SPA
Cypressselectors

Select element by data-testid attribute

Use a CSS attribute selector to target data-testid — the most stable selector strategy for production suites.

1it('interacts with elements via data-testid', () => {
2  cy.visit('/dashboard');
3
4  // data-testid never breaks due to copy, style, or structure changes —
5  // the gold standard for resilient selectors
6  cy.get('[data-testid="user-menu"]').should('be.visible');
7  cy.get('[data-testid="logout-btn"]').click();
8
9  cy.url().should('include', '/login');
10});
#data-testid#attribute selector#stable selector#get
Cypressselectors

cy.contains — find element by text

Locate elements by their visible text content — scope to a selector to avoid ambiguous matches.

1it('finds elements by visible text', () => {
2  cy.visit('/pricing');
3
4  // cy.contains(selector, text) scopes the search to a specific element type
5  cy.contains('button', 'Get started for free').click();
6
7  // Without a selector it searches all elements — good for quick assertions
8  cy.contains('Welcome to your dashboard').should('be.visible');
9
10  // Regex — case-insensitive partial match
11  cy.contains(/plan details/i).should('be.visible');
12});
#cy.contains#text#visible text#partial match
Cypressselectors

Scope commands with .within()

Use .within() to constrain all cy.get() calls to a specific parent — prevents false positives when the same names appear in multiple forms.

1it('fills billing and shipping forms independently', () => {
2  cy.visit('/checkout');
3
4  // .within() constrains all subsequent cy.get() calls to this element —
5  // prevents matching the same [name="email"] in both forms at once
6  cy.get('[data-testid="billing-form"]').within(() => {
7    cy.get('[name="first-name"]').type('Jane');
8    cy.get('[name="last-name"]').type('Doe');
9    cy.get('[name="postcode"]').type('SW1A 1AA');
10  });
11
12  cy.get('[data-testid="shipping-form"]').within(() => {
13    cy.get('[name="first-name"]').type('John');
14    cy.get('[name="postcode"]').type('EC1A 1BB');
15  });
16});
#within#scope#parent#form#context
Cypressselectors

Narrow a matched set with .filter()

Use .filter() to keep only the elements in a matched set that satisfy a selector or text condition.

1it('targets only sale cards and express radio button', () => {
2  cy.visit('/products');
3
4  // .filter() keeps only elements matching the expression —
5  // uses standard jQuery selector syntax
6  cy.get('[data-testid="product-card"]')
7    .filter(':contains("On Sale")')
8    .should('have.length.greaterThan', 0)
9    .first()
10    .find('[data-testid="sale-badge"]')
11    .should('be.visible');
12
13  // Filter by attribute value
14  cy.visit('/checkout');
15  cy.get('input[type="radio"]')
16    .filter('[value="express"]')
17    .check()
18    .should('be.checked');
19});
#filter#contains#narrow#subset#attribute
Cypressselectors

CSS attribute selectors

Select elements using CSS attribute selectors — exact match, substring, prefix, and presence checks.

1it('selects elements with various attribute patterns', () => {
2  cy.visit('/form');
3
4  // Attribute presence — all required fields
5  cy.get('input[required]').should('have.length.greaterThan', 0);
6
7  // Exact attribute value
8  cy.get('input[type="email"]').type('qa@example.com');
9
10  // Substring match (*=) — any class containing "btn-primary"
11  cy.get('[class*="btn-primary"]').first().click();
12
13  // Prefix match (^=) — data-testid starting with "card-"
14  cy.get('[data-testid^="card-"]').should('have.length', 6);
15
16  // Suffix match ($=) — href ending with ".pdf"
17  cy.get('a[href$=".pdf"]').should('exist');
18});
#attribute selector#CSS#contains#starts-with#required
Cypressauth

Reuse UI login session with cy.session()

Cache a UI-based login session so the login flow runs once per spec rather than before every test.

1// cypress/support/commands.js
2Cypress.Commands.add('loginByUi', (email, password) => {
3  cy.session(
4    [email, password], // cache key — Cypress re-runs setup if this changes
5    () => {
6      cy.visit('/login');
7      cy.get('[name="email"]').type(email);
8      cy.get('[name="password"]').type(password);
9      cy.get('[type="submit"]').click();
10      cy.url().should('include', '/dashboard');
11    },
12    {
13      validate() {
14        // Re-authenticate automatically if this cookie is missing or expired
15        cy.getCookie('auth_token').should('exist');
16      },
17      cacheAcrossSpecs: true, // reuse the same session across all spec files
18    }
19  );
20});
21
22// Usage in any spec:
23// cy.loginByUi('user@test.com', 'secret');
24// cy.visit('/dashboard');
#cy.session#UI login#cache#session#validate
Cypressauth

Login via API in beforeEach

Use a custom API login command in beforeEach — 10× faster than UI login and keeps tests independent.

1// cypress/support/commands.js
2Cypress.Commands.add('loginViaApi', (email, password) => {
3  // API login skips the UI entirely — much faster and more reliable
4  cy.request('POST', '/api/login', { email, password }).then(({ body }) => {
5    window.localStorage.setItem('auth_token', body.token);
6  });
7});
8
9// ── In any spec file ──────────────────────────────────────────
10describe('Protected pages', () => {
11  beforeEach(() => {
12    cy.loginViaApi('user@test.com', 'secret');
13    cy.visit('/dashboard');
14  });
15
16  it('shows the user menu', () => {
17    cy.get('[data-testid="user-menu"]').should('be.visible');
18  });
19
20  it('can access the settings page', () => {
21    cy.visit('/settings');
22    cy.get('h1').should('contain.text', 'Settings');
23  });
24});
#beforeEach#API login#custom command#auth#fast login
Cypressnetwork

Assert intercepted response body

Intercept a real API response, wait for it, then assert the body structure and values.

1it('users API response matches the expected contract', () => {
2  // Alias the intercept so cy.wait() can reference it by name
3  cy.intercept('GET', '/api/users').as('getUsers');
4
5  cy.visit('/users');
6
7  cy.wait('@getUsers').then(({ response }) => {
8    // Assert on the actual response — catches backend contract breaks early
9    expect(response.statusCode).to.equal(200);
10    expect(response.body).to.have.property('users');
11    expect(response.body.users).to.be.an('array').with.length.greaterThan(0);
12    expect(response.body.users[0]).to.include.keys('id', 'email', 'role');
13  });
14});
#intercept#response body#cy.wait#api contract#assert
Cypressassertions

Assert page title

Use cy.title() to retrieve the document title and chain assertions on it.

1it('page has the correct document title', () => {
2  cy.visit('/');
3
4  // cy.title() returns document.title — chain .should() to assert it
5  cy.title().should('eq', 'SnipQA — Playwright & Cypress Snippet Library');
6
7  // Partial match with 'include'
8  cy.title().should('include', 'SnipQA');
9
10  // After navigating to another route, verify the title updated
11  cy.get('a[href="/pricing"]').click();
12  cy.title().should('include', 'Pricing');
13});
#cy.title#page title#document title#SEO
Cypressassertions

Assert current URL

Use cy.url() to retrieve the full current URL and assert it after navigation or redirect.

1it('URL is correct after login redirect', () => {
2  cy.visit('/login');
3
4  cy.get('[name="email"]').type('user@test.com');
5  cy.get('[name="password"]').type('secret');
6  cy.get('[type="submit"]').click();
7
8  // cy.url() automatically retries — handles async redirects from SPAs
9  cy.url().should('include', '/dashboard');
10
11  // Exact match using baseUrl from cypress.config.js
12  cy.url().should('eq', Cypress.config('baseUrl') + '/dashboard');
13
14  // After further navigation
15  cy.get('a[href="/settings"]').click();
16  cy.url().should('match', /\/settings/);
17});
#cy.url#URL#location#redirect#navigation
Cypressassertions

Assert element is disabled or enabled

Check the disabled state of a button or input — commonly used to validate form readiness.

1it('submit button reflects form validity', () => {
2  cy.visit('/register');
3
4  // Assert disabled before the form is filled
5  cy.get('[type="submit"]').should('be.disabled');
6
7  cy.get('[name="email"]').type('qa@example.com');
8  cy.get('[name="password"]').type('Str0ng!Pass');
9
10  // Once valid, the button should become interactive
11  cy.get('[type="submit"]').should('not.be.disabled');
12
13  // Assert a read-only field is not editable
14  cy.get('[name="plan"]').should('be.disabled');
15});
#disabled#enabled#be.disabled#form validation#button state
Cypressassertions

Chain multiple assertions with .and()

Use .and() (alias for .should()) to assert multiple properties on the same element without re-querying.

1it('asserts several properties on the same element', () => {
2  cy.visit('/profile');
3
4  // .and() chains assertions on the same subject — no extra cy.get() calls
5  cy.get('[data-testid="plan-badge"]')
6    .should('be.visible')
7    .and('have.text', 'Pro')
8    .and('have.attr', 'data-plan', 'pro')
9    .and('have.class', 'badge-pro');
10
11  // Works on inputs too
12  cy.get('[name="email"]')
13    .should('be.visible')
14    .and('not.be.disabled')
15    .and('have.attr', 'type', 'email')
16    .and('have.value', 'user@test.com');
17});
#chained assertions#and#should#multiple#chain
Cypressforms

Type into a contenteditable div

Use .type() with {selectall} to interact with rich-text editors and contenteditable elements.

1it('types into a rich-text editor', () => {
2  cy.visit('/editor');
3
4  // {selectall} selects all existing content before typing the replacement —
5  // .clear() does not work on contenteditable elements
6  cy.get('[contenteditable="true"]')
7    .click()
8    .type('{selectall}Hello, Cypress!');
9
10  // Assert via invoke('text') — .should('have.value') doesn't work on div
11  cy.get('[contenteditable="true"]')
12    .invoke('text')
13    .should('include', 'Hello, Cypress!');
14});
#contenteditable#rich text#editor#selectall#type
Cypressfixtures

beforeEach and afterEach hooks

Run shared setup before every test and cleanup after every test in a describe block.

1describe('User profile', () => {
2  beforeEach(() => {
3    // API login is faster than UI login — runs before every test
4    cy.request('POST', '/api/login', {
5      email: 'user@test.com',
6      password: 'secret',
7    }).then(({ body }) => {
8      window.localStorage.setItem('auth_token', body.token);
9    });
10    cy.visit('/profile');
11  });
12
13  afterEach(() => {
14    // Clear auth state between tests to prevent bleed-through
15    cy.clearLocalStorage();
16    cy.clearCookies();
17  });
18
19  it('shows the username', () => {
20    cy.get('[data-testid="username"]').should('be.visible');
21  });
22
23  it('can update the display name', () => {
24    cy.get('[name="display-name"]').clear().type('New Name');
25    cy.get('[type="submit"]').click();
26    cy.contains('Profile updated').should('be.visible');
27  });
28});
#beforeEach#afterEach#hooks#setup#teardown
Cypressnavigation

Assert and interact after a redirect

Verify that a form submission or link triggers a redirect, then assert the landing page rendered correctly.

1it('redirects to dashboard after successful login', () => {
2  cy.visit('/login');
3
4  cy.get('[name="email"]').type('user@test.com');
5  cy.get('[name="password"]').type('secret');
6  cy.get('[type="submit"]').click();
7
8  // cy.url() retries automatically — handles async client-side redirects
9  cy.url().should('include', '/dashboard');
10
11  // Assert the landing page rendered correctly after the redirect
12  cy.get('[data-testid="welcome-banner"]').should('be.visible');
13  cy.get('[data-testid="user-menu"]').should('contain.text', 'user@test.com');
14});
#redirect#navigation#url#login#landing page
Cypresswait

Wait for URL to change after an action

Assert the URL after a button click or form submit — Cypress retries cy.url() automatically for SPA navigation.

1it('URL advances on each step of the onboarding flow', () => {
2  cy.visit('/onboarding/step-1');
3
4  cy.get('[name="company"]').type('Acme Corp');
5  cy.get('[type="submit"]').click();
6
7  // cy.url() retries — no explicit wait needed for most client-side navigations
8  cy.url().should('include', '/onboarding/step-2');
9  cy.get('h1').should('contain.text', 'Step 2');
10
11  cy.get('[type="submit"]').click();
12
13  // For slower SPAs, extend the timeout on the assertion
14  cy.url({ timeout: 10_000 }).should('include', '/onboarding/step-3');
15});
#url#navigation#SPA#redirect#timeout