-
-
Notifications
You must be signed in to change notification settings - Fork 424
test: mock client side requests in lighthouse #1171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,26 @@ | ||
| /** | ||
| * Lighthouse CI puppeteer setup script. | ||
| * Sets the color mode (light/dark) before running accessibility audits. | ||
| * | ||
| * Sets the color mode (light/dark) before running accessibility audits | ||
| * and intercepts client-side API requests using the same fixture data | ||
| * as the Playwright E2E tests. | ||
| * | ||
| * The color mode is determined by the LIGHTHOUSE_COLOR_MODE environment variable. | ||
| * If not set, defaults to 'dark'. | ||
| * | ||
| * Request interception uses CDP (Chrome DevTools Protocol) at the browser level | ||
| * so it applies to all pages Lighthouse opens, not just the setup page. | ||
| */ | ||
|
|
||
| const mockRoutes = require('./test/fixtures/mock-routes.cjs') | ||
|
|
||
| module.exports = async function setup(browser, { url }) { | ||
| const colorMode = process.env.LIGHTHOUSE_COLOR_MODE || 'dark' | ||
|
|
||
| // Set up browser-level request interception via CDP. | ||
| // This ensures mocking applies to pages Lighthouse creates after setup. | ||
| setupBrowserRequestInterception(browser) | ||
|
|
||
| const page = await browser.newPage() | ||
|
|
||
| // Set localStorage before navigating so @nuxtjs/color-mode picks it up | ||
|
|
@@ -21,3 +34,40 @@ module.exports = async function setup(browser, { url }) { | |
| // Close the page - Lighthouse will open its own with localStorage already set | ||
| await page.close() | ||
| } | ||
|
|
||
| /** | ||
| * Set up request interception on every new page target the browser creates. | ||
| * Uses Puppeteer's page-level request interception, applied automatically | ||
| * to each new page via the 'targetcreated' event. | ||
| * | ||
| * @param {import('puppeteer').Browser} browser | ||
| */ | ||
| function setupBrowserRequestInterception(browser) { | ||
| browser.on('targetcreated', async target => { | ||
| if (target.type() !== 'page') return | ||
|
|
||
| try { | ||
| const page = await target.page() | ||
| if (!page) return | ||
|
|
||
| await page.setRequestInterception(true) | ||
| page.on('request', request => { | ||
| const requestUrl = request.url() | ||
| const result = mockRoutes.matchRoute(requestUrl) | ||
|
|
||
| if (result) { | ||
| request.respond({ | ||
| status: result.response.status, | ||
| contentType: result.response.contentType, | ||
| body: result.response.body, | ||
| }) | ||
| } else { | ||
| request.continue() | ||
| } | ||
| }) | ||
|
Comment on lines
+62
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling within the The 🛡️ Proposed fix to add error handling cdp.on('Fetch.requestPaused', async event => {
+ try {
const requestUrl = event.request.url
const result = mockRoutes.matchRoute(requestUrl)
if (result) {
const body = Buffer.from(result.response.body).toString('base64')
await cdp.send('Fetch.fulfillRequest', {
requestId: event.requestId,
responseCode: result.response.status,
responseHeaders: [
{ name: 'Content-Type', value: result.response.contentType },
{ name: 'Access-Control-Allow-Origin', value: '*' },
],
body,
})
} else {
await cdp.send('Fetch.continueRequest', {
requestId: event.requestId,
})
}
+ } catch {
+ // Target may have closed mid-request; safe to ignore.
+ }
}) |
||
| } catch { | ||
| // Target may have been closed before we could set up interception. | ||
| // This is expected for transient targets like service workers. | ||
| } | ||
| }) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 81
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 156
🏁 Script executed:
rg -t js "mockRoutes\|getExternalApiName" --max-count=20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 15791
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 84
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 3017
Block unmocked external API requests during audits to preserve determinism.
Currently, when a request URL matches a known external API domain (e.g., npm registry, GitHub API) but no fixture exists,
request.continue()allows the real API call through. This can cause non-deterministic audit results. UsinggetExternalApiName()to detect these cases and respond with an error keeps audits isolated and reproducible.Suggested implementation
page.on('request', request => { const requestUrl = request.url() const result = mockRoutes.matchRoute(requestUrl) + const apiName = mockRoutes.getExternalApiName(requestUrl) if (result) { request.respond({ status: result.response.status, contentType: result.response.contentType, body: result.response.body, }) + } else if (apiName) { + request.respond({ + status: 500, + contentType: 'text/plain', + body: `Unmocked external request: ${apiName}`, + }) } else { request.continue() } })Verify the response behaviour works as expected with your Puppeteer and Lighthouse versions.