FAQ
"I passed runOnly: ['some-rule-id'] but every rule still ran"
runOnly must be an object, not a bare array. runOnly: ['img-alt-present'] is silently ignored (the engine falls through to "run everything"), because that shape has none of the fields the engine actually checks (includeRuleIds, tags, etc.). A bare array is easy to reach for, so this is worth checking first.
Fix:
runOnly: { includeRuleIds: ['img-alt-present'] }
See ENGINE_OPTIONS.md for the full shape.
"My custom rule always returns cantTell with no clear reason"
Check the result's error field first — if it says "<something> is not defined", your runInPage references a variable from outside the function body (a module-scope const, an imported helper, anything not reached through ctx.*). This is a real, common footgun: runInPage is serialized to source text and re-evaluated later in the page context, so the build never catches this — only running the rule does, and the failure looks like a normal (if uninformative) result, not a crash. See RULE_AUTHORING.md §1.1 for the full explanation and the fix (move the value inside runInPage, or route it through ctx.rule/ctx.helpers).
"A geometry-dependent rule (e.g. target-size-minimum) always says notApplicable"
Plain jsdom (no real browser) doesn't implement CSS layout — getBoundingClientRect() always returns zero geometry. Rules that need real layout report notApplicable under jsdom rather than guess. Run through a real browser instead (Puppeteer/Playwright — see INTEGRATION.md Pattern 2) to get real findings from these rules. See LIMITATIONS.md.
"I only see fail/cantTell occurrences — where's the list of elements that passed?"
By design, this engine never enumerates the elements a rule passed — only the ones it flagged. A rule's overall outcome: 'pass' means "applicable target(s) existed and none were flagged," but occurrences is [] either way. If you need to know which specific elements were checked and considered fine, that's not currently exposed — see OUTPUT_SCHEMA.md.
"A rule I expected to fire returned notApplicable / found nothing on a page I know has the issue"
Two common causes, in order of likelihood:
- The element is excluded from the accessibility tree —
aria-hidden="true", display: none, visibility: hidden, hidden, or an inert ancestor. Most rules skip content that's already invisible to assistive technology, since checking a hidden element would be meaningless and could produce a misleading fail on content no user encounters. Some rules explicitly opt out of this gating when it wouldn't make sense to (e.g. no-autoplay-audio — hidden audio still plays sound) — check the specific rule's file header comment (@applicability) in src/checks/. excludeSelectors — if you've configured this (directly or inherited from a shared config), confirm the element in question isn't matched by it. Remember this can also be scoped to a single rule via engineOptions.rules[ruleId].excludeSelectors (see ENGINE_OPTIONS.md) — if a rule you expect to fire keeps coming back notApplicable/pass for one element only, check whether that rule specifically has its own exclude list configured, not just the global one.
"Does a clean scan (pass everywhere) mean the page is WCAG conformant?"
No — see WCAG_CONFORMANCE.md. A pass means every automatable check came back clean. A meaningful fraction of WCAG requires human judgment (accurate alt text, understandable error messages) or dynamic testing this engine's architecture can't do at all (keyboard traps, reflow at zoom) — see LIMITATIONS.md for the explicit, non-exhaustive-on-purpose list.
"Should I treat cantTell as a failure?"
Treat it as "needs a human to look" — it's neither pass nor fail by design. Most teams log cantTell findings without failing CI on them, since failing a build on something the engine explicitly couldn't determine tends to train people to ignore the gate. See POLICY.md if you want to reshape this behavior (e.g. via a custom policy contract), and INTEGRATION.md for a concrete CI-gating example.
"What happens if a locale is only partially translated?"
Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright. See I18N.md for the mechanism and current coverage. Key parity is enforced rather than hoped for: npm run i18n:sync carries any new or renamed en.json key into every other locale file, and the build fails if one is out of step. A key it adds holds the English text until someone translates it, so a locale can be behind on wording without ever being behind on keys.
"runDomRulesInPage vs runa11yCoreInPage — which one do I want?"
runDomRulesInPage if you're calling it directly in the same Node process (jsdom, browser-extension content script). runa11yCoreInPage if you're handing the function itself to a different JS realm — almost always page.evaluate in Puppeteer/Playwright, which serializes the function to source text and re-runs it inside the browser tab (which has no access to your Node module scope). See INTEGRATION.md for both patterns worked out in full.