How to Map WCAG 2.2 Success Criteria to Automated Tests

To turn a WCAG 2.2 success criterion into an automated test, resolve one question first: does your pinned rule engine actually model this criterion, or is it silently unmapped? Where the engine has a check, you inherit its assertion; where it does not — as with several 2.2 additions like SC 2.5.8 Target Size (Minimum) and the resilience-based SC 1.4.12 Text Spacing — you must author a geometry- or layout-aware assertion yourself and emit a three-state result (pass / fail / cantTell) the rest of the pipeline can consume.

This is the hands-on walkthrough under the broader WCAG 2.2 vs 3.0 success criteria taxonomy reference, itself part of the Enterprise WCAG Audit Architecture & Standards Mapping strategy. The taxonomy page defines the registry and the projection model — how one atomic result becomes a 2.2 verdict; this page covers the narrower job of wiring an individual criterion into a running Playwright and axe-core enterprise configuration suite so its result is real rather than assumed.

When This Applies

Reach for a custom assertion only when the criterion is not fully decided by the rule engine you already run. That happens in three recurring cases:

  • The engine has no rule for the criterion. Several WCAG 2.2 additions are geometric or interaction-dependent, and a given pinned axe-core version may ship no automated check for them. A wcag22aa tag filter then returns zero violations for that criterion — not because the page passes, but because nothing was tested.
  • The criterion is about resilience to a user override, not a static value. SC 1.4.12 Text Spacing does not require any particular line height; it requires that no content is lost when the user applies the spec’s spacing overrides. A rule that inspects the shipped stylesheet answers the wrong question.
  • The criterion is only partly machine-decidable. Most criteria have a decidable slice (an image has no alt at all) and an undecidable slice (the alt text is present but meaningless). The automated assertion must own the decidable slice and route the rest to manual review with cantTell, never a silent pass.

If the criterion is fully covered by an ACT rule your engine implements, do not hand-roll it — register the engine’s rule ID against the criterion in the taxonomy layer and move on. This page is for the gaps.

Minimal Reproducible Example

The smallest way to see the problem is a suite that trusts the engine to cover all of WCAG 2.2 AA. A 20-by-20 pixel icon button sails through:

# NAIVE: assumes the rule engine models every WCAG 2.2 AA criterion.
def test_wcag_22_aa(page):
    page.goto("https://app.example.com/checkout")
    results = run_axe(page, tags=["wcag22aa"])   # engine returns zero violations
    assert results["violations"] == []           # reported "AA conformant"

The assertion passes, but the checkout page has a 20px close button that fails SC 2.5.8. The pinned engine version simply has no check wired to that criterion, so the wcag22aa tag never selects one, and the tiny target is reported as conformant. The criterion is unmapped, not satisfied — and a green build hides the difference. The fix is to detect unmapped criteria explicitly and supply an assertion for each.

Correct Implementation

Author the missing assertion so it measures the criterion directly and returns an atomic result the taxonomy layer already understands. Two representative criteria follow: one geometric (2.5.8), one resilience-based (1.4.12).

1. Measure geometry directly for SC 2.5.8 Target Size

The 24-by-24 CSS pixel floor is an absolute measure, not a viewport-relative one, so read each interactive target’s bounding box in CSS pixels and compare. Return cantTell for anything you cannot measure rather than absorbing it as a pass.

# custom_criteria.py — assertions for 2.2 criteria the engine does not model.
MIN_TARGET_PX = 24  # SC 2.5.8 (Minimum): absolute 24x24 CSS px floor.

def assert_target_size(page):
    """One atomic result per pointer target. Spec exceptions (inline links in
    a sentence, user-agent-controlled widgets) must be filtered upstream."""
    targets = page.locator("a[href], button, input, select, [role='button']")
    results = []
    for handle in targets.element_handles():
        box = handle.bounding_box()          # CSS pixels, layout viewport
        if box is None:                      # display:none / off-screen node
            results.append({"sc": "2.5.8", "outcome": "cantTell"})
            continue
        ok = box["width"] >= MIN_TARGET_PX and box["height"] >= MIN_TARGET_PX
        results.append({"sc": "2.5.8", "outcome": "pass" if ok else "fail"})
    return results

Because target size depends on rendered layout, drive the assertion after the page has settled — inject synthetic scroll and pointer emulation first if the target only mounts on interaction, using the same settle discipline as dynamic content boundary detection so you never measure a mid-transition box.

2. Inject the override for SC 1.4.12 Text Spacing

Text Spacing is a resilience test. The correct check applies the spec’s spacing overrides and then inspects the resulting layout for clipping or overlap — it does not read the shipped line-height.

# SC 1.4.12 — inject the user spacing overrides, then check for lost content.
SPACING_OVERRIDES = """
  * { line-height: 1.5 !important;
      letter-spacing: 0.12em !important;
      word-spacing: 0.16em !important; }
  p { margin-bottom: 2em !important; }
"""

def assert_text_spacing(page, selector):
    page.add_style_tag(content=SPACING_OVERRIDES)   # simulate the user override
    clipped = page.locator(selector).evaluate(
        "el => el.scrollHeight > el.clientHeight"
        "   || el.scrollWidth  > el.clientWidth"
    )
    return {"sc": "1.4.12", "outcome": "fail" if clipped else "pass"}

A false negative here almost always traces to the site’s own CSS winning the cascade — an !important line-height that blocks the injected override, so the clipping condition is never exercised. The engine log makes the root cause explicit:

[AUDIT_ENGINE] Rule 1.4.12 execution started.
[STYLE_INJECT] Applied user spacing overrides (line-height:1.5; letter-spacing:0.12em; word-spacing:0.16em).
[CSS_CASCADE] Override blocked: .enterprise-typography { line-height: 1.2 !important; }
[LAYOUT_CHECK] Spacing not applied -> clipping/overlap check could not run.
[RESULT] Violation confirmed: site CSS prevents user spacing overrides from taking effect.

When site styles out-specify the override, raise the injection’s specificity or attach it through document.adoptedStyleSheets, and confirm the computed style actually changed with getComputedStyle() before asserting — otherwise the check silently no-ops into a pass. Validate every emitted result against the shared contract before it leaves the runner; the taxonomy layer relies on JSON Schema validation for accessibility data so a malformed atomic result fails loudly instead of skewing a verdict.

Pipeline Integration

These assertions produce atomic results, so they slot straight into the same projection the taxonomy layer uses — but the deploy gate should not treat every criterion identically. A binary pass/fail on borderline geometric or spacing checks floods the report with noise, so route each mapped result through a tiered gate: structural failures hard-block, heuristic or near-threshold failures soft-block with a tracked ticket and a documented manual-review waiver, and passes within a small computed-value tolerance merge. Feed the failing results into your error categorization triage pipelines and let the level ceiling from the A/AA/AAA compliance level mapping decide which criteria are in scope for the gate, exactly as when setting up AAA compliance thresholds for enterprise apps.

Tiered deploy gate: which atomic results block a deploy and which merge Top-down flowchart. A WCAG 2.2 criterion feeds a geometry or style assertion into a Result? decision. Three branches: structural violation to Tier 1 (amber), heuristic or borderline to Tier 2 (indigo, opens a tracked ticket), and pass within tolerance to Allow merge (green). Tier 2 leads to a manual-review-waiver decision: yes routes to Allow merge, no routes to Hard-block deploy. Tier 1 also routes to Hard-block deploy. WCAG 2.2 criterion geometry / style assertion Result? Tier 1 structural failure Tier 2 borderline · opens tracked ticket pass within tolerance Manual-review waiver? Hard-block deploy Allow merge structural violation heuristic / borderline pass within tolerance no yes · waiver Only structural failures and un-waivered borderline results stop the deploy.

Gotchas

  • Authenticated and multi-tenant states change which targets exist. A target that only renders behind login, or only for a heavier tenant’s feature set, is never measured on an anonymous fixture. Run the geometry pass behind the real auth state and against your densest tenant, or the 2.5.8 result covers a page your users never see.
  • Viewport variance flips both criteria. Responsive layouts mount different controls per breakpoint and reflow text at each width, so a target that clears 24px at 1280px can shrink below it on a mobile drawer, and spacing that fits desktop clips on a narrow column. Pin the viewport explicitly and evaluate per breakpoint rather than trusting a single width.
  • JS-disabled and CSP-locked contexts break style injection. Behind a strict Content Security Policy or WAF, inline style injection for the 1.4.12 check may be blocked, and JavaScript-disabled crawlers never run the geometry pass at all. Serve a prerendered snapshot of the critical checkpoints and route those requests through fallback routing for JS-disabled crawlers so a blocked injection is a known path, not a phantom pass.
Mapping a WCAG 2.2 criterion: inherit the engine rule, or author a custom assertion Top-down flow. A WCAG 2.2 criterion reaches a "modeled by rule engine?" decision. Yes routes right to an inherited engine ACT rule result (green). No routes down to a custom-assertion stage that splits into two boxes: geometry for SC 2.5.8 (amber, measure bounding box against the 24 by 24 CSS pixel floor) and resilience for SC 1.4.12 (indigo, inject the spacing override, confirm the cascade, check for clipping). Both boxes emit one atomic result shown as three pills — pass (green), fail (amber), cantTell (indigo) — which flows into a shared projection producing the 2.2 verdict. The engine-inherited result also feeds that projection. WCAG 2.2 criterion Modeled by rule engine? yes · inherit engine's ACT rule result inherited no · unmapped gap author a custom assertion — the engine has no check for this SC Geometry · SC 2.5.8 Target Size read each target's bounding box in CSS px compare against the 24 × 24 floor Resilience · SC 1.4.12 Text Spacing inject spacing override, confirm cascade then check layout for clipping / overlap pass fail cantTell shared projection → WCAG 2.2 verdict engine-mapped result feeds the same projection