Skip to main content

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:

ts
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:

ts
// 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:

ts
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.

ts
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.