Skip to content

wip: nuxt a11y module integration#1603

Open
timdamen wants to merge 6 commits intonuxt:mainfrom
timdamen:feat/nuxt-a11y-module-integration
Open

wip: nuxt a11y module integration#1603
timdamen wants to merge 6 commits intonuxt:mainfrom
timdamen:feat/nuxt-a11y-module-integration

Conversation

@timdamen
Copy link

@timdamen timdamen commented Feb 28, 2026

Work in progress

🔗 Linked issue

📚 Description

(nuxt/a11y#256) Step 1 — @nuxt/a11y/test-utils exports runA11yScan(), toHaveNoA11yViolations(), Playwright helpers (injectAxe, runAxeOnPage), and createAutoScan(). Users can already compose these manually.

This PR - Step 2 makes @nuxt/test-utils an a11y engine: setup({ a11y: true }) and every $fetch and createPage/goto is automatically scanned. Violations aggregate silently, and afterAll reports the summary.

Screen.Recording.2026-02-28.at.22.15.37.mov

@timdamen timdamen requested a review from danielroe as a code owner February 28, 2026 21:10
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/test-utils@1603

commit: 3e48c56

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds accessibility (a11y) scanning and reporting across the test tooling: integrates optional @nuxt/a11y test-utils into Vitest and Playwright flows (browser, server, and Playwright helpers), introduces new TestOptions/TestContext a11y fields, and adds runtime hooks to run/observe Axe scans and collect results. Implements a Playwright reporter that aggregates per-run a11y JSON results and emits pass/fail based on thresholds. Updates build and package exports to include a Playwright reporter entry. Adds an examples/app-a11y example app, tests (Vitest, Playwright, Cucumber), and supporting configs and scripts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'wip: nuxt a11y module integration' is directly related to the changeset, which implements accessibility module integration into @nuxt/test-utils by adding a11y scanning capabilities.
Description check ✅ Passed The description clearly explains the PR as Step 2 of a11y integration, detailing how setup({ a11y: true }) enables automatic scanning of $fetch and createPage/goto calls, with violations aggregated and reported in afterAll.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
examples/app-bun/app.vue (1)

5-5: Intentional accessibility violation for demonstration purposes.

The empty <button /> lacks accessible text (no aria-label, inner text, or title). This appears intentional to demonstrate a11y scanning detecting violations. Consider adding a comment clarifying this is a deliberate test fixture, so future maintainers don't "fix" it.

+    <!-- Intentionally empty button to test a11y violation detection -->
     <button />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-bun/app.vue` at line 5, The empty self-closing <button /> in
examples/app-bun/app.vue is an intentional accessibility violation used as a
test fixture; add an inline comment immediately adjacent to the <button />
element (e.g., <!-- intentionally empty button for a11y scanner test -->) that
clearly states it is deliberate and should not be removed or changed, so future
maintainers and automated linters understand this is a test case rather than an
accidental omission.
src/e2e/types.ts (1)

73-73: Consider adding JSDoc documentation for the new a11y option.

This option enables important functionality but lacks documentation. Adding a JSDoc comment would help users understand the available options.

+  /**
+   * Enable automatic accessibility scanning during tests.
+   * When `true`, fetched HTML and page navigations are scanned for a11y violations.
+   * Pass an object to configure exclusions and violation thresholds.
+   * `@default` false
+   */
   a11y?: boolean | { exclude?: (string | RegExp)[], threshold?: number }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/types.ts` at line 73, Add a JSDoc block above the a11y type
declaration describing the option, its allowed shapes (boolean or object), the
object properties (exclude?: array of string|RegExp and threshold?: number),
expected types and any default behavior; place the comment immediately above the
line containing "a11y?: boolean | { exclude?: (string | RegExp)[], threshold?:
number }" so editors and generated docs pick it up, and include short examples
or notes on how exclude patterns are matched if relevant.
src/e2e/browser.ts (1)

57-67: Consider logging a11y scan failures for debugging.

While the silent error handling is intentional (to prevent scan failures from breaking navigation), optionally logging errors at a debug level would help users diagnose issues when a11y scanning fails unexpectedly.

     if (a11y) {
       try {
         const { runAxeOnPage } = await import('@nuxt/a11y/test-utils/playwright')
         const result = await runAxeOnPage(page, { waitForState: null })
         a11y.addResult(url, result)
       }
-      catch {
+      catch (error) {
         // a11y scan failure must not break navigation
+        if (process.env.DEBUG) {
+          console.warn('[a11y] scan failed:', error)
+        }
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/browser.ts` around lines 57 - 67, The catch block around the a11y
scan swallows errors entirely; update the catch in the block that calls
tryUseTestContext()?.a11y and runAxeOnPage(page, ...) so it logs the caught
error at debug/verbose level (e.g., include the URL and the error) before
swallowing it; keep the current behavior of not throwing but ensure failures are
recorded (use console.debug or the test context/logger if available) so
a11y.addResult(url, result) failures are visible for debugging.
examples/app-vitest/test/nuxt/a11y.spec.ts (1)

11-23: Consider wrapping in a describe block for consistency.

The test organization is functional but inconsistent with other tests in this project—registerEndpoint.spec.ts and app.spec.ts use describe blocks, while this file and MyComponent.spec.ts do not. Wrapping related tests in a describe block improves test output organization.

Note: These tests depend on an experimental alpha version of @nuxt/a11y with the test-utils subpath (currently unreleased in the public npm package).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-vitest/test/nuxt/a11y.spec.ts` around lines 11 - 23, Wrap the
two a11y tests in a describe block to match project conventions: create a
describe block (e.g., describe('a11y.spec' or describe('MyComponent
accessibility')) and move the it(...) tests inside it so both
mountSuspended/MyComponent test and the missing button label test are children
of the same describe; update any async setup/teardown inside the block if needed
so functions run correctly (refer to the existing it() tests and functions
runA11yScan, wrapInDocument, mountSuspended, and MyComponent to locate and
relocate the tests).
src/playwright.ts (1)

65-75: Consider logging a11y scan failures in debug mode.

The empty catch block silently swallows all errors, which could make debugging difficult if there's a configuration or setup issue with the a11y scanner. Consider emitting a debug-level log or using console.debug to aid troubleshooting while still preventing navigation failures.

💡 Optional: Add debug logging
       if (a11y) {
         try {
           const { runAxeOnPage } = await import('@nuxt/a11y/test-utils/playwright')
           const result = await runAxeOnPage(page, { waitForState: null })
           a11y.addResult(url, result)
         }
-        catch {
+        catch (error) {
           // a11y scan failure must not break navigation
+          if (process.env.DEBUG) {
+            console.debug('[a11y] scan failed:', error)
+          }
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/playwright.ts` around lines 65 - 75, The empty catch in the a11y scan
swallows errors; update the catch in the block that references
_nuxtHooks.ctx.a11y and runAxeOnPage to log the error at debug level (e.g.,
processLogger.debug or console.debug) while keeping navigation unaffected;
ensure the log includes context (url and the caught error) so failures from
runAxeOnPage(page, ...) are visible in debug mode but still do not rethrow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/app-playwright/app.vue`:
- Around line 5-6: The empty <button /> in app.vue has no accessible name;
either give it one (add visible text or an aria-label/aria-labelledby) or, if
it’s intentionally a test fixture for a11y scanning, add an inline comment next
to the <button /> indicating that it’s intentionally unlabeled for testing
accessibility scanning; locate the <button /> element in app.vue and apply one
of these changes so the intent is explicit and the a11y violation is resolved or
documented.

In `@package.json`:
- Line 104: The dependency entry for "@nuxt/a11y" currently points to a
pre-release commit URL ("https://pkg.pr.new/nuxt/a11y/@nuxt/a11y@627c26a");
replace this with a proper semver version (for example "^1.0.0" or the intended
released version) in package.json so the PR does not merge with a
commit-specific prerelease URL — update the "@nuxt/a11y" value to the chosen
version string and run npm/yarn install to update lockfile before merging.

In `@src/e2e/server.ts`:
- Around line 100-105: The a11y scan call in the promise.then callback (await
ctx.a11y!.scanFetchedHtml(path, data)) can throw and currently will break the
fetch; wrap that call in a try/catch (mirroring the pattern used in
src/playwright.ts lines ~67-74) so any error is caught and not rethrown,
optionally logging the error via the same logger used elsewhere on ctx, then
continue to return data; i.e., replace the direct await of
ctx.a11y!.scanFetchedHtml with a try { await ctx.a11y!.scanFetchedHtml(path,
data) } catch (err) { /* swallow or ctx.logger.error(...) */ } so failures in
scanFetchedHtml do not break the fetch.

In `@src/playwright.ts`:
- Line 69: The call to runAxeOnPage(page, { waitForState: null }) is invalid
because runAxeOnPage is not exported by `@nuxt/a11y`; replace this approach by
importing and using AxeBuilder from `@axe-core/playwright`: remove the
runAxeOnPage import/usages, add `@axe-core/playwright` (if not already installed),
instantiate new AxeBuilder({ page }) or use new AxeBuilder({ page: page }) to
run the accessibility scan and call its analyze() method, then adapt the const
result = await runAxeOnPage(...) line to await AxeBuilder(...).analyze() and
convert any result handling to the AxeBuilder response shape.

---

Nitpick comments:
In `@examples/app-bun/app.vue`:
- Line 5: The empty self-closing <button /> in examples/app-bun/app.vue is an
intentional accessibility violation used as a test fixture; add an inline
comment immediately adjacent to the <button /> element (e.g., <!-- intentionally
empty button for a11y scanner test -->) that clearly states it is deliberate and
should not be removed or changed, so future maintainers and automated linters
understand this is a test case rather than an accidental omission.

In `@examples/app-vitest/test/nuxt/a11y.spec.ts`:
- Around line 11-23: Wrap the two a11y tests in a describe block to match
project conventions: create a describe block (e.g., describe('a11y.spec' or
describe('MyComponent accessibility')) and move the it(...) tests inside it so
both mountSuspended/MyComponent test and the missing button label test are
children of the same describe; update any async setup/teardown inside the block
if needed so functions run correctly (refer to the existing it() tests and
functions runA11yScan, wrapInDocument, mountSuspended, and MyComponent to locate
and relocate the tests).

In `@src/e2e/browser.ts`:
- Around line 57-67: The catch block around the a11y scan swallows errors
entirely; update the catch in the block that calls tryUseTestContext()?.a11y and
runAxeOnPage(page, ...) so it logs the caught error at debug/verbose level
(e.g., include the URL and the error) before swallowing it; keep the current
behavior of not throwing but ensure failures are recorded (use console.debug or
the test context/logger if available) so a11y.addResult(url, result) failures
are visible for debugging.

In `@src/e2e/types.ts`:
- Line 73: Add a JSDoc block above the a11y type declaration describing the
option, its allowed shapes (boolean or object), the object properties (exclude?:
array of string|RegExp and threshold?: number), expected types and any default
behavior; place the comment immediately above the line containing "a11y?:
boolean | { exclude?: (string | RegExp)[], threshold?: number }" so editors and
generated docs pick it up, and include short examples or notes on how exclude
patterns are matched if relevant.

In `@src/playwright.ts`:
- Around line 65-75: The empty catch in the a11y scan swallows errors; update
the catch in the block that references _nuxtHooks.ctx.a11y and runAxeOnPage to
log the error at debug level (e.g., processLogger.debug or console.debug) while
keeping navigation unaffected; ensure the log includes context (url and the
caught error) so failures from runAxeOnPage(page, ...) are visible in debug mode
but still do not rethrow.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2118ff3 and cd198e2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • build.config.ts
  • examples/app-bun/app.vue
  • examples/app-bun/test/browser.e2e.spec.ts
  • examples/app-cucumber/app.vue
  • examples/app-cucumber/test/acceptance/features/support/hooks.ts
  • examples/app-jest/app.vue
  • examples/app-jest/test/browser.e2e.spec.ts
  • examples/app-playwright/app.vue
  • examples/app-playwright/playwright.config.ts
  • examples/app-vitest/test/e2e/browser.spec.ts
  • examples/app-vitest/test/nuxt/a11y.spec.ts
  • package.json
  • src/config.ts
  • src/e2e/browser.ts
  • src/e2e/context.ts
  • src/e2e/server.ts
  • src/e2e/setup/index.ts
  • src/e2e/types.ts
  • src/playwright.ts

package.json Outdated
"devDependencies": {
"@cucumber/cucumber": "12.6.0",
"@jest/globals": "30.2.0",
"@nuxt/a11y": "https://pkg.pr.new/nuxt/a11y/@nuxt/a11y@627c26a",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pre-release dependency URL should be updated before merging.

The pkg.pr.new URL points to a specific commit (627c26a). This is appropriate for WIP development, but should be replaced with a proper version (e.g., ^1.0.0 or similar) before this PR is merged to main.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 104, The dependency entry for "@nuxt/a11y" currently
points to a pre-release commit URL
("https://pkg.pr.new/nuxt/a11y/@nuxt/a11y@627c26a"); replace this with a proper
semver version (for example "^1.0.0" or the intended released version) in
package.json so the PR does not merge with a commit-specific prerelease URL —
update the "@nuxt/a11y" value to the chosen version string and run npm/yarn
install to update lockfile before merging.

if (a11y) {
try {
const { runAxeOnPage } = await import('@nuxt/a11y/test-utils/playwright')
const result = await runAxeOnPage(page, { waitForState: null })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find where runAxeOnPage is imported/used in the codebase
rg "runAxeOnPage" --type ts --type js -B 3 -A 3

Repository: nuxt/test-utils

Length of output: 933


🌐 Web query:

@nuxt/a11y runAxeOnPage API documentation

💡 Result:

runAxeOnPage is not documented in the public docs/README for @nuxt/a11y (it doesn’t appear in the module documentation on nuxt.com, nor in the repository README), so there isn’t an official “runAxeOnPage API documentation” page to reference. [1] [2]

What is documented publicly for @nuxt/a11y is the module configuration (including passing axe-core options / runOptions, and build-time report options). [1] [2]

Sources

  1. @nuxt/a11y module docs on nuxt.com (configuration/options) — https://nuxt.com/modules/a11y [1]
  2. nuxt/a11y GitHub repository README (no runAxeOnPage mentioned) — https://github.com/nuxt/a11y [2]

🌐 Web query:

site:github.com/nuxt/a11y runAxeOnPage waitForState

💡 Result:

No public (unauthenticated) web-indexed matches show up for either runAxeOnPage or waitForState inside nuxt/a11y—a site:github.com/nuxt/a11y ... search isn’t returning results, and GitHub’s code search results page requires signing in to view matches. [1]

If you need to confirm in the repo, the reliable options are:

  • Use GitHub code search while signed in: repo:nuxt/a11y runAxeOnPage / repo:nuxt/a11y waitForState. [1]
  • Clone and grep locally: git clone ... && rg "runAxeOnPage|waitForState".

Sources:
[1] https://github.com/nuxt/a11y
[2] https://nuxt.com/modules/a11y


🌐 Web query:

axe-playwright runAxeOnPage waitForState null options

💡 Result:

axe-playwright (abhinaba-ghosh) does not expose a runAxeOnPage API, and it also does not have a waitForState option in its checkA11y/getViolations/getAxeResults calls. The API is essentially:

  • await injectAxe(page) after page.goto(...)
  • then await checkA11y(page, context?, options?) (or getViolations/getAxeResults) where:
    • context can be a selector or null to scan the whole document
    • options is where you pass axeOptions (forwarded to axe.run) plus some axe-playwright extras like detailedReport, etc. [1]

If you need “wait for state” behavior, do it with Playwright before running axe, e.g.:

  • await page.waitForLoadState('networkidle') (or 'load', 'domcontentloaded')
  • or await page.locator('...').waitFor({ state: 'visible' })

Then run checkA11y(...). Playwright’s docs similarly show running the scan against the current page state after your waits/interactions. [2]

Sources: [1], [2]


🌐 Web query:

"@nuxt/a11y" "test-utils" playwright examples runAxeOnPage

💡 Result:

I couldn’t find any official runAxeOnPage helper in @nuxt/a11y or @nuxt/test-utils (the Nuxt a11y module is focused on in-browser/DevTools scanning via axe-core, not Playwright helpers). [1]

What you can do instead is compose it yourself using:

  • Nuxt’s Playwright test integration / browser helpers (goto or createPage) from @nuxt/test-utils [2]
  • an axe + Playwright library like axe-playwright (injectAxe, checkA11y, etc.) [3]

Example (@nuxt/test-utils/playwright + axe-playwright)

// tests/a11y.e2e.spec.ts
import { test, expect } from '@nuxt/test-utils/playwright'
import { injectAxe, checkA11y } from 'axe-playwright'

async function runAxeOnPage(page) {
  await injectAxe(page)
  // throws (and prints violations) if issues are found
  await checkA11y(page)
}

test('home page has no a11y violations', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await runAxeOnPage(page)
  expect(true).toBe(true)
})

Example (Vitest/Jest style with createPage)

import { describe, test } from 'vitest'
import { setup, createPage } from '@nuxt/test-utils/e2e'
import { injectAxe, checkA11y } from 'axe-playwright'

describe('a11y', async () => {
  await setup({ browser: true })

  test('no violations on /', async () => {
    const page = await createPage('/')
    await injectAxe(page)
    await checkA11y(page)
  })
})

Sources: Nuxt a11y module page [1], Nuxt testing docs for Playwright/browser helpers [2], axe-playwright usage examples [3].


🏁 Script executed:

#!/bin/bash
# Search for any definition or export of runAxeOnPage in this repo
rg "export.*runAxeOnPage|function.*runAxeOnPage|const.*runAxeOnPage" --type ts --type js

# Also check if there's any type definition file
fd "\.d\.ts$" | xargs rg "runAxeOnPage" 2>/dev/null

# Check package.json dependencies
cat package.json | grep -A 20 '"dependencies"' | head -30

Repository: nuxt/test-utils

Length of output: 1024


🏁 Script executed:

#!/bin/bash
# Check for `@nuxt/a11y` in dependencies, devDependencies, or package.json
cat package.json | jq '.dependencies["@nuxt/a11y"], .devDependencies["@nuxt/a11y"], .peerDependencies["@nuxt/a11y"]'

# Also check the entire dependencies object to see if a11y is listed
cat package.json | jq '.dependencies | keys[] | select(contains("a11y"))'
cat package.json | jq '.devDependencies | keys[] | select(contains("a11y"))'

Repository: nuxt/test-utils

Length of output: 136


🌐 Web query:

npm @nuxt/a11y "test-utils/playwright" runAxeOnPage

💡 Result:

runAxeOnPage is not exported by @nuxt/a11y (the package only exports ".", i.e. import '@nuxt/a11y', with no test-utils/* subpath exports). [1]

If you want to run axe in Playwright for a Nuxt app, use @nuxt/test-utils/playwright for the Nuxt+Playwright harness [2] and @axe-core/playwright (AxeBuilder) to scan the page. [3]

// tests/a11y.spec.ts
import { test, expect } from '@nuxt/test-utils/playwright'
import AxeBuilder from '@axe-core/playwright'

test('home has no a11y violations', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })

  const results = await new AxeBuilder({ page }).analyze()
  expect(results.violations).toEqual([])
})

Sources: [1] [2] [3]


🌐 Web query:

site:npmjs.com @nuxt/a11y package.json exports

💡 Result:

I couldn’t directly verify @nuxt/a11y’s published package.json#exports via npmjs.com because npmjs.org/package/@nuxt/a11y is returning 403 Forbidden from this environment, and GitHub’s web UI wouldn’t serve the raw package.json contents here either (only the chrome loads). ()

To get the exports map reliably, use one of these:

1) From an installed copy (most reliable)

npm i `@nuxt/a11y`
node -p "require('./node_modules/@nuxt/a11y/package.json').exports"
# or:
cat node_modules/@nuxt/a11y/package.json | jq '.exports'

2) From the npm registry API (no HTML scraping)

curl -s 'https://registry.npmjs.org/@nuxt/a11y' | jq '."dist-tags".latest'
curl -s 'https://registry.npmjs.org/@nuxt/a11y' | jq '.versions[."dist-tags".latest].exports'

If you tell me which exact version you care about (the module page currently shows v1.0.0-alpha.1 (nuxt.com)), I can also show the exact curl/jq for that pinned version.

Citations:


🏁 Script executed:

#!/bin/bash
# Query npm registry API to check `@nuxt/a11y` exports
curl -s 'https://registry.npmjs.org/@nuxt/a11y' | jq '.["dist-tags"].latest' 2>/dev/null
echo "---"
curl -s 'https://registry.npmjs.org/@nuxt/a11y' | jq '.versions | to_entries | map(select(.key | test("1\\."))) | reverse | .[0].value | {version: .version, exports: .exports}' 2>/dev/null

Repository: nuxt/test-utils

Length of output: 221


Remove or fix the runAxeOnPage import—it does not exist in @nuxt/a11y.

The @nuxt/a11y package (v1.0.0-alpha.1) only exports the root module; it does not have a test-utils/playwright subpath or a runAxeOnPage function. This import will fail at runtime. Use @axe-core/playwright (AxeBuilder) instead to run accessibility scans in Playwright tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/playwright.ts` at line 69, The call to runAxeOnPage(page, { waitForState:
null }) is invalid because runAxeOnPage is not exported by `@nuxt/a11y`; replace
this approach by importing and using AxeBuilder from `@axe-core/playwright`:
remove the runAxeOnPage import/usages, add `@axe-core/playwright` (if not already
installed), instantiate new AxeBuilder({ page }) or use new AxeBuilder({ page:
page }) to run the accessibility scan and call its analyze() method, then adapt
the const result = await runAxeOnPage(...) line to await
AxeBuilder(...).analyze() and convert any result handling to the AxeBuilder
response shape.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (4)
examples/app-a11y/package.json (1)

22-22: Pin @nuxt/test-utils to a specific version for reproducible builds.

Using "latest" can cause unexpected CI failures when new versions are published. Consider pinning to a specific version or using a version range (e.g., "^3.x.x" or workspace link).

Proposed fix
-    "@nuxt/test-utils": "latest",
+    "@nuxt/test-utils": "workspace:*",

Or pin to a specific version if not using workspaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-a11y/package.json` at line 22, The dependency entry using
"@nuxt/test-utils": "latest" should be changed to a pinned version or a
conservative range to ensure reproducible builds; update the package.json entry
for the "@nuxt/test-utils" dependency (replace "latest" with a specific version
like "3.0.0" or a range such as "^3.0.0"), then run your package manager to
update the lockfile (npm/yarn pnpm install) and commit the updated lockfile so
CI uses the locked version.
examples/app-a11y/tests/auto-scan.test.ts (1)

13-18: Replace fixed sleeps with deterministic waits.

Lines 15 and 17 rely on timing assumptions and can become flaky under load. Prefer waiting on a concrete DOM state after the click.

Proposed fix
 test('dynamic violations detected after interaction', async ({ page, goto }) => {
   await goto('/interactive', { waitUntil: 'hydration' })
-  await page.waitForTimeout(700)
   await page.getByRole('button', { name: 'Add violation' }).click()
-  await page.waitForTimeout(2000)
+  await expect(page.locator('div > button')).toBeVisible()
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-a11y/tests/auto-scan.test.ts` around lines 13 - 18, The test
'dynamic violations detected after interaction' uses fixed sleeps via
page.waitForTimeout which is flaky; replace those sleeps by waiting for a
concrete DOM state after navigation and after the Add violation click (e.g., use
page.waitForSelector or Playwright expect on a locator returned by
page.getByRole or a specific violation element to appear/update). Update the
calls around goto, page.waitForTimeout, and page.getByRole(...).click so you
wait for a specific selector or assertion that indicates the interactive content
has hydrated and that the violation was added, instead of using fixed delays.
examples/app-a11y/test/e2e/browser.spec.ts (1)

15-28: Close pages in finally blocks for failure-safe cleanup.

If an assertion fails, page.close() is skipped. Wrapping each test body in try/finally avoids leaked pages during failing runs.

♻️ Suggested refactor
   it('accessible page has no violations', { timeout: 20000 }, async () => {
     const page = await createPage('/')
-    const result = await runAxeOnPage(page)
-    expect(result.violationCount).toBe(0)
-    await page.close()
+    try {
+      const result = await runAxeOnPage(page)
+      expect(result.violationCount).toBe(0)
+    }
+    finally {
+      await page.close()
+    }
   })

   it('violations page detects issues', { timeout: 20000 }, async () => {
     const page = await createPage('/violations')
-    const result = await runAxeOnPage(page)
-    expect(result.violationCount).toBeGreaterThan(0)
-    expect(result.getByRule('button-name').length).toBeGreaterThan(0)
-    await page.close()
+    try {
+      const result = await runAxeOnPage(page)
+      expect(result.violationCount).toBeGreaterThan(0)
+      expect(result.getByRule('button-name').length).toBeGreaterThan(0)
+    }
+    finally {
+      await page.close()
+    }
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-a11y/test/e2e/browser.spec.ts` around lines 15 - 28, Tests
createPage(...) and runAxeOnPage(...) can leak browser pages when an assertion
fails because page.close() is only called at the end; wrap each test body in a
try/finally so page.close() always runs. Specifically, for both tests that call
createPage('/') and createPage('/violations'), assign the page to a variable
before assertions, execute assertions inside try, and call await page.close() in
the finally (guarding that page is defined) to ensure cleanup even on failure.
src/playwright.ts (1)

80-84: Keep and dispose the observer stop handler.

observePage(...) is awaited but its disposer is discarded. Keeping that handle and stopping it on page close makes observer lifecycle explicit and prevents lingering callbacks.

🧹 Suggested refactor
   goto: async ({ page, _nuxtHooks }, use) => {
     let observerStarted = false
+    let stopObserver: undefined | (() => Promise<void>)
     await use(async (url, options) => {
@@
         if (!observerStarted) {
           observerStarted = true
           try {
             const { observePage } = await import('@nuxt/a11y/test-utils/browser')
-            await observePage(page, (_url, result) => {
+            stopObserver = await observePage(page, (_url, result) => {
               a11y.addResult(_url, result)
             })
+            page.on('close', () => { stopObserver?.().catch(() => {}) })
           }
           catch {
             // observer setup failure must not break tests
           }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/playwright.ts` around lines 80 - 84, observePage(...) returns a disposer
that is currently ignored; capture that return value (e.g., const stopObserver =
await observePage(page, ...)) and ensure you call it when the page is closed or
during test teardown (for example register page.on('close', () =>
stopObserver()) or invoke stopObserver() in the same cleanup path that closes
the page) so the observer added by observePage is explicitly disposed and
a11y.addResult callbacks stop running after page teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/app-a11y/package.json`:
- Line 15: The package.json script "test:cucumber" uses the deprecated
NODE_OPTIONS '--loader ts-node/esm'; replace this with a supported approach:
create a small ESM preload (e.g. register-ts-node.mjs) that programmatically
registers 'ts-node/esm' and then update the script to use NODE_OPTIONS='--import
./register-ts-node.mjs' (or alternatively switch to invoking the ts-node CLI
directly if cucumber-js supports it); ensure references to "ts-node/esm" and the
"test:cucumber" npm script are updated so the test runner is started with the
new --import preload instead of --loader.

In `@examples/app-a11y/pages/violations.vue`:
- Around line 3-4: The img element in the template is using non‑self-closing
syntax which can cause parsing issues in Vue; update the <img> tag to use
self-closing syntax (i.e., replace the standalone img tag with a self-closing
img so it matches the existing self-closed button), ensuring the template uses
<img /> instead of <img>.

In
`@examples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.ts`:
- Around line 9-19: The Given step handlers call createPage('/') or
createPage('/violations') and then runAxeOnPage(page) but if runAxeOnPage throws
the subsequent page.close() is skipped and the browser page is leaked; update
both Given handlers to wrap the scan in a try/finally where runAxeOnPage(page)
(and assignment to lastScanResult) happens in the try block and page.close() is
invoked in the finally block (rethrow or let the original error propagate after
finally) so pages are always closed even on scan failures.
- Around line 7-27: The tests use a module-global lastScanResult that can leak
between scenarios or be undefined; change to scenario-scoped state and guard the
assertions: store the scan result on the Cucumber world (e.g.
this.lastScanResult) inside the Given steps where createPage and runAxeOnPage
are called, and in the Then steps assert that this.lastScanResult exists before
checking .violationCount (fail with a clear message if missing). Update
references to lastScanResult in the Then steps to use this.lastScanResult and
keep runAxeOnPage, createPage, and the Given step functions as the assignment
points so each scenario has its own scan state.

In `@examples/app-a11y/test/e2e/browser.spec.ts`:
- Line 2: Remove the unused import symbol `url` from the import statement that
currently reads "import { createPage, setup, url } from '@nuxt/test-utils/e2e'";
update the import to only include the used symbols (e.g., `createPage` and
`setup`) so lint no-unused-vars/no-unused-imports errors are resolved and CI
passes.

In `@examples/app-a11y/tests/a11y.test.ts`:
- Around line 41-53: The test's final assertion assumes no callbacks occurred
before stop; instead record the number of callbacks observed before calling stop
and assert that it does not increase after stop: in the test 'observePage stop
prevents further callbacks' call observePage, wait the pre-stop interval, then
capture const lenBeforeStop = results.length (or snapshot the last seen index),
call stop(), trigger the DOM change (button click and wait), and assert
results.length === lenBeforeStop so we verify no new callbacks happened
post-stop; refer to observePage, stop, results and ScanResult in the test.
- Around line 28-37: The test currently only asserts results.length > 0 which
can be true before the click; change the assertion to verify the click actually
produces new scan output by recording the results length before clicking (use
the existing results array), performing the click on the "Add violation" button,
then poll/assert that results.length is greater than the recorded pre-click
length (or assert that a new ScanResult with the expected violation appears);
update the test named "observePage detects dynamically added violations" and the
observePage usage accordingly.

In `@src/e2e/setup/index.ts`:
- Around line 55-74: The catch currently swallows write errors, hiding
violations — change the catch in the reporterActive block (surrounding
readFileSync(signalPath), mkdirSync(runDir,...), writeFileSync(...)) to log the
original error (use your logger or console.error) and attempt a local fallback
write to a safe location (e.g., os.tmpdir()) using the same fileName via
writeFileSync(join(tmpdir(), fileName), JSON.stringify(data), 'utf-8'); if the
fallback write also fails, rethrow the error so the upstream threshold
enforcement isn't bypassed.

In `@src/playwright-reporter.ts`:
- Around line 63-75: The aggregation currently keeps the first-seen threshold
for a project (variables: byProject, key, existing, data.threshold), making
results order-dependent; update the merge logic so the project's threshold is
deterministically combined (e.g., set threshold to Math.min(existing.threshold,
data.threshold) for a conservative/safe behavior) when existing is present, and
continue to accumulate totalViolations, routeCount, and allViolations as before;
preserve the same initial assignment (threshold: data.threshold) in the else
branch.

---

Nitpick comments:
In `@examples/app-a11y/package.json`:
- Line 22: The dependency entry using "@nuxt/test-utils": "latest" should be
changed to a pinned version or a conservative range to ensure reproducible
builds; update the package.json entry for the "@nuxt/test-utils" dependency
(replace "latest" with a specific version like "3.0.0" or a range such as
"^3.0.0"), then run your package manager to update the lockfile (npm/yarn pnpm
install) and commit the updated lockfile so CI uses the locked version.

In `@examples/app-a11y/test/e2e/browser.spec.ts`:
- Around line 15-28: Tests createPage(...) and runAxeOnPage(...) can leak
browser pages when an assertion fails because page.close() is only called at the
end; wrap each test body in a try/finally so page.close() always runs.
Specifically, for both tests that call createPage('/') and
createPage('/violations'), assign the page to a variable before assertions,
execute assertions inside try, and call await page.close() in the finally
(guarding that page is defined) to ensure cleanup even on failure.

In `@examples/app-a11y/tests/auto-scan.test.ts`:
- Around line 13-18: The test 'dynamic violations detected after interaction'
uses fixed sleeps via page.waitForTimeout which is flaky; replace those sleeps
by waiting for a concrete DOM state after navigation and after the Add violation
click (e.g., use page.waitForSelector or Playwright expect on a locator returned
by page.getByRole or a specific violation element to appear/update). Update the
calls around goto, page.waitForTimeout, and page.getByRole(...).click so you
wait for a specific selector or assertion that indicates the interactive content
has hydrated and that the violation was added, instead of using fixed delays.

In `@src/playwright.ts`:
- Around line 80-84: observePage(...) returns a disposer that is currently
ignored; capture that return value (e.g., const stopObserver = await
observePage(page, ...)) and ensure you call it when the page is closed or during
test teardown (for example register page.on('close', () => stopObserver()) or
invoke stopObserver() in the same cleanup path that closes the page) so the
observer added by observePage is explicitly disposed and a11y.addResult
callbacks stop running after page teardown.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd198e2 and a0fa941.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • build.config.ts
  • examples/app-a11y/.gitignore
  • examples/app-a11y/app.vue
  • examples/app-a11y/cucumber.json
  • examples/app-a11y/nuxt.config.ts
  • examples/app-a11y/package.json
  • examples/app-a11y/pages/index.vue
  • examples/app-a11y/pages/interactive.vue
  • examples/app-a11y/pages/violations.vue
  • examples/app-a11y/playwright.config.ts
  • examples/app-a11y/test/acceptance/features/a11y/features/accessibility.feature
  • examples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.ts
  • examples/app-a11y/test/acceptance/features/support/hooks.ts
  • examples/app-a11y/test/e2e/browser.spec.ts
  • examples/app-a11y/test/nuxt/scan.spec.ts
  • examples/app-a11y/test/nuxt/setup.ts
  • examples/app-a11y/tests/a11y.test.ts
  • examples/app-a11y/tests/auto-scan.test.ts
  • examples/app-a11y/tsconfig.json
  • examples/app-a11y/vitest.config.ts
  • package.json
  • src/e2e/browser.ts
  • src/e2e/setup/index.ts
  • src/e2e/types.ts
  • src/playwright-reporter.ts
  • src/playwright.ts
✅ Files skipped from review due to trivial changes (2)
  • examples/app-a11y/.gitignore
  • examples/app-a11y/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/e2e/types.ts
  • package.json
  • src/e2e/browser.ts

"test:vitest": "vitest run",
"test:playwright": "playwright test --project=a11y",
"test:auto-scan": "playwright test --project=auto-scan",
"test:cucumber": "NODE_OPTIONS='--loader ts-node/esm' NODE_ENV=test cucumber-js"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Node.js 20 --loader flag deprecation --import replacement

💡 Result:

In Node.js 20, using node --loader … (or --experimental-loader …) triggers an ExperimentalWarning that says the long-term replacement is to programmatically register the loader with register() and ensure it runs before your app via --import. (github.com)

Recommended replacement

1) Create a small “preload” module that registers your loader

// register-loader.mjs
import { register } from "node:module";

register("./loader.mjs", import.meta.url);

module.register() exists in Node 20 (added in v20.6.0) and registers module customization hooks (loaders) programmatically. (nodejs.org)

2) Run Node with --import so registration happens before your app

node --import ./register-loader.mjs ./app.mjs

Using --import is specifically recommended so the hooks are registered before application code runs. (docs.redhat.com)

One-liner (no extra file)

Node’s warning message also suggests an inline --import using a data: URL that calls register() (useful for quick tests/CI):

node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("./loader.js", pathToFileURL("./"));' ./app.mjs

(github.com)

Citations:


🌐 Web query:

ts-node ESM --import Node.js 20 loader

💡 Result:

To run TypeScript ESM with ts-node on Node.js 20 using the newer --import + node:module register() approach (instead of --loader):

  1. Install:
npm i -D ts-node typescript
  1. Ensure ESM project settings (minimum):
  • package.json:
{ "type": "module" }
  • tsconfig.json (minimum):
{ "compilerOptions": { "module": "ESNext" } }

(ts-node’s ESM loader expects Node’s ESM mode + ESNext modules.) [3]

  1. Create a preload/register file (ESM), e.g. register-ts-node.mjs:
import { register } from 'node:module';

register('ts-node/esm', import.meta.url);

(Node 20.6+ provides register() for programmatic loader registration.) [1]

  1. Run Node, preloading that file:
node --import ./register-ts-node.mjs ./src/index.ts

(--import preloads an ES module at startup; Node discourages --experimental-loader/--loader in favor of --import + register().) [2]

If you don’t need the Node-only invocation, ts-node also supports:

ts-node --esm ./src/index.ts
# or
ts-node-esm ./src/index.ts

[3]

Sources: Node module.register() docs [1], Node CLI guidance for loaders / --import [2], ts-node ESM loader usage (ts-node/esm) [3].


🌐 Web query:

ts-node ESM Node 20 NODE_OPTIONS --import inline solution

💡 Result:

For Node 20.8.0+ you can preload the ts-node ESM loader via --import (so you don’t need --loader), using Node’s register() from node:module:

NODE_OPTIONS="--import=data:text/javascript,import{register}from'node:module';import{pathToFileURL}from'node:url';register('ts-node/esm',pathToFileURL('./'))" \
node ./src/index.ts

This is the “inline” (data: URL) approach. [1]

If you don’t need --import, the supported/standard ts-node ESM way remains:

NODE_OPTIONS="--loader ts-node/esm" node ./src/index.ts

[2]

If your goal is specifically “use --import cleanly on Node ≥20.6”, an alternative loader that supports it directly is:

node --import=tsimp/import ./src/index.ts

[3]

Sources: [1] [2] [3]


The --loader flag is deprecated in Node.js 20+, but the proposed fix needs correction.

The --loader flag triggers an ExperimentalWarning in Node.js 20+. However, the suggested replacement using ts-node/register/transpile-only is incorrect—that module is for CommonJS (used with --require), not ESM. Since the current setup uses ts-node/esm for ESM support, switching to it would break the configuration.

The correct modern approach requires one of:

  1. A separate preload file that programmatically registers the loader:

    // register-ts-node.mjs
    import { register } from 'node:module';
    register('ts-node/esm', import.meta.url);

    Then use: NODE_OPTIONS='--import ./register-ts-node.mjs'

  2. An inline data: URL with --import (verbose and impractical for package.json)

  3. Use the ts-node CLI directly if compatible with cucumber-js

Consider the migration approach based on your project structure and Node.js version requirements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-a11y/package.json` at line 15, The package.json script
"test:cucumber" uses the deprecated NODE_OPTIONS '--loader ts-node/esm'; replace
this with a supported approach: create a small ESM preload (e.g.
register-ts-node.mjs) that programmatically registers 'ts-node/esm' and then
update the script to use NODE_OPTIONS='--import ./register-ts-node.mjs' (or
alternatively switch to invoking the ts-node CLI directly if cucumber-js
supports it); ensure references to "ts-node/esm" and the "test:cucumber" npm
script are updated so the test runner is started with the new --import preload
instead of --loader.

Comment on lines +3 to +4
<button />
<img>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix self-closing tag syntax for img element.

The <img> element is a void element and should use self-closing syntax in Vue templates to avoid potential parsing issues.

Proposed fix
     <button />
-    <img>
+    <img />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button />
<img>
<button />
<img />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-a11y/pages/violations.vue` around lines 3 - 4, The img element
in the template is using non‑self-closing syntax which can cause parsing issues
in Vue; update the <img> tag to use self-closing syntax (i.e., replace the
standalone img tag with a self-closing img so it matches the existing
self-closed button), ensuring the template uses <img /> instead of <img>.

Comment on lines +7 to +27
let lastScanResult: ScanResult

Given(/^the user navigates to the home page$/u, { timeout: 30000 }, async function (): Promise<void> {
const page = await createPage('/')
lastScanResult = await runAxeOnPage(page)
await page.close()
})

Given(/^the user navigates to the violations page$/u, { timeout: 30000 }, async function (): Promise<void> {
const page = await createPage('/violations')
lastScanResult = await runAxeOnPage(page)
await page.close()
})

Then(/^the page has no accessibility violations$/u, function (): void {
assert.strictEqual(lastScanResult.violationCount, 0, `Expected no violations but found ${lastScanResult.violationCount}`)
})

Then(/^accessibility violations are detected$/u, function (): void {
assert.ok(lastScanResult.violationCount > 0, 'Expected violations but found none')
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use scenario-safe scan state and guard assertions.

lastScanResult is module-global and directly dereferenced in Lines 22 and 26. This can cause stale cross-scenario data or a runtime crash if a Given step did not set it.

Proposed fix
-let lastScanResult: ScanResult
+let lastScanResult: ScanResult | undefined

 Then(/^the page has no accessibility violations$/u, function (): void {
+  assert.ok(lastScanResult, 'No a11y scan result is available for this scenario')
   assert.strictEqual(lastScanResult.violationCount, 0, `Expected no violations but found ${lastScanResult.violationCount}`)
 })

 Then(/^accessibility violations are detected$/u, function (): void {
+  assert.ok(lastScanResult, 'No a11y scan result is available for this scenario')
   assert.ok(lastScanResult.violationCount > 0, 'Expected violations but found none')
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let lastScanResult: ScanResult
Given(/^the user navigates to the home page$/u, { timeout: 30000 }, async function (): Promise<void> {
const page = await createPage('/')
lastScanResult = await runAxeOnPage(page)
await page.close()
})
Given(/^the user navigates to the violations page$/u, { timeout: 30000 }, async function (): Promise<void> {
const page = await createPage('/violations')
lastScanResult = await runAxeOnPage(page)
await page.close()
})
Then(/^the page has no accessibility violations$/u, function (): void {
assert.strictEqual(lastScanResult.violationCount, 0, `Expected no violations but found ${lastScanResult.violationCount}`)
})
Then(/^accessibility violations are detected$/u, function (): void {
assert.ok(lastScanResult.violationCount > 0, 'Expected violations but found none')
})
let lastScanResult: ScanResult | undefined
Given(/^the user navigates to the home page$/u, { timeout: 30000 }, async function (): Promise<void> {
const page = await createPage('/')
lastScanResult = await runAxeOnPage(page)
await page.close()
})
Given(/^the user navigates to the violations page$/u, { timeout: 30000 }, async function (): Promise<void> {
const page = await createPage('/violations')
lastScanResult = await runAxeOnPage(page)
await page.close()
})
Then(/^the page has no accessibility violations$/u, function (): void {
assert.ok(lastScanResult, 'No a11y scan result is available for this scenario')
assert.strictEqual(lastScanResult.violationCount, 0, `Expected no violations but found ${lastScanResult.violationCount}`)
})
Then(/^accessibility violations are detected$/u, function (): void {
assert.ok(lastScanResult, 'No a11y scan result is available for this scenario')
assert.ok(lastScanResult.violationCount > 0, 'Expected violations but found none')
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.ts`
around lines 7 - 27, The tests use a module-global lastScanResult that can leak
between scenarios or be undefined; change to scenario-scoped state and guard the
assertions: store the scan result on the Cucumber world (e.g.
this.lastScanResult) inside the Given steps where createPage and runAxeOnPage
are called, and in the Then steps assert that this.lastScanResult exists before
checking .violationCount (fail with a clear message if missing). Update
references to lastScanResult in the Then steps to use this.lastScanResult and
keep runAxeOnPage, createPage, and the Given step functions as the assignment
points so each scenario has its own scan state.

Comment on lines +55 to +74
if (reporterActive) {
try {
const runId = readFileSync(signalPath, 'utf-8').trim()
const runDir = join(A11Y_BASE_DIR, runId)
mkdirSync(runDir, { recursive: true })
const a11yOptions = typeof ctx.options.a11y === 'object' ? ctx.options.a11y : {}
const data = {
projectName: ctx.playwrightProjectName || '',
threshold: a11yOptions.threshold ?? 0,
routeCount: routes.length,
totalViolations,
violations: allViolations,
}
const fileName = `worker-${process.pid}-${Date.now()}.json`
writeFileSync(join(runDir, fileName), JSON.stringify(data), 'utf-8')
}
catch {
// fall through — reporter will see partial data
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reporter write failures are silently ignored without local fallback.

If Lines 57-70 fail, the catch at Lines 71-73 drops the error and the run bypasses local threshold enforcement (Lines 75-87). This can hide real violations.

Proposed fix
-        const reporterActive = existsSync(signalPath)
+        let reporterActive = existsSync(signalPath)
@@
-          catch {
-            // fall through — reporter will see partial data
+          catch (error) {
+            reporterActive = false
+            console.warn('[`@nuxt/a11y`] Failed to persist reporter payload; falling back to local threshold checks.', error)
           }
         }
-        else {
+        if (!reporterActive) {
           console.log(`[`@nuxt/a11y`] Scanned ${routes.length} route(s) \u2014 ${totalViolations} violation(s)`)
           if (ctx.a11y.exceedsThreshold()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/e2e/setup/index.ts` around lines 55 - 74, The catch currently swallows
write errors, hiding violations — change the catch in the reporterActive block
(surrounding readFileSync(signalPath), mkdirSync(runDir,...),
writeFileSync(...)) to log the original error (use your logger or console.error)
and attempt a local fallback write to a safe location (e.g., os.tmpdir()) using
the same fileName via writeFileSync(join(tmpdir(), fileName),
JSON.stringify(data), 'utf-8'); if the fallback write also fails, rethrow the
error so the upstream threshold enforcement isn't bypassed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
examples/app-a11y/tests/a11y.test.ts (1)

47-55: ⚠️ Potential issue | 🟠 Major

Capture the baseline after stop() to avoid racey failures.

At Line 48, the baseline is taken before await stop() (Line 49). In-flight pre-stop callbacks can still arrive and fail the test even when post-stop behavior is correct.

Proposed fix
-  const lengthBeforeStop = results.length
-  await stop()
+  await stop()
+  const lengthAfterStop = results.length
@@
-  expect(results.length).toBe(lengthBeforeStop)
+  expect(results.length).toBe(lengthAfterStop)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/app-a11y/tests/a11y.test.ts` around lines 47 - 55, The test captures
the baseline lengthBeforeStop before calling await stop(), which allows
in-flight callbacks to mutate results and cause flakiness; move the line that
records the baseline (const lengthBeforeStop = results.length) to immediately
after await stop() so the snapshot is taken post-stop, ensuring no further
callbacks affect the assertion that results.length remains unchanged after
clicking "Add violation".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/playwright-reporter.ts`:
- Around line 1-112: Knip flags this module as unused; register its exported
reporter or silence Knip: add the module export (default export NuxtA11yReporter
/ class NuxtA11yReporter) as a recognized entrypoint in your Knip configuration
(or add an explicit ignore rule for this reporter file/module) so Knip no longer
treats the reporter as unused; update the Knip config accordingly and run the
knip check to confirm the PR no longer fails.

---

Duplicate comments:
In `@examples/app-a11y/tests/a11y.test.ts`:
- Around line 47-55: The test captures the baseline lengthBeforeStop before
calling await stop(), which allows in-flight callbacks to mutate results and
cause flakiness; move the line that records the baseline (const lengthBeforeStop
= results.length) to immediately after await stop() so the snapshot is taken
post-stop, ensuring no further callbacks affect the assertion that
results.length remains unchanged after clicking "Add violation".

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0fa941 and 3e48c56.

📒 Files selected for processing (6)
  • examples/app-a11y/package.json
  • examples/app-a11y/register-ts-node.mjs
  • examples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.ts
  • examples/app-a11y/test/e2e/browser.spec.ts
  • examples/app-a11y/tests/a11y.test.ts
  • src/playwright-reporter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/app-a11y/test/e2e/browser.spec.ts
  • examples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.ts

Comment on lines +1 to +112
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import type { FullConfig, FullResult, Reporter } from '@playwright/test/reporter'

export interface A11yResultFile {
projectName: string
threshold: number
routeCount: number
totalViolations: number
violations: unknown[]
}

export const A11Y_BASE_DIR = join(tmpdir(), '.nuxt-test-a11y')

export function getSignalPath(key: string): string {
const hash = createHash('md5').update(key).digest('hex').slice(0, 12)
return join(A11Y_BASE_DIR, `signal-${hash}`)
}

class NuxtA11yReporter implements Reporter {
private runId = ''
private signalPath = ''
private runDir = ''

onBegin(_config: FullConfig): void {
this.runId = `run-${Date.now()}-${process.pid}`
this.runDir = join(A11Y_BASE_DIR, this.runId)
mkdirSync(this.runDir, { recursive: true })

this.signalPath = getSignalPath(process.cwd())
mkdirSync(A11Y_BASE_DIR, { recursive: true })
writeFileSync(this.signalPath, this.runId, 'utf-8')
}

async onEnd(_result: FullResult): Promise<{ status?: FullResult['status'] } | undefined> {
try {
return await this.aggregateAndReport()
}
finally {
this.cleanup()
}
}

private async aggregateAndReport(): Promise<{ status?: FullResult['status'] } | undefined> {
if (!existsSync(this.runDir)) return undefined

const files = readdirSync(this.runDir).filter(f => f.endsWith('.json'))
if (files.length === 0) return undefined

const byProject = new Map<string, {
threshold: number
totalViolations: number
routeCount: number
allViolations: unknown[]
}>()

for (const file of files) {
const data: A11yResultFile = JSON.parse(readFileSync(join(this.runDir, file), 'utf-8'))
const key = data.projectName || ''
const existing = byProject.get(key)
if (existing) {
existing.threshold = Math.min(existing.threshold, data.threshold)
existing.totalViolations += data.totalViolations
existing.routeCount += data.routeCount
existing.allViolations.push(...data.violations)
}
else {
byProject.set(key, {
threshold: data.threshold,
totalViolations: data.totalViolations,
routeCount: data.routeCount,
allViolations: [...data.violations],
})
}
}

let failed = false
for (const [projectName, data] of byProject) {
const label = projectName ? `[@nuxt/a11y:${projectName}]` : '[@nuxt/a11y]'
console.log(`${label} Scanned ${data.routeCount} route(s) — ${data.totalViolations} violation(s)`)

if (data.totalViolations > data.threshold) {
failed = true
let detail = ''
try {
const { formatViolations } = await import('@nuxt/a11y/test-utils')
detail = '\n\n' + formatViolations(data.allViolations as Parameters<typeof formatViolations>[0])
}
catch {
// formatViolations is optional
}
console.error(`${label} Violation count (${data.totalViolations}) exceeds threshold (${data.threshold})${detail}`)
}
}

return failed ? { status: 'failed' } : undefined
}

private cleanup(): void {
try {
if (existsSync(this.signalPath)) rmSync(this.signalPath)
if (existsSync(this.runDir)) rmSync(this.runDir, { recursive: true })
}
catch {
// best-effort cleanup
}
}
}

export default NuxtA11yReporter
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

CI blocker: Knip currently flags this reporter as unused.

Pipeline logs show src/playwright-reporter.ts is detected as unused, which keeps the PR red. Please register this entrypoint in Knip config (or explicitly ignore it if intentional public API).

🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 1-1: Unused file detected by knip: src/playwright-reporter.ts. Consider removing or excluding unused files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/playwright-reporter.ts` around lines 1 - 112, Knip flags this module as
unused; register its exported reporter or silence Knip: add the module export
(default export NuxtA11yReporter / class NuxtA11yReporter) as a recognized
entrypoint in your Knip configuration (or add an explicit ignore rule for this
reporter file/module) so Knip no longer treats the reporter as unused; update
the Knip config accordingly and run the knip check to confirm the PR no longer
fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant