Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5a375d3
feat: add interactive playground page for auto-scan demo
timdamen Feb 28, 2026
4ff5f53
feat: Define shared types for test-utils
timdamen Feb 28, 2026
973a8c0
feat: Implement failure message formatting
timdamen Feb 28, 2026
ddab81f
feat: Implement core runA11yScan utility
timdamen Feb 28, 2026
cc59fca
feat: Configure unbuild for test-utils output
timdamen Feb 28, 2026
5852422
feat: Update package.json exports and peer dependencies
timdamen Feb 28, 2026
77b9d9d
feat: Add unit tests for failure message formatting
timdamen Feb 28, 2026
f09bf55
feat: Add unit tests for runA11yScan and ScanResult
timdamen Feb 28, 2026
17adfb7
feat: Implement multi-route auto-scan utility with threshold support
timdamen Feb 28, 2026
4efdec0
feat: Add unit tests for auto-scan utility
timdamen Feb 28, 2026
ac5c028
feat: Implement Playwright browser helpers with auto-wait
timdamen Feb 28, 2026
98443f8
feat: Implement Vitest custom matcher toHaveNoA11yViolations
timdamen Feb 28, 2026
06b2493
feat: Add unit tests for Vitest custom matcher
timdamen Feb 28, 2026
6c94112
feat: Set up auto-scan integration test across all playground routes
timdamen Feb 28, 2026
28912ec
feat: Set up Playwright integration test for playground with test-utils
timdamen Feb 28, 2026
1c324bf
feat: Set up Vitest integration test for playground with test-utils
timdamen Feb 28, 2026
7ad78cd
feat: Add JSDoc documentation for public API
timdamen Feb 28, 2026
0957da4
fix: e2e test failure
timdamen Feb 28, 2026
4d29e65
fix: add extra utlity need for intergration
timdamen Feb 28, 2026
4584827
chore: QR comments
timdamen Feb 28, 2026
748b85a
chore: linting
timdamen Feb 28, 2026
117cb75
test: fix test case
timdamen Feb 28, 2026
627c26a
test: improve tests
timdamen Feb 28, 2026
f046bbb
chore: fix ups
timdamen Feb 28, 2026
89ad000
merge: resolve conflicts with main in interactive.vue
timdamen Feb 28, 2026
6ff37f4
feat: change name playwright to browser and add Observer mode
timdamen Mar 1, 2026
5505a5a
fix: cap axe running time
timdamen Mar 1, 2026
b16cc65
fix: throws auto-scan if already exist
timdamen Mar 1, 2026
1b7bba3
fix: accumulation axe results
timdamen Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions build.config.test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
'src/test-utils/index',
'src/test-utils/setup',
'src/test-utils/browser',
],
outDir: 'dist',
clean: false,
declaration: 'node16',
externals: [
'vitest',
'playwright-core',
'axe-core',
'linkedom',
],
})
13 changes: 13 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
hooks: {
'build:done'(ctx) {
for (const warning of ctx.warnings) {
if (warning.includes('test-utils')) {
ctx.warnings.delete(warning)
}
}
},
},
})
37 changes: 36 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,43 @@
".": {
"types": "./dist/types.d.mts",
"import": "./dist/module.mjs"
},
"./test-utils": {
"types": "./dist/test-utils/index.d.mts",
"import": "./dist/test-utils/index.mjs"
},
"./test-utils/setup": {
"types": "./dist/test-utils/setup.d.mts",
"import": "./dist/test-utils/setup.mjs"
},
"./test-utils/browser": {
"types": "./dist/test-utils/browser.d.mts",
"import": "./dist/test-utils/browser.mjs"
}
},
"main": "./dist/module.mjs",
"typesVersions": {
"*": {
".": [
"./dist/types.d.mts"
],
"test-utils": [
"./dist/test-utils/index.d.mts"
],
"test-utils/setup": [
"./dist/test-utils/setup.d.mts"
],
"test-utils/browser": [
"./dist/test-utils/browser.d.mts"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build && npm run client:build",
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build && npm run build:test-utils && npm run client:build",
"build:test-utils": "unbuild --config build.config.test-utils",
"client:build": "nuxt generate client",
"client:dev": "nuxt dev client --port 3030",
"dev": "npm run play:dev",
Expand All @@ -45,6 +67,18 @@
"linkedom": "^0.18.12",
"sirv": "^3.0.2"
},
"peerDependencies": {
"vitest": ">=1.0.0",
"playwright-core": ">=1.0.0"
},
"peerDependenciesMeta": {
"vitest": {
"optional": true
},
"playwright-core": {
"optional": true
}
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.18",
"@nuxt/devtools": "latest",
Expand All @@ -62,6 +96,7 @@
"pkg-pr-new": "0.0.63",
"playwright-core": "1.58.2",
"typescript": "~5.9.3",
"unbuild": "^3.6.1",
"vite": "7.3.1",
"vitest": "4.0.18",
"vue-tsc": "3.2.5"
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions src/test-utils/auto-scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { AutoScanOptions, ScanResult } from './types'
import { runA11yScan } from './index'

/**
* Create a multi-route auto-scan utility that accumulates accessibility
* scan results across multiple pages and supports threshold-based failure.
*
* @param options - Configuration for route exclusion and violation threshold
* @returns An object with methods to scan HTML, retrieve results, and check thresholds
*
* @example
* ```ts
* import { $fetch } from '@nuxt/test-utils'
* import { createAutoScan } from '@nuxt/a11y/test-utils'
*
* const autoScan = createAutoScan({ threshold: 10, exclude: ['/admin'] })
*
* for (const route of ['/', '/about', '/contact']) {
* const html = await $fetch<string>(route, { responseType: 'text' })
* await autoScan.scanFetchedHtml(route, html)
* }
*
* const results = autoScan.getResults()
* if (autoScan.exceedsThreshold()) {
* console.error('Too many accessibility violations!')
* }
* ```
*/
export function createAutoScan(options: AutoScanOptions = {}) {
const { exclude = [], threshold = 0 } = options
const results = new Map<string, ScanResult>()

function normalizeUrl(url: string): string {
try {
const parsed = new URL(url, 'http://nuxt-a11y.local')
return `${parsed.pathname}${parsed.search}`
}
catch {
return url
}
}

function isExcluded(url: string): boolean {
const normalizedUrl = normalizeUrl(url)
return exclude.some(pattern =>
typeof pattern === 'string' ? normalizedUrl.includes(pattern) : pattern.test(normalizedUrl),
)
}

function mergeResult(url: string, result: ScanResult): void {
const key = normalizeUrl(url)
const existing = results.get(key)
if (!existing) {
results.set(key, result)
return
}

results.set(key, {
...result,
violations: [...existing.violations, ...result.violations],
violationCount: existing.violationCount + result.violationCount,
getByImpact: level => [...existing.violations, ...result.violations].filter(v => v.impact === level),
getByRule: ruleId => [...existing.violations, ...result.violations].filter(v => v.id === ruleId),
getByTag: tag => [...existing.violations, ...result.violations].filter(v => v.tags.includes(tag)),
})
}

return {
/**
* Scan fetched HTML for a given URL and accumulate results.
* Excluded URLs are silently skipped.
*
* @param url - The route URL being scanned
* @param html - The HTML content to scan
*/
async scanFetchedHtml(url: string, html: string): Promise<void> {
if (isExcluded(url))
return

const normalizedUrl = normalizeUrl(url)
const result = await runA11yScan(html, { route: normalizedUrl })
mergeResult(normalizedUrl, result)
},

addResult(url: string, result: ScanResult): void {
if (isExcluded(url))
return
mergeResult(url, result)
},

/**
* Get all accumulated scan results keyed by URL.
*
* @returns A record mapping URLs to their scan results
*/
getResults(): Record<string, ScanResult> {
return Object.fromEntries(results)
},

/**
* Check if the total violation count exceeds the configured threshold.
*
* @returns `true` if total violations exceed the threshold, `false` otherwise
*/
exceedsThreshold(): boolean {
let total = 0
for (const result of results.values()) {
total += result.violationCount
}
return total > threshold
},
}
}
Loading
Loading