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
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});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 sessionWait 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});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});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.pngCustom 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 }) => { ... });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});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});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});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 authenticatedWait 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});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');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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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}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});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 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});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});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});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});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});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});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});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');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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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=firefoxMock 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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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});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}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});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});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});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});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});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});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');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});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});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});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});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});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});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});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});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});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});