fix: set emulatedUserAgent and use finalDisplayedUrl in lighthouse_audit (#2795)

`lighthouse_audit` never set `emulatedUserAgent`, so desktop audits ran
with Lighthouse's default mobile UA. It now mirrors the UA constants
Lighthouse's own presets use. It also reported `URL: undefined` in
snapshot mode, because `mainDocumentUrl` is only populated for
navigations; it now uses `finalDisplayedUrl`.
This commit is contained in:
Nikolay Vitkov
2026-09-21 16:00:30 +00:00
committed by GitHub
parent 5068584894
commit 941f82a3ec
3 changed files with 98 additions and 2 deletions
+15 -1
View File
@@ -21,6 +21,16 @@ import {ToolCategory} from './categories.js';
import {startTrace} from './performance.js';
import {definePageTool} from './ToolDefinition.js';
// Kept in sync with `constants.userAgents` in Lighthouse's
// core/config/constants.js, which `lighthouse:default` and the `desktop`
// preset use. The bundle in src/third_party only re-exports the runner
// entrypoints, so these cannot be imported and are mirrored here alongside the
// screen emulation metrics below. Refresh via scripts/update-lighthouse.ts.
const MOBILE_USER_AGENT =
'Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36';
const DESKTOP_USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
name: 'lighthouse_audit',
description: `Get Lighthouse score and reports for accessibility, SEO, best practices, and agentic browsing. This excludes performance. For performance audits, run ${startTrace(args).name}`,
@@ -79,6 +89,7 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
deviceScaleFactor: 1,
disabled: false,
};
flags.emulatedUserAgent = DESKTOP_USER_AGENT;
} else {
flags.formFactor = 'mobile';
flags.screenEmulation = {
@@ -88,6 +99,7 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
deviceScaleFactor: 1.75,
disabled: false,
};
flags.emulatedUserAgent = MOBILE_USER_AGENT;
}
let result: RunnerResult | undefined;
@@ -162,7 +174,9 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
summary: {
mode,
device,
url: lhr.mainDocumentUrl,
// `mainDocumentUrl` is only set for navigations, whereas
// `finalDisplayedUrl` is populated for every gather mode.
url: lhr.finalDisplayedUrl,
scores: categoryScores,
audits: {
failed: failedAudits,
+5 -1
View File
@@ -201,12 +201,16 @@ export function createHandlerMocks(options: Partial<ParsedArguments> = {}): {
return {page, context, response, args};
}
export function createMockRunnerResult(): RunnerResult {
export function createMockRunnerResult(
lhrOverrides: Partial<Result> = {},
): RunnerResult {
const lhr = {
finalDisplayedUrl: 'http://localhost',
mainDocumentUrl: 'http://localhost',
categories: {},
audits: {},
timing: {total: 0},
...lhrOverrides,
};
return {
lhr: lhr as unknown as Result,
+78
View File
@@ -108,6 +108,84 @@ describe('lighthouse', () => {
sinon.assert.calledOnceWithExactly(page.restoreEmulation);
});
it('emulates a desktop user agent for desktop audits', async () => {
const {page, context, response, args} = createHandlerMocks();
context.saveTemporaryFile.resolves({filepath: 'report.json'});
const navigation = sinon
.stub(lighthouseRunner, 'navigation')
.resolves(createMockRunnerResult());
await lighthouseAudit(args).handler(
{
params: {
mode: 'navigation',
device: 'desktop',
},
page,
},
response,
context,
);
const {flags} = navigation.firstCall.args[2];
assert.equal(flags?.formFactor, 'desktop');
assert.match(String(flags?.emulatedUserAgent), /Macintosh/);
assert.doesNotMatch(String(flags?.emulatedUserAgent), /Mobile/);
});
it('emulates a mobile user agent for mobile audits', async () => {
const {page, context, response, args} = createHandlerMocks();
context.saveTemporaryFile.resolves({filepath: 'report.json'});
const snapshot = sinon
.stub(lighthouseRunner, 'snapshot')
.resolves(createMockRunnerResult());
await lighthouseAudit(args).handler(
{
params: {
mode: 'snapshot',
device: 'mobile',
},
page,
},
response,
context,
);
const {flags} = snapshot.firstCall.args[1];
assert.equal(flags?.formFactor, 'mobile');
assert.match(String(flags?.emulatedUserAgent), /Mobile Safari/);
});
it('reports the URL in snapshot mode, where mainDocumentUrl is unset', async () => {
const {page, context, response, args} = createHandlerMocks();
context.saveTemporaryFile.resolves({filepath: 'report.json'});
sinon.stub(lighthouseRunner, 'snapshot').resolves(
createMockRunnerResult({
mainDocumentUrl: undefined,
finalDisplayedUrl: 'https://example.com/page',
}),
);
await lighthouseAudit(args).handler(
{
params: {
mode: 'snapshot',
device: 'mobile',
},
page,
},
response,
context,
);
sinon.assert.calledOnce(response.attachLighthouseResult);
assert.equal(
response.attachLighthouseResult.firstCall.args[0].summary.url,
'https://example.com/page',
);
});
it('runs Lighthouse in snapshot mode with mobile device', async () => {
server.addHtmlRoute('/test-mobile', html`<div>Test Mobile</div>`);