Domphy

checkLayout

checkLayout is the main orchestration entry point for @domphy/audit. It runs all five audit checks in parallel on the current page state and returns a combined AuditResult.

import { checkLayout } from "@domphy/audit"

const result = await checkLayout(page)
console.log(result.ok)       // boolean — false if any issues found
console.log(result.issues)   // AuditIssue[]
console.log(result.svg)      // annotated SVG string

Signature

function checkLayout(
  page: AuditPage,
  options?: AuditOptions
): Promise<AuditResult>

AuditPage is a minimal interface compatible with any Playwright Page object:

interface AuditPage {
  evaluate<T>(fn: () => T | Promise<T>): Promise<T>
  evaluate<T, A>(fn: (arg: A) => T | Promise<T>, arg: A): Promise<T>
}

Pass a Playwright page directly — it satisfies the interface.

AuditOptions

interface AuditOptions {
  checks?: ("overlap" | "geometry" | "contrast" | "theme" | "overlay")[]
  tolerance?: number
  minContrast?: number
}

checks — which checks to run

Default: ["theme", "overlap", "geometry", "contrast", "overlay"] (all five).

Omit checks that are not relevant to the page under test:

// Only run contrast and theme checks
const result = await checkLayout(page, {
  checks: ["theme", "contrast"]
})

// Run everything except overlap (page intentionally layers elements)
const result = await checkLayout(page, {
  checks: ["theme", "geometry", "contrast", "overlay"]
})

// Run a single check
const result = await checkLayout(page, { checks: ["contrast"] })

Checks that are excluded from checks return zero issues. The SVG snapshot is always collected regardless of checks.

tolerance — geometry deviation in pixels

Default: 1 (px).

Used by the geometry check. A Domphy button's rendered height is compared against the expected value from the formula (6 + 2d) × U. If the deviation exceeds tolerance, the button is flagged.

// Allow up to 2px deviation — useful for subpixel rounding on HiDPI displays
const result = await checkLayout(page, { tolerance: 2 })

Has no effect when "geometry" is not included in checks.

minContrast — minimum WCAG contrast ratio

Default: 4.5 (WCAG AA for normal text).

Used by the contrast check. Any text element with a ratio below this threshold is flagged.

// WCAG AA: 4.5 (default)
const aa = await checkLayout(page, { minContrast: 4.5 })

// WCAG AA for large text (18pt+): 3.0
const large = await checkLayout(page, { minContrast: 3.0 })

// WCAG AAA for body text: 7.0
const aaa = await checkLayout(page, { minContrast: 7.0 })

Has no effect when "contrast" is not included in checks.

Execution model

checkLayout runs all selected checks in parallel via Promise.all, plus one additional snapshot call for the SVG. Total browser round-trips: 1 per check + 1 for the snapshot. For the default 5-check run, that is 6 concurrent page.evaluate() calls.

checkTheme(page)
detectOverlaps(page)     ← all 5 run concurrently
verifyGeometry(page, tolerance)
checkContrast(page, minContrast)
checkOverlays(page)
snapshot(page)           ← always runs for SVG

Issues from all selected checks are merged into a single issues array. The svg is built from the snapshot and all collected issues.

Calling individual checks

All check functions are exported individually. Call them directly when you need results from a single check without the overhead of checkLayout:

import {
  checkTheme,
  detectOverlaps,
  verifyGeometry,
  checkContrast,
  checkOverlays,
} from "@domphy/audit"

// Run only the contrast check
const contrastIssues = await checkContrast(page)           // default 4.5:1
const strict = await checkContrast(page, 7.0)              // custom threshold

// Run only the geometry check
const geometryIssues = await verifyGeometry(page)          // default tolerance 1px
const loose = await verifyGeometry(page, 2)               // 2px tolerance

// Run theme, overlap, and overlay checks
const [theme, overlaps, overlays] = await Promise.all([
  checkTheme(page),
  detectOverlaps(page),
  checkOverlays(page),
])

The individual functions each return Promise<AuditIssue[]> — not AuditResult.

checkLayout vs scanInteractive

checkLayoutscanInteractive
Page interfaceAuditPageAuditPageFull
Requires hover()NoYes
Geometry checkYes (default)No
Hover trigger discoveryNoYes
OptionsAuditOptions{ hoverDelay, staticOnly }
Configurable checksYes (checks array)No (fixed set)
Deduplicates issuesNoYes

checkLayout is the right choice for:

  • Most page-level regression tests
  • CI audit jobs where interactive hover is not needed
  • When you need AuditOptions to tune thresholds or skip checks

scanInteractive is the right choice for:

  • Pages with hover-triggered dropdowns, menus, or popovers
  • When you want automatic overlay trigger discovery
  • Full end-to-end interactive audits

Note: scanInteractive does not run the geometry check. If you need to test button geometry on a page that also has interactive overlays, run both:

import { checkLayout, scanInteractive } from "@domphy/audit"
import type { AuditPageFull } from "@domphy/audit"

await page.goto("/")
await page.waitForLoadState("networkidle")

// Run static checks including geometry
const static_ = await checkLayout(page)

// Run interactive checks for overlays
const interactive = await scanInteractive(page as AuditPageFull)

// Merge and report
const allIssues = [...static_.issues, ...interactive.issues]
if (allIssues.length > 0) {
  for (const issue of allIssues) {
    console.log(`[${issue.type}] ${issue.message}`)
  }
}

Full example

import { test, expect } from "@playwright/test"
import { checkLayout } from "@domphy/audit"
import { writeFileSync } from "node:fs"

test.describe("layout audit", () => {
  test("homepage — all checks", async ({ page }) => {
    await page.goto("/")
    await page.waitForLoadState("networkidle")

    const result = await checkLayout(page)

    if (!result.ok) {
      writeFileSync("test-results/audit-homepage.svg", result.svg)
      console.table(
        result.issues.map((i) => ({
          type: i.type,
          message: i.message.slice(0, 80),
          x: i.rect?.x,
          y: i.rect?.y,
        }))
      )
    }

    expect(result.ok).toBe(true)
  })

  test("settings page — contrast only", async ({ page }) => {
    await page.goto("/settings")
    await page.waitForLoadState("networkidle")

    // Only audit contrast; skip geometry/overlap which are tested elsewhere
    const result = await checkLayout(page, {
      checks: ["contrast"],
      minContrast: 4.5,
    })

    expect(result.issues).toHaveLength(0)
  })

  test("editor page — loose geometry tolerance", async ({ page }) => {
    await page.goto("/editor")
    await page.waitForLoadState("networkidle")

    // Editor uses a canvas + toolbar; allow 2px rounding in button geometry
    const result = await checkLayout(page, {
      checks: ["theme", "contrast", "geometry"],
      tolerance: 2,
    })

    expect(result.ok).toBe(true)
  })
})