mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 05:02:31 +00:00
refactor(lighthouse): move Lighthouse runner methods to McpPage and add bundle types (#3283)
Moves Lighthouse runner invocations (`lighthouseNavigation` and `lighthouseSnapshot`) onto `McpPage` / `ContextPage`, adds type declarations for `src/third_party/lighthouse-devtools-mcp-bundle.js`, and copies the bundle in `scripts/post-build.ts`. No user-visible behavior change.
This commit is contained in:
1 parent
b20f63b798
commit
e1823a2f59
7 files changed
+86
-55
No files matched your search
@@ -178,6 +178,15 @@ export const ExperimentName = {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
|
||||
// Copy lighthouse bundle that tsc ignores due to lighthouse-devtools-mcp-bundle.d.ts
|
||||
const lighthouseBundle = 'src/third_party/lighthouse-devtools-mcp-bundle.js';
|
||||
const lighthouseBundleDest = path.join(BUILD_DIR, lighthouseBundle);
|
||||
fs.mkdirSync(path.dirname(lighthouseBundleDest), {recursive: true});
|
||||
fs.copyFileSync(
|
||||
path.join(process.cwd(), lighthouseBundle),
|
||||
lighthouseBundleDest,
|
||||
);
|
||||
|
||||
copyDevToolsDescriptionFiles();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,11 @@ import {
|
||||
type ConsoleMessage,
|
||||
type HTTPRequest,
|
||||
DevTools,
|
||||
type Flags,
|
||||
type JSONSchema7Definition,
|
||||
lighthouseNavigation,
|
||||
lighthouseSnapshot,
|
||||
type RunnerResult,
|
||||
} from './third_party/index.js';
|
||||
import type {ToolGroups} from './tools/thirdPartyDeveloper.js';
|
||||
import type {
|
||||
@@ -1474,6 +1478,18 @@ export class McpPage implements ContextPage {
|
||||
};
|
||||
}
|
||||
|
||||
lighthouseNavigation(flags?: Flags): Promise<RunnerResult | undefined> {
|
||||
return lighthouseNavigation(this.pptrPage, this.pptrPage.url(), {
|
||||
flags,
|
||||
});
|
||||
}
|
||||
|
||||
lighthouseSnapshot(flags?: Flags): Promise<RunnerResult | undefined> {
|
||||
return lighthouseSnapshot(this.pptrPage, {
|
||||
flags,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to ignore favicon request as they make our test flaky
|
||||
*/
|
||||
|
||||
Vendored
+17
-15
@@ -114,24 +114,26 @@ function loadLighthouse(): Promise<typeof LighthouseBundle> {
|
||||
return lighthouseBundle;
|
||||
}
|
||||
|
||||
export const lighthouseRunner = {
|
||||
async snapshot(page: Page, options: {flags?: Flags}): Promise<RunnerResult> {
|
||||
const {snapshot} = await loadLighthouse();
|
||||
return await snapshot(page, options);
|
||||
},
|
||||
async navigation(
|
||||
page: Page,
|
||||
url: string,
|
||||
options: {flags?: Flags},
|
||||
): Promise<RunnerResult> {
|
||||
const {navigation} = await loadLighthouse();
|
||||
return await navigation(page, url, options);
|
||||
},
|
||||
};
|
||||
export async function lighthouseSnapshot(
|
||||
page: Page,
|
||||
options: {flags?: Flags},
|
||||
): Promise<RunnerResult | undefined> {
|
||||
const {snapshot} = await loadLighthouse();
|
||||
return await snapshot(page, options);
|
||||
}
|
||||
|
||||
export async function lighthouseNavigation(
|
||||
page: Page,
|
||||
url: string,
|
||||
options: {flags?: Flags},
|
||||
): Promise<RunnerResult | undefined> {
|
||||
const {navigation} = await loadLighthouse();
|
||||
return await navigation(page, url, options);
|
||||
}
|
||||
|
||||
export async function generateReport(
|
||||
lhr: Result,
|
||||
format: string,
|
||||
format: OutputMode,
|
||||
): Promise<string> {
|
||||
const lighthouse = await loadLighthouse();
|
||||
return lighthouse.generateReport(lhr, format);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Flags, OutputMode, Result, RunnerResult} from 'lighthouse';
|
||||
import type {Page} from 'puppeteer-core';
|
||||
|
||||
export declare function snapshot(
|
||||
page: Page,
|
||||
options: {flags?: Flags},
|
||||
): Promise<RunnerResult | undefined>;
|
||||
|
||||
export declare function navigation(
|
||||
page: Page,
|
||||
url: string,
|
||||
options: {flags?: Flags},
|
||||
): Promise<RunnerResult | undefined>;
|
||||
|
||||
export declare function generateReport(lhr: Result, format: OutputMode): string;
|
||||
@@ -35,8 +35,10 @@ import type {
|
||||
UninstallPWAOptions,
|
||||
Viewport,
|
||||
DevTools,
|
||||
Flags,
|
||||
Protocol,
|
||||
Page,
|
||||
RunnerResult,
|
||||
WebMCPTool,
|
||||
} from '../third_party/index.js';
|
||||
import type {InsightName, TraceResult} from '../processors/PerformanceTrace.js';
|
||||
@@ -545,6 +547,8 @@ export type ContextPage = Readonly<{
|
||||
ensureDevToolsCommentBridge(
|
||||
devtoolsPage: Page,
|
||||
): Promise<DevToolsCommentBridge | undefined>;
|
||||
lighthouseNavigation(flags?: Flags): Promise<RunnerResult | undefined>;
|
||||
lighthouseSnapshot(flags?: Flags): Promise<RunnerResult | undefined>;
|
||||
}>;
|
||||
|
||||
export function defineTool<Schema extends zod.ZodRawShape>(
|
||||
|
||||
@@ -9,7 +9,6 @@ import path from 'node:path';
|
||||
import type {ParsedArguments} from '../config/ConfigParser.js';
|
||||
|
||||
import {
|
||||
lighthouseRunner,
|
||||
generateReport,
|
||||
zod,
|
||||
type Flags,
|
||||
@@ -202,7 +201,7 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
|
||||
'best-practices',
|
||||
'agentic-browsing',
|
||||
];
|
||||
const formats = ['json', 'html'] as OutputMode[];
|
||||
const formats: OutputMode[] = ['json', 'html'];
|
||||
const {
|
||||
mode = 'navigation',
|
||||
device = 'desktop',
|
||||
@@ -241,13 +240,11 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
|
||||
let result: RunnerResult | undefined;
|
||||
try {
|
||||
if (mode === 'navigation') {
|
||||
const url = page.pptrPage.url();
|
||||
try {
|
||||
result = await lighthouseRunner.navigation(page.pptrPage, url, {
|
||||
flags,
|
||||
});
|
||||
result = await page.lighthouseNavigation(flags);
|
||||
} catch (error) {
|
||||
if (isInvalidUrlError(error)) {
|
||||
const url = page.pptrPage.url();
|
||||
throw new Error(
|
||||
`Lighthouse "navigation" mode cannot audit ${url}: it only supports http:, https:, chrome: and chrome-extension: URLs. Use mode "snapshot" to audit the page in its current state.`,
|
||||
{cause: error},
|
||||
@@ -256,9 +253,7 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
result = await lighthouseRunner.snapshot(page.pptrPage, {
|
||||
flags,
|
||||
});
|
||||
result = await page.lighthouseSnapshot(flags);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {lighthouseRunner} from '../../src/third_party/index.js';
|
||||
import {lighthouseAudit} from '../../src/tools/lighthouse.js';
|
||||
import {createHandlerMocks, createMockRunnerResult} from '../mocks.js';
|
||||
import {serverHooks} from '../server.js';
|
||||
@@ -90,7 +89,7 @@ describe('lighthouse', () => {
|
||||
const invalidUrl = Object.assign(new Error('INVALID_URL'), {
|
||||
code: 'INVALID_URL',
|
||||
});
|
||||
sinon.stub(lighthouseRunner, 'navigation').rejects(invalidUrl);
|
||||
page.lighthouseNavigation.rejects(invalidUrl);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
@@ -115,9 +114,7 @@ describe('lighthouse', () => {
|
||||
|
||||
it('passes other navigation errors through unchanged', async () => {
|
||||
const {page, context, response, args} = createHandlerMocks();
|
||||
sinon
|
||||
.stub(lighthouseRunner, 'navigation')
|
||||
.rejects(new Error('PROTOCOL_TIMEOUT'));
|
||||
page.lighthouseNavigation.rejects(new Error('PROTOCOL_TIMEOUT'));
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
@@ -139,9 +136,7 @@ describe('lighthouse', () => {
|
||||
it('restores emulation', async () => {
|
||||
const {page, context, response, args} = createHandlerMocks();
|
||||
context.saveTemporaryFile.resolves({filepath: 'report.json'});
|
||||
sinon
|
||||
.stub(lighthouseRunner, 'snapshot')
|
||||
.resolves(createMockRunnerResult());
|
||||
page.lighthouseSnapshot.resolves(createMockRunnerResult());
|
||||
|
||||
await lighthouseAudit(args).handler(
|
||||
{
|
||||
@@ -160,9 +155,7 @@ describe('lighthouse', () => {
|
||||
|
||||
it('restores emulation even when audit fails', async () => {
|
||||
const {page, context, response, args} = createHandlerMocks();
|
||||
sinon
|
||||
.stub(lighthouseRunner, 'snapshot')
|
||||
.rejects(new Error('Audit failed'));
|
||||
page.lighthouseSnapshot.rejects(new Error('Audit failed'));
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
@@ -186,7 +179,7 @@ describe('lighthouse', () => {
|
||||
it('fails when Lighthouse reports a runtime error', async () => {
|
||||
const {page, context, response, args} = createHandlerMocks();
|
||||
context.saveTemporaryFile.resolves({filepath: 'report.json'});
|
||||
sinon.stub(lighthouseRunner, 'navigation').resolves(
|
||||
page.lighthouseNavigation.resolves(
|
||||
createMockRunnerResult({
|
||||
runtimeError: {
|
||||
code: 'ERRORED_DOCUMENT_REQUEST',
|
||||
@@ -222,9 +215,7 @@ describe('lighthouse', () => {
|
||||
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());
|
||||
page.lighthouseNavigation.resolves(createMockRunnerResult());
|
||||
|
||||
await lighthouseAudit(args).handler(
|
||||
{
|
||||
@@ -238,7 +229,7 @@ describe('lighthouse', () => {
|
||||
context,
|
||||
);
|
||||
|
||||
const {flags} = navigation.firstCall.args[2];
|
||||
const [flags] = page.lighthouseNavigation.firstCall.args;
|
||||
assert.equal(flags?.formFactor, 'desktop');
|
||||
assert.match(String(flags?.emulatedUserAgent), /Macintosh/);
|
||||
assert.doesNotMatch(String(flags?.emulatedUserAgent), /Mobile/);
|
||||
@@ -247,9 +238,7 @@ describe('lighthouse', () => {
|
||||
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());
|
||||
page.lighthouseSnapshot.resolves(createMockRunnerResult());
|
||||
|
||||
await lighthouseAudit(args).handler(
|
||||
{
|
||||
@@ -263,7 +252,7 @@ describe('lighthouse', () => {
|
||||
context,
|
||||
);
|
||||
|
||||
const {flags} = snapshot.firstCall.args[1];
|
||||
const [flags] = page.lighthouseSnapshot.firstCall.args;
|
||||
assert.equal(flags?.formFactor, 'mobile');
|
||||
assert.match(String(flags?.emulatedUserAgent), /Mobile Safari/);
|
||||
});
|
||||
@@ -271,7 +260,7 @@ describe('lighthouse', () => {
|
||||
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(
|
||||
page.lighthouseSnapshot.resolves(
|
||||
createMockRunnerResult({
|
||||
mainDocumentUrl: undefined,
|
||||
finalDisplayedUrl: 'https://example.com/page',
|
||||
@@ -305,10 +294,7 @@ describe('lighthouse', () => {
|
||||
context.saveTemporaryFile
|
||||
.withArgs(sinon.match.instanceOf(Uint8Array), 'report.html')
|
||||
.resolves({filepath: '/tmp/report.html'});
|
||||
const snapshot = sinon
|
||||
.stub(lighthouseRunner, 'snapshot')
|
||||
.resolves(createMockRunnerResult());
|
||||
const navigation = sinon.stub(lighthouseRunner, 'navigation');
|
||||
page.lighthouseSnapshot.resolves(createMockRunnerResult());
|
||||
|
||||
await lighthouseAudit(args).handler(
|
||||
{
|
||||
@@ -322,8 +308,8 @@ describe('lighthouse', () => {
|
||||
context,
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(snapshot);
|
||||
sinon.assert.notCalled(navigation);
|
||||
sinon.assert.calledOnce(page.lighthouseSnapshot);
|
||||
sinon.assert.notCalled(page.lighthouseNavigation);
|
||||
sinon.assert.calledTwice(context.saveTemporaryFile);
|
||||
sinon.assert.notCalled(context.saveFile);
|
||||
sinon.assert.calledOnce(response.attachLighthouseResult);
|
||||
@@ -344,9 +330,7 @@ describe('lighthouse', () => {
|
||||
context.saveFile
|
||||
.withArgs(sinon.match.instanceOf(Uint8Array), reportPath, '.html')
|
||||
.resolves({filename: `${reportPath}.html`});
|
||||
sinon
|
||||
.stub(lighthouseRunner, 'snapshot')
|
||||
.resolves(createMockRunnerResult());
|
||||
page.lighthouseSnapshot.resolves(createMockRunnerResult());
|
||||
|
||||
await lighthouseAudit(args).handler(
|
||||
{
|
||||
@@ -389,7 +373,7 @@ describe('lighthouse', () => {
|
||||
snippet: `<div class="item-${id}">`,
|
||||
nodeLabel: `Item ${id}`,
|
||||
});
|
||||
sinon.stub(lighthouseRunner, 'snapshot').resolves(
|
||||
page.lighthouseSnapshot.resolves(
|
||||
createMockRunnerResult({
|
||||
categories: {
|
||||
accessibility: {
|
||||
|
||||
Reference in new issue
Block a user