Automating Responsive Testing Across Breakpoints
AI generated
PASS
expect()
Testing · Cypress · Playwright · Responsive Design
Automating Responsive Testing Across Breakpoints
One test suite, every screen size

An E2E test that only checks the default resolution misses most of the layout bugs real users actually run into. This article shows how to configure Cypress and Playwright so the same test suite automatically runs across mobile, tablet, and desktop breakpoints, reliably catches broken navigation, overlapping elements, and text truncation, and maintains screenshot baselines per screen size, without duplicating a single test.

13 min. read Breakpoints · Viewport Testing · Visual Regression Cypress 13 · Playwright 1.4x · Tailwind CSS v4

1. Why responsive tests need their own breakpoints

Most E2E suites run against a single, fixed viewport, often 1280x720 or whatever the default size of the test runner happens to be. That's enough to confirm a feature basically works, but it says nothing about whether the same page is actually usable on a smartphone. In e-commerce shops in particular, a large share of traffic comes from mobile devices, and that's exactly where layouts break most often: buttons drift below the fold, forms become unusable, or the main menu disappears entirely because the Alpine.js toggle behind it was never tested under realistic conditions.

Responsive testing means running the same test suite deliberately at multiple screen sizes, instead of just clicking through breakpoints manually in a browser. The key is not to pick test sizes arbitrarily, but to align them with the breakpoints actually used in the CSS, namely Tailwind's sm/md/lg/xl scale. That way tests cover exactly the transitions where the layout genuinely changes, instead of checking random in-between sizes where nothing breaks anyway.

2. Defining breakpoints: the Tailwind scale as a testing foundation

Tailwind CSS defines breakpoints as minimum widths: sm from 640px, md from 768px, lg from 1024px, xl from 1280px, and 2xl from 1536px. In the Hyvä theme, these values live in the @theme block as part of Tailwind v4's CSS-first configuration. For tests, inventing your own pixel values makes little sense. Instead, test just below and just above every boundary, for example 375px for mobile devices below sm, 768px right at the md transition, and 1280px for desktop starting at xl.

The core rule against duplication: define breakpoints exactly once, in a shared configuration file, and import them from there into both Cypress and Playwright tests. If a breakpoint changes in the Tailwind theme, only one place in the test code needs updating. Without this single source of truth, test viewports and actual CSS breakpoints drift apart over time, and tests end up checking sizes that no longer matter in production code at all.


/* Tailwind v4 CSS-first breakpoint tokens - the single source of truth for the theme */
@theme {
  --breakpoint-sm: 640px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 1024px;
  --breakpoint-xl: 1280px;
  --breakpoint-2xl: 1536px;
}

/* Test viewports mirror these tokens, offset just below/above each boundary */

3. Cypress: viewport commands and parametrized tests

cy.viewport(width, height) sets the window size for the current test and accepts either pixel values or predefined presets like iphone-x or ipad-2. The command alone, however, isn't enough to avoid duplication. The clean pattern is to define a list of breakpoints and generate a separate describe block per breakpoint with Object.entries().forEach(). Cypress has no native describe.each, but looping over an array delivers exactly the same result: the same test code runs unchanged for every screen size, only the viewport and the expected visibility change.

It's important to set the viewport inside a beforeEach rather than after cy.visit(), since some layout calculations and media-query listeners already kick in during the initial render. Assertions inside the parametrized test should explicitly check breakpoint-dependent conditions, such as whether the window width is below the md value, instead of hand-writing a separate, near-identical test case for every size.


// cypress/support/breakpoints.js - single source of truth, mirrors the Tailwind scale
export const BREAKPOINTS = {
  mobile:  { width: 375,  height: 667  }, // below sm
  tablet:  { width: 768,  height: 1024 }, // md
  desktop: { width: 1280, height: 800  }, // xl
};

// cypress/e2e/responsive/nav.cy.js
import { BREAKPOINTS } from '../../support/breakpoints';

Object.entries(BREAKPOINTS).forEach(([name, size]) => {
  describe(`Navigation at ${name} (${size.width}x${size.height})`, () => {
    beforeEach(() => {
      cy.viewport(size.width, size.height);
      cy.visit('/');
    });

    it('shows the correct nav pattern for this breakpoint', () => {
      if (size.width < 768) {
        cy.get('[data-testid="nav-hamburger"]').should('be.visible');
        cy.get('[data-testid="nav-desktop-links"]').should('not.be.visible');
      } else {
        cy.get('[data-testid="nav-hamburger"]').should('not.be.visible');
        cy.get('[data-testid="nav-desktop-links"]').should('be.visible');
      }
    });
  });
});

4. Playwright: projects and device presets for breakpoints

Playwright solves the same problem at the configuration level instead of in the test code. In playwright.config.js, the projects array defines any number of execution contexts, each with its own viewport, its own browser, or even its own device preset from the devices module. The same set of spec files then runs automatically against every project, with no forEach loops needed in the test itself. That's the big structural difference from Cypress: parametrization happens in the runner, not in the test code.

For breakpoint tests, it's best to combine custom viewport values with the devices presets, which also bring realistic user-agent strings, touch support, and pixel ratio along with them. A mobile-375 project using the iPhone 13 preset tests not just width, but also touch events and viewport-meta behavior, which can cause rendering to differ from real devices. Running npx playwright test --project=mobile-375 lets you isolate a single breakpoint on its own, which saves time when debugging a specific layout bug locally.


// playwright.config.js - one project per breakpoint, same spec files reused everywhere
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/responsive',
  projects: [
    {
      name: 'mobile-375',
      use: { ...devices['iPhone 13'], viewport: { width: 375, height: 667 } },
    },
    {
      name: 'tablet-768',
      use: { viewport: { width: 768, height: 1024 } },
    },
    {
      name: 'desktop-1280',
      use: { viewport: { width: 1280, height: 800 } },
    },
    {
      name: 'desktop-1536',
      use: { viewport: { width: 1536, height: 900 } },
    },
  ],
});

// tests/responsive/nav.spec.js - runs unchanged against every project defined above
import { test, expect } from '@playwright/test';

test('nav pattern matches the active viewport', async ({ page, viewport }) => {
  await page.goto('/');
  const isMobile = (viewport?.width ?? 0) < 768;
  const hamburger = page.getByTestId('nav-hamburger');
  await expect(hamburger).toBeVisible({ visible: isMobile });
});

Navigation is the element that changes most drastically between breakpoints, structurally, not just visually. Below md, Hyvä typically shows a hamburger icon that reveals an off-canvas menu via Alpine.js x-show, while above md the full desktop nav bar is visible directly. A test that only checks desktop selectors completely misses whether the mobile menu works at all, and a test that only runs at mobile sizes misses regressions in the desktop nav. Both paths need their own, conditional assertions.

Beyond plain visibility, it's worth checking aria-expanded on the hamburger button before and after the click, since this attribute is often the source of truth for both screen readers and CSS transitions. For off-canvas menus, focus trapping matters too: once opened, focus should move into the menu, and tab navigation shouldn't be able to leave it while it's open. These behavioral differences between breakpoints can be checked cleanly in Playwright via the viewport fixture, and in Cypress via the breakpoint loop shown in the previous section.

6. Detecting layout bugs: overflow, overlap, text truncation

Three classes of bugs keep showing up at specific breakpoints. Horizontal overflow happens when an element is wider than its container, for example a table that doesn't wrap or an image that's too wide without max-width: 100%. It can be detected reliably and automatically by comparing document.documentElement.scrollWidth against window.innerWidth. If the scroll value is larger, unwanted horizontal scroll exists at that width, regardless of which element is causing it.

Overlapping elements commonly show up with absolutely positioned badges, sticky headers, or dropdown menus that run out of room at certain widths. getBoundingClientRect() lets you check whether the rectangles of two elements that, by design, should never overlap actually intersect. Text truncation bugs show up when text shortened with text-overflow: ellipsis gets cut so aggressively at smaller breakpoints that it becomes unreadable. A simple, reliable check compares element.scrollWidth with element.clientWidth and fails once the truncated portion crosses a defined threshold, instead of just checking that some text is present at all.

7. Viewport-specific screenshot baselines for visual regression

Functional assertions like visibility and attributes catch structural bugs, but not purely visual regressions like shifted spacing or wrong colors. That requires visual regression testing with screenshot comparison, and the baseline images absolutely must be kept separate per breakpoint. A single baseline set with no breakpoint awareness leads to constant false positives, because the exact same component is arranged differently at 375px than at 1280px, even though both states are correct.

Both Playwright (toHaveScreenshot()) and Cypress with a plugin like cypress-image-diff automatically append the project or browser name to the baseline filename, so nav.spec.js-mobile-375-chromium.png and nav.spec.js-desktop-1280-chromium.png are maintained independently. For stable baselines, it's important to disable CSS animations before taking the screenshot, mask dynamic content like dates or cart counters, and update baselines deliberately and individually rather than regenerating the entire image collection wholesale.


# Update visual regression baselines only for the breakpoints that actually changed
npx playwright test --project=mobile-375 --update-snapshots
npx playwright test --project=tablet-768 --update-snapshots

# Run the full breakpoint matrix and diff against the existing baselines
npx playwright test tests/responsive/visual.spec.js

# Cypress equivalent: cypress-image-diff writes one baseline set per viewport name
npx cypress run --spec "cypress/e2e/responsive/visual.cy.js" --env updateSnapshots=true

8. CI matrix: running breakpoints in parallel in the pipeline

If every breakpoint runs sequentially in a single CI job, each breakpoint's runtime adds directly to the pipeline's total runtime, and a single flaky test blocks every subsequent breakpoint run. The more robust solution is a matrix strategy: each breakpoint runs as its own, parallel job, using the Playwright project name or the Cypress grep tag as the matrix variable. If one breakpoint fails, the others still run to completion, and the failure is immediately tied to a specific breakpoint.

fail-fast: false is essential here, because CI runners cancel the entire matrix by default the moment a single job fails. For responsive tests that's counterproductive, since a failed mobile test says nothing about the desktop state. Uploading artifacts for failed screenshots per matrix entry speeds up debugging further too, since reviewers can open the visual diff directly in the CI interface without having to reproduce the test locally.


# .github/workflows/responsive-tests.yml
name: Responsive E2E Tests
on: [pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        breakpoint: [mobile-375, tablet-768, desktop-1280, desktop-1536]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - name: Run breakpoint project
        run: npx playwright test --project=${{ matrix.breakpoint }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report-${{ matrix.breakpoint }}
          path: playwright-report/

9. Responsive testing patterns compared side by side

Many teams start out testing responsive layouts ad hoc, with hardcoded pixel values scattered across individual test files and no clear structure for baselines or CI parallelization. The table below shows which patterns actually hold up and which ones quickly turn into a maintenance burden as the test suite grows.

Test scenario Risky / Error-prone Recommended pattern Benefit
Viewport definition cy.viewport(390, 844) per test file Import breakpoints from a central fixture No drift between CSS and tests
Test coverage One test for every screen size Parametrized per breakpoint (forEach / projects) Covers real layout bugs
Navigation assertions Only desktop selectors checked Conditional assertions for hamburger and desktop nav Catches broken mobile menus
Screenshot baselines One baseline set with no breakpoint awareness Baseline filename includes breakpoint suffix Avoids false positives
CI execution All breakpoints sequential in one job Matrix strategy with one job per breakpoint Faster pipeline, isolated failures
Device emulation Only viewport size simulated devices presets with real UA strings and touch More realistic interaction behavior

The common denominator across every recommended pattern is centralization: one source of breakpoints, one parametrized testing logic, one clear naming convention for baselines. Get these three things right and you can extend the test suite with new breakpoints without touching any existing test code, just the central configuration.

Mironsoft

E2E test automation, visual regression testing, and CI pipelines for Magento and Hyvä stores

Want responsive tests that actually cover every breakpoint?

We build parametrized Cypress and Playwright suites for your store, set up screenshot baselines per breakpoint, and integrate the full matrix into your CI pipeline, so layout bugs get caught before customers see them.

Test setup

Setting up breakpoint fixtures, parametrized specs, and Playwright projects

Visual regression

Screenshot baselines per breakpoint, masking dynamic content

CI integration

Matrix strategy, parallel jobs, and artifact reports for every breakpoint

10. Summary

Responsive testing across breakpoints solves a simple but consequential problem: a test at a single screen size says nothing about the actual user experience on every other device class. Breakpoints aligned with Tailwind's sm/md/lg/xl scale, imported from a central fixture, and reused in both Cypress loops and Playwright projects prevent duplication and drift between CSS and test code. Navigation, horizontal overflow, overlapping elements, and text truncation can all be caught reliably and automatically with targeted, breakpoint-aware assertions.

Screenshot baselines are only trustworthy when kept strictly separate per breakpoint, and a CI matrix with fail-fast: false ensures a failed mobile test doesn't block evaluation of the desktop results. Once these building blocks are set up cleanly, extending the test suite with new breakpoints only requires adjusting the central configuration, with no duplication of existing test code at all.

Responsive Testing Across Breakpoints - The Essentials at a Glance

Central breakpoints

One fixture file, aligned with Tailwind's sm/md/lg/xl scale, imported into both Cypress and Playwright.

Parametrized tests

forEach over breakpoints in Cypress, a projects array in Playwright, zero test duplication.

Catching layout bugs

Overflow via scrollWidth, overlap via getBoundingClientRect(), truncation via width comparison.

CI matrix

One parallel job per breakpoint, fail-fast: false, artifact upload for failed screenshots.

11. FAQ: Responsive Testing Across Breakpoints

1What are breakpoints in the context of E2E tests?
Viewport widths at which the layout changes structurally. Tests run deliberately at the widths also used as media-query boundaries in the CSS.
2Which viewport sizes should I test?
At minimum mobile below 640px, the md transition at 768px, and desktop from 1280px. For wide desktops, add 1536px for 2xl.
3How do I parametrize tests in Cypress?
Define a breakpoint list, use Object.entries().forEach() to generate a describe block per entry that sets cy.viewport() in beforeEach.
4How do Playwright projects work?
One execution context per breakpoint in the projects array, each with its own viewport or devices preset. The same specs run automatically against every project.
5How do I test hamburger menu vs. desktop nav?
Conditional assertions per breakpoint, plus checking aria-expanded and focus trapping inside the open off-canvas menu.
6How do I detect horizontal overflow automatically?
Compare document.documentElement.scrollWidth with window.innerWidth. A larger scroll value means unwanted horizontal scroll.
7How do I avoid flakiness in screenshots?
Disable animations, mask dynamic content, keep baselines strictly separate per breakpoint.
8How do I parallelize breakpoint tests in CI?
Matrix strategy with one job per breakpoint, fail-fast: false prevents the other jobs from being canceled on a single failure.
9Real devices or just viewport sizes?
devices presets combine width with real UA strings, touch, and pixel ratio, covering more real-world differences than pure width changes.
10How often should the full matrix run?
On every pull request with layout or CSS changes. For pure backend changes, a single default viewport is usually enough.