@mandujs/core 0.54.32 → 0.55.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -654
- package/package.json +106 -210
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +15 -15
- package/src/a11y/types.ts +125 -125
- package/src/agent/__tests__/apply.test.ts +224 -0
- package/src/agent/__tests__/context.test.ts +97 -98
- package/src/agent/apply.ts +1031 -0
- package/src/agent/context.ts +552 -552
- package/src/agent/index.ts +4 -3
- package/src/agent/plan.ts +161 -243
- package/src/agent/repair.ts +239 -162
- package/src/agent/sync.ts +197 -198
- package/src/agent/types.ts +242 -94
- package/src/agent/verify.ts +55 -55
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +15 -5
- package/src/auth/__tests__/reset.test.ts +2 -2
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +14 -2
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__snapshots__/build.test.ts.snap +5 -5
- package/src/bundler/__tests__/build-runner.ts +166 -166
- package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -524
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +49 -20
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +24 -24
- package/src/bundler/__tests__/generation.test.ts +447 -0
- package/src/bundler/__tests__/hdr.test.ts +24 -18
- package/src/bundler/__tests__/hmr-client.test.ts +62 -24
- package/src/bundler/__tests__/jsx-runtime-shim.test.ts +179 -140
- package/src/bundler/__tests__/manifest-schema.test.ts +305 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -42
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +898 -915
- package/src/bundler/build.ts +113 -24
- package/src/bundler/client-boundary-transform.ts +977 -977
- package/src/bundler/css.ts +65 -44
- package/src/bundler/dev.ts +105 -73
- package/src/bundler/fast-refresh-preamble.ts +47 -47
- package/src/bundler/generation.ts +602 -0
- package/src/bundler/index.ts +3 -3
- package/src/bundler/manifest-schema.ts +55 -40
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +16 -15
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/prerender.ts +29 -9
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +1 -1
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +58 -56
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/props-serialization.test.ts +37 -37
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/island.ts +79 -79
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/props-serialization.ts +233 -233
- package/src/client/runtime-entry.ts +598 -598
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -50
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image-compat.ts +3 -0
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +1 -1
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +9 -9
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/generate-types.ts +1 -1
- package/src/content/index.ts +2 -2
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +9 -9
- package/src/db/index.ts +2 -2
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +82 -82
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/smoke.test.ts +2 -2
- package/src/desktop/__tests__/webview-fallback.test.ts +12 -32
- package/src/desktop/__tests__/window.test.ts +20 -99
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/webview-fallback.ts +17 -4
- package/src/desktop/window.ts +17 -8
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/diagnose/__tests__/checks.test.ts +136 -136
- package/src/diagnose/checks.ts +185 -185
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +10 -10
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +5 -1
- package/src/filling/auth.ts +308 -308
- package/src/filling/body-parse.test.ts +60 -60
- package/src/filling/cookie-codec.ts +5 -3
- package/src/filling/deps.ts +265 -265
- package/src/filling/head-method.test.ts +154 -154
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +48 -48
- package/src/generator/templates.ts +219 -219
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +39 -24
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/index.ts +11 -11
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/__tests__/id.test.ts +3 -3
- package/src/id/index.ts +105 -105
- package/src/index.ts +9 -48
- package/src/internal/client-boundary.ts +266 -266
- package/src/internal/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -779
- package/src/kitchen/api/bundle-inspector.ts +179 -0
- package/src/kitchen/api/errors-grouping.ts +126 -126
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/kitchen/kitchen-handler.ts +36 -0
- package/src/kitchen/kitchen-ui.ts +456 -0
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/perf/user-marks.ts +1 -1
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +7 -7
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +227 -227
- package/src/router/client-entry.ts +218 -218
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.test.ts +532 -532
- package/src/router/fs-routes.ts +53 -53
- package/src/router/fs-scanner.ts +216 -216
- package/src/router/fs-types.ts +19 -19
- package/src/router/route-source-analyzer.ts +521 -521
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/inline-client-hydration.test.ts +237 -237
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +517 -517
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/__tests__/searchparams-page-props.test.ts +81 -81
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/fast-refresh-types.ts +1 -1
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +30 -11
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/page-render-response.ts +287 -274
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/rate-limit.ts +1 -1
- package/src/runtime/redirect.ts +1 -1
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/router.test.ts +4 -4
- package/src/runtime/router.ts +10 -10
- package/src/runtime/server.ts +127 -211
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +43 -17
- package/src/runtime/static-files.ts +369 -289
- package/src/runtime/streaming-ssr.ts +219 -180
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/__tests__/scheduler.test.ts +12 -11
- package/src/scheduler/index.ts +11 -5
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/spec/schema.ts +35 -35
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/index.ts +9 -3
- package/src/testing/lcov.ts +192 -0
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
|
@@ -1,333 +1,333 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the Phase 18.χ accessibility audit runner.
|
|
3
|
-
*
|
|
4
|
-
* Philosophy: we never actually install axe-core/jsdom in the test
|
|
5
|
-
* environment — that would both contradict the "optional peerDep"
|
|
6
|
-
* contract and slow every `bun test` run by a full second. Instead
|
|
7
|
-
* each test injects `axeLoader` + `domLoader` fakes and asserts on the
|
|
8
|
-
* report shape. A single "no deps installed" case exercises the real
|
|
9
|
-
* resolution path by leaving both loaders undefined.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
13
|
-
import fs from "fs/promises";
|
|
14
|
-
import path from "path";
|
|
15
|
-
import os from "os";
|
|
16
|
-
import { runAudit, formatAuditReport } from "../run-audit";
|
|
17
|
-
import type { AuditImpact } from "../types";
|
|
18
|
-
|
|
19
|
-
async function mkTmp(): Promise<string> {
|
|
20
|
-
return fs.mkdtemp(path.join(os.tmpdir(), "mandu-a11y-"));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function writeHtml(dir: string, name: string, html: string): Promise<string> {
|
|
24
|
-
const full = path.join(dir, name);
|
|
25
|
-
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
26
|
-
await fs.writeFile(full, html, "utf-8");
|
|
27
|
-
return full;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Fake axe-core handle. Returns the violations that the test preloads
|
|
32
|
-
* into `queued` for the next `.run()` call. Enables multi-file
|
|
33
|
-
* scenarios where each file yields a different result.
|
|
34
|
-
*/
|
|
35
|
-
function createFakeAxe(queued: Array<unknown>) {
|
|
36
|
-
let i = 0;
|
|
37
|
-
return {
|
|
38
|
-
default: {
|
|
39
|
-
async run() {
|
|
40
|
-
const next = queued[i] ?? { violations: [] };
|
|
41
|
-
i += 1;
|
|
42
|
-
return next;
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** A DOM provider stub that returns a no-op window. axe never touches
|
|
49
|
-
* it because the fake axe above ignores its `context` argument. */
|
|
50
|
-
const fakeDom = {
|
|
51
|
-
kind: "jsdom" as const,
|
|
52
|
-
async fromHtml() {
|
|
53
|
-
return {
|
|
54
|
-
window: { document: {} },
|
|
55
|
-
dispose() { /* no-op */ },
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
describe("runAudit — dependency resolution", () => {
|
|
61
|
-
it("returns axe-missing when axeLoader rejects (graceful degradation)", async () => {
|
|
62
|
-
const report = await runAudit([], {
|
|
63
|
-
axeLoader: async () => { throw new Error("Cannot find module 'axe-core'"); },
|
|
64
|
-
});
|
|
65
|
-
expect(report.outcome).toBe("axe-missing");
|
|
66
|
-
expect(report.filesScanned).toBe(0);
|
|
67
|
-
expect(report.violations).toEqual([]);
|
|
68
|
-
expect(report.note).toMatch(/axe-core not installed/);
|
|
69
|
-
expect(report.durationMs).toBe(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("returns axe-missing when no DOM provider is available", async () => {
|
|
73
|
-
const report = await runAudit([], {
|
|
74
|
-
axeLoader: async () => createFakeAxe([]),
|
|
75
|
-
domLoader: async () => null,
|
|
76
|
-
});
|
|
77
|
-
expect(report.outcome).toBe("axe-missing");
|
|
78
|
-
expect(report.note).toMatch(/DOM provider/);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("treats loader returning non-axe-shape as missing", async () => {
|
|
82
|
-
const report = await runAudit([], {
|
|
83
|
-
axeLoader: async () => ({ notAxe: true }),
|
|
84
|
-
});
|
|
85
|
-
expect(report.outcome).toBe("axe-missing");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("runAudit — real audit flow", () => {
|
|
90
|
-
let tmp: string;
|
|
91
|
-
beforeEach(async () => { tmp = await mkTmp(); });
|
|
92
|
-
afterEach(async () => { await fs.rm(tmp, { recursive: true, force: true }); });
|
|
93
|
-
|
|
94
|
-
it("reports outcome=ok when no violations fire", async () => {
|
|
95
|
-
const file = await writeHtml(tmp, "clean.html", "<html><body><h1>Hi</h1></body></html>");
|
|
96
|
-
const report = await runAudit([file], {
|
|
97
|
-
axeLoader: async () => createFakeAxe([{ violations: [] }]),
|
|
98
|
-
domLoader: async () => fakeDom,
|
|
99
|
-
});
|
|
100
|
-
expect(report.outcome).toBe("ok");
|
|
101
|
-
expect(report.filesScanned).toBe(1);
|
|
102
|
-
expect(report.violations).toEqual([]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("aggregates violations across multiple files with per-file attribution", async () => {
|
|
106
|
-
const a = await writeHtml(tmp, "a.html", "<html></html>");
|
|
107
|
-
const b = await writeHtml(tmp, "b.html", "<html></html>");
|
|
108
|
-
const violationA = {
|
|
109
|
-
id: "image-alt",
|
|
110
|
-
impact: "serious" as AuditImpact,
|
|
111
|
-
help: "Images must have alternate text",
|
|
112
|
-
helpUrl: "https://axe.dev/image-alt",
|
|
113
|
-
nodes: [{ target: ["html > body > img"], failureSummary: "missing alt" }],
|
|
114
|
-
};
|
|
115
|
-
const violationB = {
|
|
116
|
-
id: "color-contrast",
|
|
117
|
-
impact: "critical" as AuditImpact,
|
|
118
|
-
help: "Elements must have sufficient contrast",
|
|
119
|
-
nodes: [{ target: "html > body > p", failureSummary: "1.2:1 ratio" }],
|
|
120
|
-
};
|
|
121
|
-
const report = await runAudit([a, b], {
|
|
122
|
-
axeLoader: async () =>
|
|
123
|
-
createFakeAxe([
|
|
124
|
-
{ violations: [violationA] },
|
|
125
|
-
{ violations: [violationB] },
|
|
126
|
-
]),
|
|
127
|
-
domLoader: async () => fakeDom,
|
|
128
|
-
});
|
|
129
|
-
expect(report.outcome).toBe("violations");
|
|
130
|
-
expect(report.filesScanned).toBe(2);
|
|
131
|
-
expect(report.violations.length).toBe(2);
|
|
132
|
-
expect(report.violations[0].file).toBe(path.resolve(a));
|
|
133
|
-
expect(report.violations[1].file).toBe(path.resolve(b));
|
|
134
|
-
expect(report.impactCounts.serious).toBe(1);
|
|
135
|
-
expect(report.impactCounts.critical).toBe(1);
|
|
136
|
-
expect(report.impactCounts.minor).toBe(0);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("drops violations below minImpact threshold", async () => {
|
|
140
|
-
const file = await writeHtml(tmp, "page.html", "<html></html>");
|
|
141
|
-
const report = await runAudit([file], {
|
|
142
|
-
minImpact: "serious",
|
|
143
|
-
axeLoader: async () =>
|
|
144
|
-
createFakeAxe([
|
|
145
|
-
{
|
|
146
|
-
violations: [
|
|
147
|
-
{ id: "minor-rule", impact: "minor", help: "x", nodes: [] },
|
|
148
|
-
{ id: "moderate-rule", impact: "moderate", help: "y", nodes: [] },
|
|
149
|
-
{ id: "serious-rule", impact: "serious", help: "z", nodes: [{ target: "x" }] },
|
|
150
|
-
{ id: "critical-rule", impact: "critical", help: "w", nodes: [{ target: "y" }] },
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
]),
|
|
154
|
-
domLoader: async () => fakeDom,
|
|
155
|
-
});
|
|
156
|
-
expect(report.violations.map((v) => v.rule)).toEqual([
|
|
157
|
-
"serious-rule",
|
|
158
|
-
"critical-rule",
|
|
159
|
-
]);
|
|
160
|
-
expect(report.impactCounts.minor).toBe(0);
|
|
161
|
-
expect(report.impactCounts.moderate).toBe(0);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("attaches fixHint for recognised rule ids", async () => {
|
|
165
|
-
const file = await writeHtml(tmp, "form.html", "<html></html>");
|
|
166
|
-
const report = await runAudit([file], {
|
|
167
|
-
axeLoader: async () =>
|
|
168
|
-
createFakeAxe([
|
|
169
|
-
{
|
|
170
|
-
violations: [
|
|
171
|
-
{ id: "label", impact: "serious", help: "Labels", nodes: [{ target: "input" }] },
|
|
172
|
-
{ id: "unknown-rule-xyz", impact: "critical", help: "?", nodes: [{ target: "div" }] },
|
|
173
|
-
],
|
|
174
|
-
},
|
|
175
|
-
]),
|
|
176
|
-
domLoader: async () => fakeDom,
|
|
177
|
-
});
|
|
178
|
-
const labelV = report.violations.find((v) => v.rule === "label")!;
|
|
179
|
-
const unknownV = report.violations.find((v) => v.rule === "unknown-rule-xyz")!;
|
|
180
|
-
expect(labelV.fixHint).toMatch(/label/i);
|
|
181
|
-
expect(unknownV.fixHint).toBeUndefined();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("caps node list at 10 per violation and truncates long HTML snippets", async () => {
|
|
185
|
-
const file = await writeHtml(tmp, "big.html", "<html></html>");
|
|
186
|
-
const manyNodes = Array.from({ length: 25 }, (_, i) => ({
|
|
187
|
-
target: [`#node${i}`],
|
|
188
|
-
failureSummary: "x",
|
|
189
|
-
html: "<div>" + "a".repeat(500) + "</div>",
|
|
190
|
-
}));
|
|
191
|
-
const report = await runAudit([file], {
|
|
192
|
-
axeLoader: async () =>
|
|
193
|
-
createFakeAxe([
|
|
194
|
-
{ violations: [{ id: "landmark-one-main", impact: "moderate", help: "m", nodes: manyNodes }] },
|
|
195
|
-
]),
|
|
196
|
-
domLoader: async () => fakeDom,
|
|
197
|
-
});
|
|
198
|
-
const v = report.violations[0];
|
|
199
|
-
expect(v.nodes.length).toBe(10);
|
|
200
|
-
for (const n of v.nodes) {
|
|
201
|
-
expect(n.html!.length).toBeLessThanOrEqual(300);
|
|
202
|
-
expect(n.html!.endsWith("...")).toBe(true);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("skips unreadable files without failing the whole run", async () => {
|
|
207
|
-
const good = await writeHtml(tmp, "good.html", "<html></html>");
|
|
208
|
-
const missing = path.join(tmp, "does-not-exist.html");
|
|
209
|
-
const report = await runAudit([missing, good], {
|
|
210
|
-
axeLoader: async () =>
|
|
211
|
-
createFakeAxe([
|
|
212
|
-
{ violations: [] },
|
|
213
|
-
]),
|
|
214
|
-
domLoader: async () => fakeDom,
|
|
215
|
-
});
|
|
216
|
-
expect(report.filesScanned).toBe(1);
|
|
217
|
-
expect(report.outcome).toBe("ok");
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("honours maxFiles cap", async () => {
|
|
221
|
-
const files: string[] = [];
|
|
222
|
-
for (let i = 0; i < 5; i++) {
|
|
223
|
-
files.push(await writeHtml(tmp, `p${i}.html`, "<html></html>"));
|
|
224
|
-
}
|
|
225
|
-
const report = await runAudit(files, {
|
|
226
|
-
maxFiles: 2,
|
|
227
|
-
axeLoader: async () =>
|
|
228
|
-
createFakeAxe([
|
|
229
|
-
{ violations: [] },
|
|
230
|
-
{ violations: [] },
|
|
231
|
-
{ violations: [] },
|
|
232
|
-
{ violations: [] },
|
|
233
|
-
{ violations: [] },
|
|
234
|
-
]),
|
|
235
|
-
domLoader: async () => fakeDom,
|
|
236
|
-
});
|
|
237
|
-
expect(report.filesScanned).toBe(2);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("produces a JSON-serialisable report (stable shape contract)", async () => {
|
|
241
|
-
const file = await writeHtml(tmp, "x.html", "<html></html>");
|
|
242
|
-
const report = await runAudit([file], {
|
|
243
|
-
axeLoader: async () =>
|
|
244
|
-
createFakeAxe([
|
|
245
|
-
{
|
|
246
|
-
violations: [
|
|
247
|
-
{
|
|
248
|
-
id: "image-alt",
|
|
249
|
-
impact: "serious",
|
|
250
|
-
help: "Images",
|
|
251
|
-
helpUrl: "https://x",
|
|
252
|
-
nodes: [{ target: ["img"], failureSummary: "s" }],
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
]),
|
|
257
|
-
domLoader: async () => fakeDom,
|
|
258
|
-
});
|
|
259
|
-
const roundtrip = JSON.parse(JSON.stringify(report));
|
|
260
|
-
expect(roundtrip).toMatchObject({
|
|
261
|
-
outcome: "violations",
|
|
262
|
-
filesScanned: 1,
|
|
263
|
-
minImpact: "minor",
|
|
264
|
-
});
|
|
265
|
-
expect(roundtrip.violations[0]).toMatchObject({
|
|
266
|
-
rule: "image-alt",
|
|
267
|
-
impact: "serious",
|
|
268
|
-
help: "Images",
|
|
269
|
-
fixHint: expect.any(String),
|
|
270
|
-
});
|
|
271
|
-
expect(Array.isArray(roundtrip.violations[0].nodes)).toBe(true);
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
describe("formatAuditReport", () => {
|
|
276
|
-
it("renders an actionable message when deps are missing", () => {
|
|
277
|
-
const text = formatAuditReport({
|
|
278
|
-
outcome: "axe-missing",
|
|
279
|
-
filesScanned: 0,
|
|
280
|
-
violations: [],
|
|
281
|
-
impactCounts: { minor: 0, moderate: 0, serious: 0, critical: 0 },
|
|
282
|
-
minImpact: "minor",
|
|
283
|
-
durationMs: 0,
|
|
284
|
-
note: "axe-core not installed — skipping audit",
|
|
285
|
-
});
|
|
286
|
-
expect(text).toMatch(/bun add -d axe-core jsdom/);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("prints a PASS summary when there are zero violations", () => {
|
|
290
|
-
const text = formatAuditReport({
|
|
291
|
-
outcome: "ok",
|
|
292
|
-
filesScanned: 4,
|
|
293
|
-
violations: [],
|
|
294
|
-
impactCounts: { minor: 0, moderate: 0, serious: 0, critical: 0 },
|
|
295
|
-
minImpact: "minor",
|
|
296
|
-
durationMs: 12,
|
|
297
|
-
});
|
|
298
|
-
expect(text).toMatch(/PASS/);
|
|
299
|
-
expect(text).toMatch(/Files scanned: 4/);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("groups violations by rule and surfaces fix hints + doc links", () => {
|
|
303
|
-
const text = formatAuditReport({
|
|
304
|
-
outcome: "violations",
|
|
305
|
-
filesScanned: 2,
|
|
306
|
-
violations: [
|
|
307
|
-
{
|
|
308
|
-
file: "a.html",
|
|
309
|
-
rule: "image-alt",
|
|
310
|
-
impact: "serious",
|
|
311
|
-
help: "Images must have alt",
|
|
312
|
-
helpUrl: "https://example/image-alt",
|
|
313
|
-
nodes: [{ target: "img", failureSummary: "x" }],
|
|
314
|
-
fixHint: "Add alt attribute",
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
file: "b.html",
|
|
318
|
-
rule: "image-alt",
|
|
319
|
-
impact: "serious",
|
|
320
|
-
help: "Images must have alt",
|
|
321
|
-
nodes: [{ target: "img", failureSummary: "x" }],
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
impactCounts: { minor: 0, moderate: 0, serious: 2, critical: 0 },
|
|
325
|
-
minImpact: "minor",
|
|
326
|
-
durationMs: 45,
|
|
327
|
-
});
|
|
328
|
-
expect(text).toMatch(/\[SERIOUS\] image-alt/);
|
|
329
|
-
expect(text).toMatch(/Fix: Add alt attribute/);
|
|
330
|
-
expect(text).toMatch(/Docs: https:\/\/example\/image-alt/);
|
|
331
|
-
expect(text).toMatch(/2 file\(s\)/);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Phase 18.χ accessibility audit runner.
|
|
3
|
+
*
|
|
4
|
+
* Philosophy: we never actually install axe-core/jsdom in the test
|
|
5
|
+
* environment — that would both contradict the "optional peerDep"
|
|
6
|
+
* contract and slow every `bun test` run by a full second. Instead
|
|
7
|
+
* each test injects `axeLoader` + `domLoader` fakes and asserts on the
|
|
8
|
+
* report shape. A single "no deps installed" case exercises the real
|
|
9
|
+
* resolution path by leaving both loaders undefined.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import os from "os";
|
|
16
|
+
import { runAudit, formatAuditReport } from "../run-audit";
|
|
17
|
+
import type { AuditImpact } from "../types";
|
|
18
|
+
|
|
19
|
+
async function mkTmp(): Promise<string> {
|
|
20
|
+
return fs.mkdtemp(path.join(os.tmpdir(), "mandu-a11y-"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function writeHtml(dir: string, name: string, html: string): Promise<string> {
|
|
24
|
+
const full = path.join(dir, name);
|
|
25
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
26
|
+
await fs.writeFile(full, html, "utf-8");
|
|
27
|
+
return full;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fake axe-core handle. Returns the violations that the test preloads
|
|
32
|
+
* into `queued` for the next `.run()` call. Enables multi-file
|
|
33
|
+
* scenarios where each file yields a different result.
|
|
34
|
+
*/
|
|
35
|
+
function createFakeAxe(queued: Array<unknown>) {
|
|
36
|
+
let i = 0;
|
|
37
|
+
return {
|
|
38
|
+
default: {
|
|
39
|
+
async run() {
|
|
40
|
+
const next = queued[i] ?? { violations: [] };
|
|
41
|
+
i += 1;
|
|
42
|
+
return next;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A DOM provider stub that returns a no-op window. axe never touches
|
|
49
|
+
* it because the fake axe above ignores its `context` argument. */
|
|
50
|
+
const fakeDom = {
|
|
51
|
+
kind: "jsdom" as const,
|
|
52
|
+
async fromHtml() {
|
|
53
|
+
return {
|
|
54
|
+
window: { document: {} },
|
|
55
|
+
dispose() { /* no-op */ },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe("runAudit — dependency resolution", () => {
|
|
61
|
+
it("returns axe-missing when axeLoader rejects (graceful degradation)", async () => {
|
|
62
|
+
const report = await runAudit([], {
|
|
63
|
+
axeLoader: async () => { throw new Error("Cannot find module 'axe-core'"); },
|
|
64
|
+
});
|
|
65
|
+
expect(report.outcome).toBe("axe-missing");
|
|
66
|
+
expect(report.filesScanned).toBe(0);
|
|
67
|
+
expect(report.violations).toEqual([]);
|
|
68
|
+
expect(report.note).toMatch(/axe-core not installed/);
|
|
69
|
+
expect(report.durationMs).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns axe-missing when no DOM provider is available", async () => {
|
|
73
|
+
const report = await runAudit([], {
|
|
74
|
+
axeLoader: async () => createFakeAxe([]),
|
|
75
|
+
domLoader: async () => null,
|
|
76
|
+
});
|
|
77
|
+
expect(report.outcome).toBe("axe-missing");
|
|
78
|
+
expect(report.note).toMatch(/DOM provider/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("treats loader returning non-axe-shape as missing", async () => {
|
|
82
|
+
const report = await runAudit([], {
|
|
83
|
+
axeLoader: async () => ({ notAxe: true }),
|
|
84
|
+
});
|
|
85
|
+
expect(report.outcome).toBe("axe-missing");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("runAudit — real audit flow", () => {
|
|
90
|
+
let tmp: string;
|
|
91
|
+
beforeEach(async () => { tmp = await mkTmp(); });
|
|
92
|
+
afterEach(async () => { await fs.rm(tmp, { recursive: true, force: true }); });
|
|
93
|
+
|
|
94
|
+
it("reports outcome=ok when no violations fire", async () => {
|
|
95
|
+
const file = await writeHtml(tmp, "clean.html", "<html><body><h1>Hi</h1></body></html>");
|
|
96
|
+
const report = await runAudit([file], {
|
|
97
|
+
axeLoader: async () => createFakeAxe([{ violations: [] }]),
|
|
98
|
+
domLoader: async () => fakeDom,
|
|
99
|
+
});
|
|
100
|
+
expect(report.outcome).toBe("ok");
|
|
101
|
+
expect(report.filesScanned).toBe(1);
|
|
102
|
+
expect(report.violations).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("aggregates violations across multiple files with per-file attribution", async () => {
|
|
106
|
+
const a = await writeHtml(tmp, "a.html", "<html></html>");
|
|
107
|
+
const b = await writeHtml(tmp, "b.html", "<html></html>");
|
|
108
|
+
const violationA = {
|
|
109
|
+
id: "image-alt",
|
|
110
|
+
impact: "serious" as AuditImpact,
|
|
111
|
+
help: "Images must have alternate text",
|
|
112
|
+
helpUrl: "https://axe.dev/image-alt",
|
|
113
|
+
nodes: [{ target: ["html > body > img"], failureSummary: "missing alt" }],
|
|
114
|
+
};
|
|
115
|
+
const violationB = {
|
|
116
|
+
id: "color-contrast",
|
|
117
|
+
impact: "critical" as AuditImpact,
|
|
118
|
+
help: "Elements must have sufficient contrast",
|
|
119
|
+
nodes: [{ target: "html > body > p", failureSummary: "1.2:1 ratio" }],
|
|
120
|
+
};
|
|
121
|
+
const report = await runAudit([a, b], {
|
|
122
|
+
axeLoader: async () =>
|
|
123
|
+
createFakeAxe([
|
|
124
|
+
{ violations: [violationA] },
|
|
125
|
+
{ violations: [violationB] },
|
|
126
|
+
]),
|
|
127
|
+
domLoader: async () => fakeDom,
|
|
128
|
+
});
|
|
129
|
+
expect(report.outcome).toBe("violations");
|
|
130
|
+
expect(report.filesScanned).toBe(2);
|
|
131
|
+
expect(report.violations.length).toBe(2);
|
|
132
|
+
expect(report.violations[0].file).toBe(path.resolve(a));
|
|
133
|
+
expect(report.violations[1].file).toBe(path.resolve(b));
|
|
134
|
+
expect(report.impactCounts.serious).toBe(1);
|
|
135
|
+
expect(report.impactCounts.critical).toBe(1);
|
|
136
|
+
expect(report.impactCounts.minor).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("drops violations below minImpact threshold", async () => {
|
|
140
|
+
const file = await writeHtml(tmp, "page.html", "<html></html>");
|
|
141
|
+
const report = await runAudit([file], {
|
|
142
|
+
minImpact: "serious",
|
|
143
|
+
axeLoader: async () =>
|
|
144
|
+
createFakeAxe([
|
|
145
|
+
{
|
|
146
|
+
violations: [
|
|
147
|
+
{ id: "minor-rule", impact: "minor", help: "x", nodes: [] },
|
|
148
|
+
{ id: "moderate-rule", impact: "moderate", help: "y", nodes: [] },
|
|
149
|
+
{ id: "serious-rule", impact: "serious", help: "z", nodes: [{ target: "x" }] },
|
|
150
|
+
{ id: "critical-rule", impact: "critical", help: "w", nodes: [{ target: "y" }] },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
]),
|
|
154
|
+
domLoader: async () => fakeDom,
|
|
155
|
+
});
|
|
156
|
+
expect(report.violations.map((v) => v.rule)).toEqual([
|
|
157
|
+
"serious-rule",
|
|
158
|
+
"critical-rule",
|
|
159
|
+
]);
|
|
160
|
+
expect(report.impactCounts.minor).toBe(0);
|
|
161
|
+
expect(report.impactCounts.moderate).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("attaches fixHint for recognised rule ids", async () => {
|
|
165
|
+
const file = await writeHtml(tmp, "form.html", "<html></html>");
|
|
166
|
+
const report = await runAudit([file], {
|
|
167
|
+
axeLoader: async () =>
|
|
168
|
+
createFakeAxe([
|
|
169
|
+
{
|
|
170
|
+
violations: [
|
|
171
|
+
{ id: "label", impact: "serious", help: "Labels", nodes: [{ target: "input" }] },
|
|
172
|
+
{ id: "unknown-rule-xyz", impact: "critical", help: "?", nodes: [{ target: "div" }] },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
]),
|
|
176
|
+
domLoader: async () => fakeDom,
|
|
177
|
+
});
|
|
178
|
+
const labelV = report.violations.find((v) => v.rule === "label")!;
|
|
179
|
+
const unknownV = report.violations.find((v) => v.rule === "unknown-rule-xyz")!;
|
|
180
|
+
expect(labelV.fixHint).toMatch(/label/i);
|
|
181
|
+
expect(unknownV.fixHint).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("caps node list at 10 per violation and truncates long HTML snippets", async () => {
|
|
185
|
+
const file = await writeHtml(tmp, "big.html", "<html></html>");
|
|
186
|
+
const manyNodes = Array.from({ length: 25 }, (_, i) => ({
|
|
187
|
+
target: [`#node${i}`],
|
|
188
|
+
failureSummary: "x",
|
|
189
|
+
html: "<div>" + "a".repeat(500) + "</div>",
|
|
190
|
+
}));
|
|
191
|
+
const report = await runAudit([file], {
|
|
192
|
+
axeLoader: async () =>
|
|
193
|
+
createFakeAxe([
|
|
194
|
+
{ violations: [{ id: "landmark-one-main", impact: "moderate", help: "m", nodes: manyNodes }] },
|
|
195
|
+
]),
|
|
196
|
+
domLoader: async () => fakeDom,
|
|
197
|
+
});
|
|
198
|
+
const v = report.violations[0];
|
|
199
|
+
expect(v.nodes.length).toBe(10);
|
|
200
|
+
for (const n of v.nodes) {
|
|
201
|
+
expect(n.html!.length).toBeLessThanOrEqual(300);
|
|
202
|
+
expect(n.html!.endsWith("...")).toBe(true);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("skips unreadable files without failing the whole run", async () => {
|
|
207
|
+
const good = await writeHtml(tmp, "good.html", "<html></html>");
|
|
208
|
+
const missing = path.join(tmp, "does-not-exist.html");
|
|
209
|
+
const report = await runAudit([missing, good], {
|
|
210
|
+
axeLoader: async () =>
|
|
211
|
+
createFakeAxe([
|
|
212
|
+
{ violations: [] },
|
|
213
|
+
]),
|
|
214
|
+
domLoader: async () => fakeDom,
|
|
215
|
+
});
|
|
216
|
+
expect(report.filesScanned).toBe(1);
|
|
217
|
+
expect(report.outcome).toBe("ok");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("honours maxFiles cap", async () => {
|
|
221
|
+
const files: string[] = [];
|
|
222
|
+
for (let i = 0; i < 5; i++) {
|
|
223
|
+
files.push(await writeHtml(tmp, `p${i}.html`, "<html></html>"));
|
|
224
|
+
}
|
|
225
|
+
const report = await runAudit(files, {
|
|
226
|
+
maxFiles: 2,
|
|
227
|
+
axeLoader: async () =>
|
|
228
|
+
createFakeAxe([
|
|
229
|
+
{ violations: [] },
|
|
230
|
+
{ violations: [] },
|
|
231
|
+
{ violations: [] },
|
|
232
|
+
{ violations: [] },
|
|
233
|
+
{ violations: [] },
|
|
234
|
+
]),
|
|
235
|
+
domLoader: async () => fakeDom,
|
|
236
|
+
});
|
|
237
|
+
expect(report.filesScanned).toBe(2);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("produces a JSON-serialisable report (stable shape contract)", async () => {
|
|
241
|
+
const file = await writeHtml(tmp, "x.html", "<html></html>");
|
|
242
|
+
const report = await runAudit([file], {
|
|
243
|
+
axeLoader: async () =>
|
|
244
|
+
createFakeAxe([
|
|
245
|
+
{
|
|
246
|
+
violations: [
|
|
247
|
+
{
|
|
248
|
+
id: "image-alt",
|
|
249
|
+
impact: "serious",
|
|
250
|
+
help: "Images",
|
|
251
|
+
helpUrl: "https://x",
|
|
252
|
+
nodes: [{ target: ["img"], failureSummary: "s" }],
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
]),
|
|
257
|
+
domLoader: async () => fakeDom,
|
|
258
|
+
});
|
|
259
|
+
const roundtrip = JSON.parse(JSON.stringify(report));
|
|
260
|
+
expect(roundtrip).toMatchObject({
|
|
261
|
+
outcome: "violations",
|
|
262
|
+
filesScanned: 1,
|
|
263
|
+
minImpact: "minor",
|
|
264
|
+
});
|
|
265
|
+
expect(roundtrip.violations[0]).toMatchObject({
|
|
266
|
+
rule: "image-alt",
|
|
267
|
+
impact: "serious",
|
|
268
|
+
help: "Images",
|
|
269
|
+
fixHint: expect.any(String),
|
|
270
|
+
});
|
|
271
|
+
expect(Array.isArray(roundtrip.violations[0].nodes)).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("formatAuditReport", () => {
|
|
276
|
+
it("renders an actionable message when deps are missing", () => {
|
|
277
|
+
const text = formatAuditReport({
|
|
278
|
+
outcome: "axe-missing",
|
|
279
|
+
filesScanned: 0,
|
|
280
|
+
violations: [],
|
|
281
|
+
impactCounts: { minor: 0, moderate: 0, serious: 0, critical: 0 },
|
|
282
|
+
minImpact: "minor",
|
|
283
|
+
durationMs: 0,
|
|
284
|
+
note: "axe-core not installed — skipping audit",
|
|
285
|
+
});
|
|
286
|
+
expect(text).toMatch(/bun add -d axe-core jsdom/);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("prints a PASS summary when there are zero violations", () => {
|
|
290
|
+
const text = formatAuditReport({
|
|
291
|
+
outcome: "ok",
|
|
292
|
+
filesScanned: 4,
|
|
293
|
+
violations: [],
|
|
294
|
+
impactCounts: { minor: 0, moderate: 0, serious: 0, critical: 0 },
|
|
295
|
+
minImpact: "minor",
|
|
296
|
+
durationMs: 12,
|
|
297
|
+
});
|
|
298
|
+
expect(text).toMatch(/PASS/);
|
|
299
|
+
expect(text).toMatch(/Files scanned: 4/);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("groups violations by rule and surfaces fix hints + doc links", () => {
|
|
303
|
+
const text = formatAuditReport({
|
|
304
|
+
outcome: "violations",
|
|
305
|
+
filesScanned: 2,
|
|
306
|
+
violations: [
|
|
307
|
+
{
|
|
308
|
+
file: "a.html",
|
|
309
|
+
rule: "image-alt",
|
|
310
|
+
impact: "serious",
|
|
311
|
+
help: "Images must have alt",
|
|
312
|
+
helpUrl: "https://example/image-alt",
|
|
313
|
+
nodes: [{ target: "img", failureSummary: "x" }],
|
|
314
|
+
fixHint: "Add alt attribute",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
file: "b.html",
|
|
318
|
+
rule: "image-alt",
|
|
319
|
+
impact: "serious",
|
|
320
|
+
help: "Images must have alt",
|
|
321
|
+
nodes: [{ target: "img", failureSummary: "x" }],
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
impactCounts: { minor: 0, moderate: 0, serious: 2, critical: 0 },
|
|
325
|
+
minImpact: "minor",
|
|
326
|
+
durationMs: 45,
|
|
327
|
+
});
|
|
328
|
+
expect(text).toMatch(/\[SERIOUS\] image-alt/);
|
|
329
|
+
expect(text).toMatch(/Fix: Add alt attribute/);
|
|
330
|
+
expect(text).toMatch(/Docs: https:\/\/example\/image-alt/);
|
|
331
|
+
expect(text).toMatch(/2 file\(s\)/);
|
|
332
|
+
});
|
|
333
|
+
});
|