From 1f30d8aff6ef097a78b5a7febcb447d80cb56bfd Mon Sep 17 00:00:00 2001 From: Duncan Faulkner Date: Sat, 3 Oct 2026 22:34:31 +0100 Subject: [PATCH] fix(report): hide the in-app overlay before scanning, so contrast below the fold is checked (0.15.8) The overlay's tab-order path is a fixed SVG that overflows the viewport. axe's color-contrast counted it as covering text below the fold and returned incomplete (bgOverlap), which report mode doesn't list, so contrast below the first 900px went unreported in light and dark passes. Report mode now hides [data-ngb-a11y-overlay] before axe runs. New E2E page reproduces it and fails without the fix. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/report/headless.ts | 12 ++++++++++++ src/testing/e2e-report.spec.ts | 25 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07eb6ba..888081e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to `@ngbracket/a11y-devtools` are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## 0.15.8 + +### Fixed + +- Report mode now checks colour contrast below the fold when the app runs the + in-app overlay. The overlay's tab-order path is drawn on a fixed SVG that + runs past the bottom of the viewport. axe counted it as covering the text + there, so its contrast check returned "needs review" instead of a result, + and report mode doesn't list those. Contrast problems on parts of a page + below the report's 900px-tall viewport could go unreported, in light and + dark passes. Report mode now hides the overlay before it scans. On the + public demo this brings back two dark-mode contrast failures the dark pass + had missed. + ## 0.15.7 ### Fixed diff --git a/package-lock.json b/package-lock.json index 94b9af8..5b49207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ngbracket/a11y-devtools", - "version": "0.15.7", + "version": "0.15.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ngbracket/a11y-devtools", - "version": "0.15.7", + "version": "0.15.8", "license": "MIT", "dependencies": { "axe-core": "^4.13.0" diff --git a/package.json b/package.json index e4d462e..aa0ef97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ngbracket/a11y-devtools", - "version": "0.15.7", + "version": "0.15.8", "description": "Finds accessibility problems in Angular apps and names the component behind each one. A dev overlay, or headless reports for CI.", "license": "MIT", "author": "Duncan Faulkner", diff --git a/src/report/headless.ts b/src/report/headless.ts index f52fa23..adf9e30 100644 --- a/src/report/headless.ts +++ b/src/report/headless.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import type { RunOptions as AxeRunOptions } from 'axe-core'; import type { Browser, Page } from 'playwright'; import { detectTabTrap, type FocusObservation } from '../keyboard/focus-walk.js'; +import { OVERLAY_EXCLUDE_SELECTOR } from '../overlay.js'; import type { A11yFinding } from '../scan.js'; import { findingsNotIn } from './baseline.js'; import type { PageReport, ScanChecks, ScanReport } from './format.js'; @@ -185,6 +186,16 @@ interface PassConfig { /** Settle time after switching a class/attribute theme, so colour transitions finish before axe reads them. */ const THEME_SETTLE_MS = 300; +/** + * Hides the in-app overlay (if the app runs it) for the headless scan. Its + * tab-order path is a fixed SVG that overflows the viewport, and axe's + * color-contrast counts it as covering text below the fold: that text came + * back "needs review" instead of pass/fail, so its contrast was never + * reported. Excluding the overlay from axe's context isn't enough; the exclude + * stops axe checking the overlay, not seeing it. + */ +const HIDE_OVERLAY_CSS = `${OVERLAY_EXCLUDE_SELECTOR} { display: none !important; }`; + /** Scan every route in one colour scheme, in its own browser context. */ async function scanPass( browser: Browser, @@ -224,6 +235,7 @@ async function scanPass( await page.waitForTimeout(THEME_SETTLE_MS); } if (config.beforeScan) await config.beforeScan(page, { colorScheme, route }); + await page.addStyleTag({ content: HIDE_OVERLAY_CSS }); await page.addScriptTag({ content: inPageScript }); const findings = (await page.evaluate((args) => { const w = window as unknown as { diff --git a/src/testing/e2e-report.spec.ts b/src/testing/e2e-report.spec.ts index 4f92816..c3fa4bb 100644 --- a/src/testing/e2e-report.spec.ts +++ b/src/testing/e2e-report.spec.ts @@ -141,6 +141,26 @@ const PAGES: Record = {

Attribute

Theme-dependent text

`, + // An app running the in-app overlay with its tab-order path drawn. The path's + // SVG is fixed over the viewport and overflows it, which made axe's + // color-contrast give up ("needs review") on text below the fold, so it was + // never reported. Report mode hides the overlay before it scans. The layer is + // a shown popover with a pill button, as keepInTopLayer and the toggle make it. + '/overlay-below-fold': ` +
+

Overlay

+
+

Low contrast, below the fold

+
+
+ + + +
+ + `, + // Plain axe + layout: a missing alt, and a positive tabindex that jumps up the page. '/axe': `
@@ -213,6 +233,11 @@ describe.skipIf(!ready)('report-mode in a real browser (E2E)', () => { expect(report.pages.map((p) => p.label)).toEqual(Object.keys(PAGES)); }); + it('checks contrast below the fold even when the app runs the in-app overlay', () => { + const below = page('/overlay-below-fold').findings.filter((f) => f.html.includes('below-fold')); + expect(below.map((f) => f.id)).toEqual(['color-contrast']); + }); + it('reports axe violations and layout-dependent keyboard findings', () => { expect(ids('/axe')).toContain('image-alt'); expect(ids('/axe')).toContain('ngbr/tab-order-mismatch');