The SDK
Widths, themes and personas
A rule only sees the page it ran on, at the width, in the theme and as the person your test chose.
Scanning at more than one viewport
A rule only ever sees the viewport it ran in. A navigation that collapses into a menu below a breakpoint is a different document, not a narrower one, so a scan at 1280 has no opinion about the same page at 390.
Setsquare cannot widen that for you — it scans the pages your suite drives, at the sizes your suite drives them, and never resizes to capture a width of its own. A project per width does, and it needs no changes to your tests:
export default withSetsquare(
defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{
name: 'tablet',
testMatch: /a11y\.spec\.ts/,
use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } },
},
{
name: 'mobile',
testMatch: /a11y\.spec\.ts/,
use: { ...devices['Desktop Chrome'], viewport: { width: 390, height: 844 } },
},
],
}),
);Spreading Desktop Chrome and overriding only viewport holds the engine
constant, so the width is the only variable — devices['iPhone 15'] is WebKit
and changes both at once. testMatch keeps the extra projects to the specs whose
layout is the subject; a spec that sets its own viewport, or that checks request
origins or resolves links, returns the same answer at every width. And 320 needs
no project of its own: setsquare-reflow measures WCAG 1.4.10 there on every
surface already, by resizing at teardown and putting the viewport back.
The viewport is part of a scan's identity, so evidence from several widths survives instead of collapsing into one row, and the report, the contact sheet and the per-surface tables group by band — laptop 1024px and wider, tablet 768px to 1023px, mobile narrower than 768px — with the exact size beside every name. Those names describe a width and never a device: a 640px viewport is in the mobile band whether it is a phone or a desktop browser with text at 200%, and every artifact that prints a band name prints that caveat with it.
With one viewport in the run, Setsquare says nothing about narrow widths. Not "no narrow-viewport failures found" — that would be a claim about your phone layout from a run that never looked at it.
2. Themes: dark, high contrast, whatever you call yours
Setsquare has no built-in list of theme names. Yours are yours.
If a Playwright project is a theme, map project name to theme name:
// withSetsquare(yourConfig, setsquareOptions) — two arguments, config first.
// Setsquare's options never go inside your config object.
export default withSetsquare(
defineConfig({
projects: [
{ name: 'dark-desktop', use: { colorScheme: 'dark' } },
// `forcedColors` and `reducedMotion` are NOT accepted directly under `use:`
// — Playwright does not declare them there, and `use: { forcedColors: … }`
// does not compile. They go through `contextOptions`.
{ name: 'hc-desktop', use: { contextOptions: { forcedColors: 'active' } } },
],
}),
{ themes: { 'dark-desktop': 'dark', 'hc-desktop': 'forced-colors' } },
);If your themes are toggled inside tests — a data-theme attribute, a class on
<html> — you do not have to configure anything. Setsquare reads the theme off the
page, which is the only source that works for that pattern.
And if neither fits, the callback is the general escape hatch:
export default withSetsquare(defineConfig(yourConfig), {
theme: ({ project, urlPattern, observedTheme }) =>
project.endsWith('-hc') ? 'high-contrast' : observedTheme,
});Config outranks observation — you telling us beats us sniffing — and the callback outranks the map.
Worth knowing: with one theme in the run, Setsquare says nothing about theme-specific failures. Not "no theme-specific failures found", which would be a claim about your dark mode from a run that never looked at it.
4. Personas: who was looking at the page
Same shape as themes, and the same reason: a finding on a page an admin sees and a finding on a page everyone sees are different findings.
export default withSetsquare(
defineConfig({
projects: [
{ name: 'recruiter', use: { storageState: '.auth/recruiter.json' } },
{ name: 'candidate', use: { storageState: '.auth/candidate.json' } },
{ name: 'admin', use: { storageState: '.auth/admin.json' } },
],
}),
{ personas: { recruiter: 'recruiter', candidate: 'candidate', admin: 'admin' } },
);Then setsquare audit will tell you something like "3 roles configured, 1 with
evidence" — which is usually the most uncomfortable line in the report, and the most
useful.