Skip to content

Commit 33a9df0

Browse files
authored
test: mock client side requests in lighthouse (#1171)
1 parent 16b012a commit 33a9df0

File tree

7 files changed

+611
-450
lines changed

7 files changed

+611
-450
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ jobs:
184184
run: pnpm build:test
185185

186186
- name: ♿ Accessibility audit (Lighthouse - ${{ matrix.mode }} mode)
187-
run: ./scripts/lighthouse-a11y.sh
187+
run: pnpm test:a11y:prebuilt
188188
env:
189189
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
190190
LIGHTHOUSE_COLOR_MODE: ${{ matrix.mode }}

CONTRIBUTING.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ This focus helps guide our project decisions as a community and what we choose t
5252
- [Testing](#testing)
5353
- [Unit tests](#unit-tests)
5454
- [Component accessibility tests](#component-accessibility-tests)
55+
- [Lighthouse accessibility tests](#lighthouse-accessibility-tests)
5556
- [End to end tests](#end-to-end-tests)
5657
- [Test fixtures (mocking external APIs)](#test-fixtures-mocking-external-apis)
5758
- [Submitting changes](#submitting-changes)
@@ -111,6 +112,7 @@ pnpm test # Run all Vitest tests
111112
pnpm test:unit # Unit tests only
112113
pnpm test:nuxt # Nuxt component tests
113114
pnpm test:browser # Playwright E2E tests
115+
pnpm test:a11y # Lighthouse accessibility audits
114116
```
115117

116118
### Project structure
@@ -598,6 +600,40 @@ A coverage test in `test/unit/a11y-component-coverage.spec.ts` ensures all compo
598600
> [!IMPORTANT]
599601
> Just because axe-core doesn't find any obvious issues, it does not mean a component is accessible. Please do additional checks and use best practices.
600602
603+
### Lighthouse accessibility tests
604+
605+
In addition to component-level axe audits, the project runs full-page accessibility audits using [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci). These test the rendered pages in both light and dark mode against Lighthouse's accessibility category, requiring a perfect score.
606+
607+
#### How it works
608+
609+
1. The project is built in test mode (`pnpm build:test`), which activates server-side fixture mocking
610+
2. Lighthouse CI starts a preview server and audits three URLs: `/`, `/search?q=nuxt`, and `/package/nuxt`
611+
3. A Puppeteer setup script (`lighthouse-setup.cjs`) runs before each audit to set the color mode and intercept client-side API requests using the same fixtures as the E2E tests
612+
613+
#### Running locally
614+
615+
```bash
616+
# Build + run both light and dark audits
617+
pnpm test:a11y
618+
619+
# Or against an existing test build
620+
pnpm test:a11y:prebuilt
621+
622+
# Or run a single color mode manually
623+
pnpm build:test
624+
LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse-a11y.sh
625+
```
626+
627+
This requires Chrome or Chromium to be installed. The script will auto-detect common installation paths. Results are printed to the terminal and saved in `.lighthouseci/`.
628+
629+
#### Configuration
630+
631+
| File | Purpose |
632+
| ---------------------------- | --------------------------------------------------------- |
633+
| `.lighthouserc.cjs` | Lighthouse CI config (URLs, assertions, Chrome path) |
634+
| `lighthouse-setup.cjs` | Puppeteer script for color mode + client-side API mocking |
635+
| `scripts/lighthouse-a11y.sh` | Shell wrapper that runs the audit for a given color mode |
636+
601637
### End to end tests
602638

603639
Write end-to-end tests using Playwright:
@@ -619,10 +655,12 @@ E2E tests use a fixture system to mock external API requests, ensuring tests are
619655
- Serves pre-recorded fixture data from `test/fixtures/`
620656
- Enabled via `NUXT_TEST_FIXTURES=true` or Nuxt test mode
621657

622-
**Client-side mocking** (`test/e2e/test-utils.ts`):
658+
**Client-side mocking** (`test/fixtures/mock-routes.cjs`):
623659

624-
- Uses Playwright's route interception to mock browser requests
625-
- All test files import from `./test-utils` instead of `@nuxt/test-utils/playwright`
660+
- Shared URL matching and response generation logic used by both Playwright E2E tests and Lighthouse CI
661+
- Playwright tests (`test/e2e/test-utils.ts`) use this via `page.route()` interception
662+
- Lighthouse tests (`lighthouse-setup.cjs`) use this via Puppeteer request interception
663+
- All E2E test files import from `./test-utils` instead of `@nuxt/test-utils/playwright`
626664
- Throws a clear error if an unmocked external request is detected
627665

628666
#### Fixture files
@@ -670,7 +708,7 @@ URL: https://registry.npmjs.org/some-package
670708
You need to either:
671709

672710
1. Add a fixture file for that package/endpoint
673-
2. Update the mock handlers in `test/e2e/test-utils.ts` (client) or `modules/runtime/server/cache.ts` (server)
711+
2. Update the mock handlers in `test/fixtures/mock-routes.cjs` (client) or `modules/runtime/server/cache.ts` (server)
674712

675713
## Submitting changes
676714

knip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const config: KnipConfig = {
3232
'@vercel/kv',
3333
'@voidzero-dev/vite-plus-core',
3434
'vite-plus!',
35-
'h3',
35+
'puppeteer',
3636
/** Needs to be explicitly installed, even though it is not imported, to avoid type errors. */
3737
'unplugin-vue-router',
3838
'vite-plugin-pwa',

lighthouse-setup.cjs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
/**
22
* Lighthouse CI puppeteer setup script.
3-
* Sets the color mode (light/dark) before running accessibility audits.
3+
*
4+
* Sets the color mode (light/dark) before running accessibility audits
5+
* and intercepts client-side API requests using the same fixture data
6+
* as the Playwright E2E tests.
47
*
58
* The color mode is determined by the LIGHTHOUSE_COLOR_MODE environment variable.
69
* If not set, defaults to 'dark'.
10+
*
11+
* Request interception uses CDP (Chrome DevTools Protocol) Fetch domain
12+
* at the browser level, which avoids conflicts with Lighthouse's own
13+
* Puppeteer-level request interception.
714
*/
815

16+
const mockRoutes = require('./test/fixtures/mock-routes.cjs')
17+
918
module.exports = async function setup(browser, { url }) {
1019
const colorMode = process.env.LIGHTHOUSE_COLOR_MODE || 'dark'
20+
21+
// Set up browser-level request interception via CDP Fetch domain.
22+
// This operates below Puppeteer's request interception layer so it
23+
// doesn't conflict with Lighthouse's own setRequestInterception usage.
24+
await setupCdpRequestInterception(browser)
25+
1126
const page = await browser.newPage()
1227

1328
// Set localStorage before navigating so @nuxtjs/color-mode picks it up
@@ -21,3 +36,55 @@ module.exports = async function setup(browser, { url }) {
2136
// Close the page - Lighthouse will open its own with localStorage already set
2237
await page.close()
2338
}
39+
40+
/**
41+
* Set up request interception using CDP's Fetch domain on the browser's
42+
* default context. This intercepts requests at a lower level than Puppeteer's
43+
* page.setRequestInterception(), avoiding "Request is already handled!" errors
44+
* when Lighthouse sets up its own interception.
45+
*
46+
* @param {import('puppeteer').Browser} browser
47+
*/
48+
async function setupCdpRequestInterception(browser) {
49+
// Build URL pattern list for CDP Fetch.enable from our route definitions
50+
const cdpPatterns = mockRoutes.routes.map(route => ({
51+
urlPattern: route.pattern.replace('/**', '/*'),
52+
requestStage: 'Request',
53+
}))
54+
55+
// Listen for new targets so we can attach CDP interception to each page
56+
browser.on('targetcreated', async target => {
57+
if (target.type() !== 'page') return
58+
59+
try {
60+
const cdp = await target.createCDPSession()
61+
62+
cdp.on('Fetch.requestPaused', async event => {
63+
const requestUrl = event.request.url
64+
const result = mockRoutes.matchRoute(requestUrl)
65+
66+
if (result) {
67+
const body = Buffer.from(result.response.body).toString('base64')
68+
await cdp.send('Fetch.fulfillRequest', {
69+
requestId: event.requestId,
70+
responseCode: result.response.status,
71+
responseHeaders: [
72+
{ name: 'Content-Type', value: result.response.contentType },
73+
{ name: 'Access-Control-Allow-Origin', value: '*' },
74+
],
75+
body,
76+
})
77+
} else {
78+
await cdp.send('Fetch.continueRequest', {
79+
requestId: event.requestId,
80+
})
81+
}
82+
})
83+
84+
await cdp.send('Fetch.enable', { patterns: cdpPatterns })
85+
} catch {
86+
// Target may have been closed before we could attach.
87+
// This is expected for transient targets like service workers.
88+
}
89+
})
90+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"generate:fixtures": "node scripts/generate-fixtures.ts",
3232
"generate:lexicons": "lex build --lexicons lexicons --out shared/types/lexicons --clear",
3333
"test": "vite test",
34+
"test:a11y": "pnpm build:test && pnpm test:a11y:prebuilt",
35+
"test:a11y:prebuilt": "LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse-a11y.sh && LIGHTHOUSE_COLOR_MODE=light ./scripts/lighthouse-a11y.sh",
3436
"test:browser": "pnpm build:test && pnpm test:browser:prebuilt",
3537
"test:browser:prebuilt": "playwright test",
3638
"test:browser:ui": "pnpm build:test && pnpm test:browser:prebuilt --ui",

0 commit comments

Comments
 (0)