Conversation
commit: |
📝 WalkthroughWalkthroughAdds accessibility (a11y) scanning and reporting across the test tooling: integrates optional Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 (noaria-label, inner text, ortitle). 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.tsandapp.spec.tsusedescribeblocks, while this file andMyComponent.spec.tsdo not. Wrapping related tests in adescribeblock improves test output organization.Note: These tests depend on an experimental alpha version of
@nuxt/a11ywith thetest-utilssubpath (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.debugto 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (19)
build.config.tsexamples/app-bun/app.vueexamples/app-bun/test/browser.e2e.spec.tsexamples/app-cucumber/app.vueexamples/app-cucumber/test/acceptance/features/support/hooks.tsexamples/app-jest/app.vueexamples/app-jest/test/browser.e2e.spec.tsexamples/app-playwright/app.vueexamples/app-playwright/playwright.config.tsexamples/app-vitest/test/e2e/browser.spec.tsexamples/app-vitest/test/nuxt/a11y.spec.tspackage.jsonsrc/config.tssrc/e2e/browser.tssrc/e2e/context.tssrc/e2e/server.tssrc/e2e/setup/index.tssrc/e2e/types.tssrc/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", |
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
🧩 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 3Repository: 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
@nuxt/a11ymodule docs on nuxt.com (configuration/options) — https://nuxt.com/modules/a11y [1]nuxt/a11yGitHub repository README (norunAxeOnPagementioned) — 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)afterpage.goto(...)- then
await checkA11y(page, context?, options?)(orgetViolations/getAxeResults) where:contextcan be a selector ornullto scan the whole documentoptionsis where you passaxeOptions(forwarded toaxe.run) plus someaxe-playwrightextras likedetailedReport, 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 (
gotoorcreatePage) 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 -30Repository: 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/nullRepository: 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.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (4)
examples/app-a11y/package.json (1)
22-22: Pin@nuxt/test-utilsto 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 infinallyblocks for failure-safe cleanup.If an assertion fails,
page.close()is skipped. Wrapping each test body intry/finallyavoids 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (26)
build.config.tsexamples/app-a11y/.gitignoreexamples/app-a11y/app.vueexamples/app-a11y/cucumber.jsonexamples/app-a11y/nuxt.config.tsexamples/app-a11y/package.jsonexamples/app-a11y/pages/index.vueexamples/app-a11y/pages/interactive.vueexamples/app-a11y/pages/violations.vueexamples/app-a11y/playwright.config.tsexamples/app-a11y/test/acceptance/features/a11y/features/accessibility.featureexamples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.tsexamples/app-a11y/test/acceptance/features/support/hooks.tsexamples/app-a11y/test/e2e/browser.spec.tsexamples/app-a11y/test/nuxt/scan.spec.tsexamples/app-a11y/test/nuxt/setup.tsexamples/app-a11y/tests/a11y.test.tsexamples/app-a11y/tests/auto-scan.test.tsexamples/app-a11y/tsconfig.jsonexamples/app-a11y/vitest.config.tspackage.jsonsrc/e2e/browser.tssrc/e2e/setup/index.tssrc/e2e/types.tssrc/playwright-reporter.tssrc/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
examples/app-a11y/package.json
Outdated
| "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" |
There was a problem hiding this comment.
🧩 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.mjsUsing --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.mjsCitations:
- 1: Remove --experimental-loader ExperimentalWarning as the option doesn't exist anymore / is no longer experimental nodejs/node#51196
- 2: https://nodejs.org/download/release/v20.6.0/docs/api/module.html?utm_source=openai
- 3: https://docs.redhat.com/en/documentation/red_hat_build_of_node.js/20/html/release_notes_for_node.js_20/features-nodejs?utm_source=openai
- 4: Remove --experimental-loader ExperimentalWarning as the option doesn't exist anymore / is no longer experimental nodejs/node#51196
🌐 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):
- Install:
npm i -D ts-node typescript- 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]
- 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]
- 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.tsThis 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:
-
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' -
An inline
data:URL with--import(verbose and impractical for package.json) -
Use the
ts-nodeCLI 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.
| <button /> | ||
| <img> |
There was a problem hiding this comment.
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.
| <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>.
| 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') | ||
| }) |
There was a problem hiding this comment.
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.
| 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.
examples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.ts
Show resolved
Hide resolved
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
examples/app-a11y/tests/a11y.test.ts (1)
47-55:⚠️ Potential issue | 🟠 MajorCapture 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
📒 Files selected for processing (6)
examples/app-a11y/package.jsonexamples/app-a11y/register-ts-node.mjsexamples/app-a11y/test/acceptance/features/a11y/step-definitions/a11y.given-steps.tsexamples/app-a11y/test/e2e/browser.spec.tsexamples/app-a11y/tests/a11y.test.tssrc/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
| 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 |
There was a problem hiding this comment.
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.
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