Guide — responsive regression in CI
Three escalating setups, all built on Playwright.
1. Assertions in a test
The quickest start: sweep and assert inline.
import { test, expect } from '@playwright/test';
import { r$ } from '@responsivejs/design';
test('pricing page layout', async ({ page }) => {
const r = r$(page);
await r.sweep({
url: '/pricing',
widths: [320, 768, 1280, 1920],
selectors: ['h1', '.plan-card', '.cta'],
});
r.assert
.noOverflow()
.childrenEqualWidth('.plan-grid')
.minSize('.cta', { height: 44 })
.breakpointSafe([768, 1280]);
expect(r.report().pass, r.log('console') && '').toBe(true);
});Tip: breakpointSafe re-measures at bp±1 — add those widths (or use a contract, which does it for you).
2. The full oracle
One call adds the aesthetic score and a11y (when axe-core is installed):
const report = await r.analyze(); // reuses the sweep
expect(report.pass).toBe(true); // fails only on error severity
console.log(formatCompact(report)); // r$ PASS E0/W2/I1 (312 checks, 4 widths)Publish machine-readable results to code-scanning ecosystems:
import { writeFileSync } from 'node:fs';
import { formatSARIF } from '@responsivejs/design';
writeFileSync('responsive.sarif', formatSARIF(report, { toolVersion: '1.0.0' }));# .github/workflows/ci.yml — surfaces violations in the Security tab / PR annotations
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: responsive.sarif }3. Contracts (the durable setup)
Freeze expectations in a versioned JSON file next to the code:
// tests/contracts/build-home-contract.ts — run once, commit the output
import { writeFileSync } from 'node:fs';
import { contract } from '@responsivejs/contract';
const home = contract('home')
.viewport({ widths: [320, 768, 1280] })
.select('nav', '.main-nav')
.assert('noOverflow', undefined, { id: 'no-bleed' })
.below(768).assert('hidden', { selector: '$nav' }, { id: 'nav-mobile', description: 'nav collapses to burger' })
.from(768).assert('visible', { selector: '$nav' }, { id: 'nav-desktop' })
.score({ min: 0.55 })
.baseline('$nav', 'width', { px: 4 })
.build();
writeFileSync('tests/contracts/home.contract.json', JSON.stringify(home, null, 2));The CI test is five lines, forever:
import home from './contracts/home.contract.json' with { type: 'json' };
import { verifyContract, formatContractConsole } from '@responsivejs/design';
test('home keeps its design contract', async ({ page }) => {
await page.goto('/');
const report = await verifyContract(home, page);
expect(report.pass, formatContractConsole(report)).toBe(true);
});Recording baselines
Baselines turn "it looked right when we shipped it" into data:
// After an approved visual change — re-pin and commit:
const r = r$(page);
await r.sweep({ url: '/', widths: [320, 768, 1280], selectors: ['.main-nav'] });
const repinned = recordBaseline(home, r.raw);
writeFileSync('tests/contracts/home.contract.json', JSON.stringify(repinned, null, 2));From then on, verifyContract fails when the measured curve drifts beyond tolerance — geometric visual regression without screenshots.
Editor support
Add the schema for autocomplete and inline validation of contract files:
{ "$schema": "https://raw.githubusercontent.com/AleSaiani/ResponsiveJS/main/packages/contract/schema/design-contract.v1.json" }Browser installation in CI
- run: pnpm exec playwright install --with-deps chromiumUnit-level checks (analyzeStore, verifyContract(contract, store), all constraint logic) run in plain Node — no browser needed. Keep the browser job separate from your test matrix.