lighthouse 13.1.0 → 13.3.0
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/cli/bin.js +5 -0
- package/cli/cli-flags.js +2 -2
- package/cli/test/smokehouse/lighthouse-runners/bundle.js +7 -3
- package/cli/test/smokehouse/lighthouse-runners/cli.js +5 -1
- package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.js +7 -3
- package/cli/test/smokehouse/lighthouse-runners/devtools.js +4 -1
- package/cli/test/smokehouse/smokehouse.js +7 -2
- package/core/audits/accessibility/autocomplete-valid.d.ts +10 -0
- package/core/audits/accessibility/autocomplete-valid.js +44 -0
- package/core/audits/accessibility/presentation-role-conflict.d.ts +10 -0
- package/core/audits/accessibility/presentation-role-conflict.js +46 -0
- package/core/audits/accessibility/svg-img-alt.d.ts +10 -0
- package/core/audits/accessibility/svg-img-alt.js +44 -0
- package/core/audits/agentic/agent-accessibility-tree.d.ts +19 -0
- package/core/audits/agentic/agent-accessibility-tree.js +115 -0
- package/core/audits/agentic/llms-txt.d.ts +20 -0
- package/core/audits/agentic/llms-txt.js +111 -0
- package/core/audits/insights/insight-audit.d.ts +2 -2
- package/core/audits/insights/insight-audit.js +16 -6
- package/core/audits/layout-shifts.js +1 -1
- package/core/audits/server-response-time.js +3 -3
- package/core/audits/webmcp-form-coverage.d.ts +16 -0
- package/core/audits/webmcp-form-coverage.js +90 -0
- package/core/audits/webmcp-registered-tools.d.ts +21 -0
- package/core/audits/webmcp-registered-tools.js +149 -0
- package/core/audits/webmcp-schema-validity.d.ts +22 -0
- package/core/audits/webmcp-schema-validity.js +141 -0
- package/core/computed/document-urls.js +0 -1
- package/core/computed/main-resource.js +0 -2
- package/core/computed/metrics/lantern-metric.js +4 -4
- package/core/computed/metrics/lcp-breakdown.js +1 -1
- package/core/computed/metrics/time-to-first-byte.js +1 -1
- package/core/computed/navigation-insights.js +2 -1
- package/core/computed/network-analysis.js +0 -1
- package/core/config/agentic-browsing-config.d.ts +12 -0
- package/core/config/agentic-browsing-config.js +73 -0
- package/core/config/default-config.js +51 -0
- package/core/gather/gatherers/accessibility.js +5 -1
- package/core/gather/gatherers/agentic/llms-txt.d.ts +10 -0
- package/core/gather/gatherers/agentic/llms-txt.js +28 -0
- package/core/gather/gatherers/inputs.js +2 -0
- package/core/gather/gatherers/meta-elements.js +1 -1
- package/core/gather/gatherers/trace-elements.js +1 -1
- package/core/gather/gatherers/webmcp-schema.d.ts +25 -0
- package/core/gather/gatherers/webmcp-schema.js +105 -0
- package/core/gather/gatherers/webmcp.d.ts +58 -0
- package/core/gather/gatherers/webmcp.js +159 -0
- package/core/lib/baseline/web-features-metadata.json +1 -1
- package/core/lib/cdt/generated/SourceMap.js +2 -2
- package/core/lib/deprecations-strings.d.ts +26 -20
- package/core/lib/deprecations-strings.js +7 -0
- package/core/lib/navigation-error.js +0 -6
- package/core/lib/network-request.js +0 -1
- package/core/lib/page-functions.d.ts +3 -3
- package/core/lib/page-functions.js +11 -4
- package/core/lib/tracehouse/trace-processor.d.ts +5 -4
- package/core/lib/tracehouse/trace-processor.js +85 -19
- package/core/runner.js +3 -0
- package/core/scoring.d.ts +1 -0
- package/dist/report/bundle.esm.js +1 -1
- package/dist/report/flow.js +3 -3
- package/dist/report/standalone.js +1 -1
- package/flow-report/src/summary/category.tsx +1 -1
- package/package.json +11 -11
- package/report/renderer/category-renderer.js +1 -1
- package/report/renderer/report-utils.d.ts +2 -1
- package/report/renderer/report-utils.js +7 -2
- package/shared/localization/locales/ar-XB.json +72 -36
- package/shared/localization/locales/ar.json +72 -36
- package/shared/localization/locales/bg.json +72 -36
- package/shared/localization/locales/ca.json +72 -36
- package/shared/localization/locales/cs.json +72 -36
- package/shared/localization/locales/da.json +74 -38
- package/shared/localization/locales/de.json +72 -36
- package/shared/localization/locales/el.json +73 -37
- package/shared/localization/locales/en-GB.json +74 -38
- package/shared/localization/locales/en-US.json +263 -17
- package/shared/localization/locales/en-XL.json +263 -17
- package/shared/localization/locales/es-419.json +72 -36
- package/shared/localization/locales/es.json +73 -37
- package/shared/localization/locales/fi.json +72 -36
- package/shared/localization/locales/fil.json +74 -38
- package/shared/localization/locales/fr.json +162 -126
- package/shared/localization/locales/he.json +74 -38
- package/shared/localization/locales/hi.json +73 -37
- package/shared/localization/locales/hr.json +72 -36
- package/shared/localization/locales/hu.json +73 -37
- package/shared/localization/locales/id.json +74 -38
- package/shared/localization/locales/it.json +72 -36
- package/shared/localization/locales/ja.json +72 -36
- package/shared/localization/locales/ko.json +72 -36
- package/shared/localization/locales/lt.json +72 -36
- package/shared/localization/locales/lv.json +72 -36
- package/shared/localization/locales/nl.json +73 -37
- package/shared/localization/locales/no.json +72 -36
- package/shared/localization/locales/pl.json +72 -36
- package/shared/localization/locales/pt-PT.json +72 -36
- package/shared/localization/locales/pt.json +74 -38
- package/shared/localization/locales/ro.json +72 -36
- package/shared/localization/locales/ru.json +72 -36
- package/shared/localization/locales/sk.json +72 -36
- package/shared/localization/locales/sl.json +72 -36
- package/shared/localization/locales/sr-Latn.json +73 -37
- package/shared/localization/locales/sr.json +73 -37
- package/shared/localization/locales/sv.json +75 -39
- package/shared/localization/locales/ta.json +73 -37
- package/shared/localization/locales/te.json +72 -36
- package/shared/localization/locales/th.json +73 -37
- package/shared/localization/locales/tr.json +72 -36
- package/shared/localization/locales/uk.json +72 -36
- package/shared/localization/locales/vi.json +74 -38
- package/shared/localization/locales/zh-HK.json +72 -36
- package/shared/localization/locales/zh-TW.json +74 -38
- package/shared/localization/locales/zh.json +75 -39
- package/types/artifacts.d.ts +33 -0
- package/types/config.d.ts +1 -0
- package/types/internal/smokehouse.d.ts +7 -1
- package/types/lhr/lhr.d.ts +11 -0
|
@@ -100,6 +100,21 @@ const UIStrings = {
|
|
|
100
100
|
bestPracticesBrowserCompatGroupTitle: 'Browser Compatibility',
|
|
101
101
|
/** Title of the General group of the Best Practices category. Within this section are the audits that don't belong to a specific group but are of general interest. */
|
|
102
102
|
bestPracticesGeneralGroupTitle: 'General',
|
|
103
|
+
/** Title of the Agentic Browsing category of audits. */
|
|
104
|
+
agenticBrowsingCategoryTitle: 'Agentic Browsing',
|
|
105
|
+
/** Description of the Agentic Browsing category. */
|
|
106
|
+
agenticBrowsingCategoryDescription: 'These checks ensure high-quality, [browsable websites for AI agents](https://goo.gle/lighthouse-agentic-web) ' +
|
|
107
|
+
'and validate the correctness of WebMCP integrations. ' +
|
|
108
|
+
'This category is still under development and subject to change.',
|
|
109
|
+
/** Title of the WebMCP group of audits. */
|
|
110
|
+
webmcpGroupTitle: 'WebMCP',
|
|
111
|
+
/** Description of the WebMCP group. */
|
|
112
|
+
webmcpGroupDescription: 'Audits validating WebMCP integration.',
|
|
113
|
+
/** Title of the Agent Accessibility group of audits. */
|
|
114
|
+
agentAccessibilityGroupTitle: 'Agent Accessibility',
|
|
115
|
+
/** Description of the Agent Accessibility group of audits. */
|
|
116
|
+
agentAccessibilityGroupDescription: 'These audits highlight best practices for improving the ' +
|
|
117
|
+
'accessibility of the website for AI agents.',
|
|
103
118
|
};
|
|
104
119
|
|
|
105
120
|
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
@@ -108,6 +123,9 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
|
108
123
|
const defaultConfig = {
|
|
109
124
|
settings: constants.defaultSettings,
|
|
110
125
|
artifacts: [
|
|
126
|
+
{id: 'WebMCP', gatherer: 'webmcp'},
|
|
127
|
+
{id: 'WebMcpSchemaIssues', gatherer: 'webmcp-schema'},
|
|
128
|
+
{id: 'LlmsTxt', gatherer: 'agentic/llms-txt'},
|
|
111
129
|
// Artifacts which can be depended on come first.
|
|
112
130
|
{id: 'DevtoolsLog', gatherer: 'devtools-log'},
|
|
113
131
|
{id: 'Trace', gatherer: 'trace'},
|
|
@@ -254,6 +272,9 @@ const defaultConfig = {
|
|
|
254
272
|
'accessibility/manual/offscreen-content-hidden',
|
|
255
273
|
'accessibility/manual/use-landmarks',
|
|
256
274
|
'accessibility/manual/visual-order-follows-dom',
|
|
275
|
+
'accessibility/autocomplete-valid',
|
|
276
|
+
'accessibility/presentation-role-conflict',
|
|
277
|
+
'accessibility/svg-img-alt',
|
|
257
278
|
'byte-efficiency/total-byte-weight',
|
|
258
279
|
'byte-efficiency/unminified-css',
|
|
259
280
|
'byte-efficiency/unminified-javascript',
|
|
@@ -276,6 +297,11 @@ const defaultConfig = {
|
|
|
276
297
|
'seo/hreflang',
|
|
277
298
|
'seo/canonical',
|
|
278
299
|
'seo/manual/structured-data',
|
|
300
|
+
'agentic/agent-accessibility-tree',
|
|
301
|
+
'webmcp-registered-tools',
|
|
302
|
+
'webmcp-form-coverage',
|
|
303
|
+
'webmcp-schema-validity',
|
|
304
|
+
'agentic/llms-txt',
|
|
279
305
|
'bf-cache',
|
|
280
306
|
'insights/cache-insight',
|
|
281
307
|
'insights/cls-culprits-insight',
|
|
@@ -363,6 +389,14 @@ const defaultConfig = {
|
|
|
363
389
|
'best-practices-general': {
|
|
364
390
|
title: str_(UIStrings.bestPracticesGeneralGroupTitle),
|
|
365
391
|
},
|
|
392
|
+
'webmcp': {
|
|
393
|
+
title: str_(UIStrings.webmcpGroupTitle),
|
|
394
|
+
description: str_(UIStrings.webmcpGroupDescription),
|
|
395
|
+
},
|
|
396
|
+
'agent-accessibility': {
|
|
397
|
+
title: str_(UIStrings.agentAccessibilityGroupTitle),
|
|
398
|
+
description: str_(UIStrings.agentAccessibilityGroupDescription),
|
|
399
|
+
},
|
|
366
400
|
// Group for audits that should not be displayed.
|
|
367
401
|
'hidden': {title: ''},
|
|
368
402
|
},
|
|
@@ -515,6 +549,9 @@ const defaultConfig = {
|
|
|
515
549
|
{id: 'valid-lang', weight: 7, group: 'a11y-language'}, // Serious, wcag2aa
|
|
516
550
|
{id: 'video-caption', weight: 10, group: 'a11y-audio-video'}, // Critical, wcag2a
|
|
517
551
|
{id: 'landmark-one-main', weight: 3, group: 'a11y-best-practices'}, // Moderate, best-practice
|
|
552
|
+
{id: 'autocomplete-valid', weight: 1, group: 'a11y-best-practices'}, // Informational
|
|
553
|
+
{id: 'presentation-role-conflict', weight: 1, group: 'a11y-best-practices'}, // Informational
|
|
554
|
+
{id: 'svg-img-alt', weight: 1, group: 'a11y-best-practices'}, // Informational
|
|
518
555
|
// Manual audits
|
|
519
556
|
{id: 'focusable-controls', weight: 0},
|
|
520
557
|
{id: 'interactive-element-affordance', weight: 0},
|
|
@@ -595,6 +632,20 @@ const defaultConfig = {
|
|
|
595
632
|
{id: 'structured-data', weight: 0},
|
|
596
633
|
],
|
|
597
634
|
},
|
|
635
|
+
'agentic-browsing': {
|
|
636
|
+
title: str_(UIStrings.agenticBrowsingCategoryTitle),
|
|
637
|
+
description: str_(UIStrings.agenticBrowsingCategoryDescription),
|
|
638
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
639
|
+
categoryScoreDisplayMode: 'fraction',
|
|
640
|
+
auditRefs: [
|
|
641
|
+
{id: 'agent-accessibility-tree', weight: 1, group: 'agent-accessibility'},
|
|
642
|
+
{id: 'webmcp-form-coverage', weight: 1, group: 'webmcp'},
|
|
643
|
+
{id: 'webmcp-registered-tools', weight: 1, group: 'webmcp'},
|
|
644
|
+
{id: 'webmcp-schema-validity', weight: 1, group: 'webmcp'},
|
|
645
|
+
{id: 'cumulative-layout-shift', weight: 1, acronym: 'CLS'},
|
|
646
|
+
{id: 'llms-txt', weight: 1, group: 'agent-accessibility'},
|
|
647
|
+
],
|
|
648
|
+
},
|
|
598
649
|
},
|
|
599
650
|
};
|
|
600
651
|
|
|
@@ -50,6 +50,7 @@ async function runA11yChecks() {
|
|
|
50
50
|
'aria-roledescription': {enabled: false},
|
|
51
51
|
'aria-treeitem-name': {enabled: true},
|
|
52
52
|
'aria-text': {enabled: true},
|
|
53
|
+
'autocomplete-valid': {enabled: true},
|
|
53
54
|
'audio-caption': {enabled: false},
|
|
54
55
|
'blink': {enabled: false},
|
|
55
56
|
'duplicate-id': {enabled: false},
|
|
@@ -69,6 +70,7 @@ async function runA11yChecks() {
|
|
|
69
70
|
// https://github.com/dequelabs/axe-core/issues/2958
|
|
70
71
|
'nested-interactive': {enabled: false},
|
|
71
72
|
'no-autoplay-audio': {enabled: false},
|
|
73
|
+
'presentation-role-conflict': {enabled: true},
|
|
72
74
|
'role-img-alt': {enabled: false},
|
|
73
75
|
'scrollable-region-focusable': {enabled: false},
|
|
74
76
|
'select-name': {enabled: true},
|
|
@@ -76,7 +78,7 @@ async function runA11yChecks() {
|
|
|
76
78
|
'skip-link': {enabled: true},
|
|
77
79
|
// https://github.com/GoogleChrome/lighthouse/issues/16163
|
|
78
80
|
'summary-name': {enabled: false},
|
|
79
|
-
'svg-img-alt': {enabled:
|
|
81
|
+
'svg-img-alt': {enabled: true},
|
|
80
82
|
'tabindex': {enabled: true},
|
|
81
83
|
'table-duplicate-name': {enabled: true},
|
|
82
84
|
'table-fake-caption': {enabled: true},
|
|
@@ -176,6 +178,8 @@ function createAxeRuleResultArtifact(result) {
|
|
|
176
178
|
tags: result.tags,
|
|
177
179
|
nodes,
|
|
178
180
|
error,
|
|
181
|
+
help: result.help,
|
|
182
|
+
description: result.description,
|
|
179
183
|
};
|
|
180
184
|
}
|
|
181
185
|
/* c8 ignore stop */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default LlmsTxt;
|
|
2
|
+
declare class LlmsTxt extends BaseGatherer {
|
|
3
|
+
/**
|
|
4
|
+
* @param {LH.Gatherer.Context} passContext
|
|
5
|
+
* @return {Promise<LH.Artifacts['LlmsTxt']>}
|
|
6
|
+
*/
|
|
7
|
+
getArtifact(passContext: LH.Gatherer.Context): Promise<LH.Artifacts["LlmsTxt"]>;
|
|
8
|
+
}
|
|
9
|
+
import BaseGatherer from '../../base-gatherer.js';
|
|
10
|
+
//# sourceMappingURL=llms-txt.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import BaseGatherer from '../../base-gatherer.js';
|
|
9
|
+
|
|
10
|
+
class LlmsTxt extends BaseGatherer {
|
|
11
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
12
|
+
meta = {
|
|
13
|
+
supportedModes: ['snapshot', 'navigation'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {LH.Gatherer.Context} passContext
|
|
18
|
+
* @return {Promise<LH.Artifacts['LlmsTxt']>}
|
|
19
|
+
*/
|
|
20
|
+
async getArtifact(passContext) {
|
|
21
|
+
const {finalDisplayedUrl} = passContext.baseArtifacts.URL;
|
|
22
|
+
const llmUrl = new URL('/llms.txt', finalDisplayedUrl).href;
|
|
23
|
+
return passContext.driver.fetcher.fetchResource(llmUrl)
|
|
24
|
+
.catch(err => ({status: null, content: null, errorMessage: err.message}));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default LlmsTxt;
|
|
@@ -33,6 +33,8 @@ function collectElements() {
|
|
|
33
33
|
autocomplete: formEl.autocomplete,
|
|
34
34
|
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
35
35
|
node: getNodeDetails(formEl),
|
|
36
|
+
webMcpToolname: formEl.getAttribute('toolname'),
|
|
37
|
+
webMcpTooldescription: formEl.getAttribute('tooldescription'),
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -32,7 +32,7 @@ function collectMetaElements() {
|
|
|
32
32
|
property: getAttribute('property'),
|
|
33
33
|
httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
|
|
34
34
|
charset: getAttribute('charset'),
|
|
35
|
-
node: functions.getNodeDetails(meta),
|
|
35
|
+
node: /** @type {LH.Artifacts.NodeDetails} */ (functions.getNodeDetails(meta)),
|
|
36
36
|
};
|
|
37
37
|
});
|
|
38
38
|
}
|
|
@@ -142,7 +142,7 @@ class TraceElements extends BaseGatherer {
|
|
|
142
142
|
}, new Set());
|
|
143
143
|
|
|
144
144
|
// TODO: handle digging into Map in recursiveObjectEnumerate.
|
|
145
|
-
for (const shift of insightSet.model.CLSCulprits
|
|
145
|
+
for (const shift of insightSet.model.CLSCulprits?.shifts.values() ?? []) {
|
|
146
146
|
nodeIds.push(...shift.unsizedImages.map(s => s.backendNodeId));
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default WebMcpSchemaIssues;
|
|
2
|
+
declare class WebMcpSchemaIssues extends BaseGatherer {
|
|
3
|
+
/** @type {LH.Artifacts.WebMcpSchemaIssue[]} */
|
|
4
|
+
_issues: LH.Artifacts.WebMcpSchemaIssue[];
|
|
5
|
+
_onIssueAdded: (event: Record<string, any>) => void;
|
|
6
|
+
/**
|
|
7
|
+
* @param {Record<string, any>} event
|
|
8
|
+
*/
|
|
9
|
+
onIssueAdded(event: Record<string, any>): void;
|
|
10
|
+
/**
|
|
11
|
+
* @param {LH.Gatherer.Context} passContext
|
|
12
|
+
*/
|
|
13
|
+
startInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* @param {LH.Gatherer.Context} passContext
|
|
16
|
+
*/
|
|
17
|
+
stopInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* @param {LH.Gatherer.Context} context
|
|
20
|
+
* @return {Promise<LH.Artifacts.WebMcpSchemaIssue[]>}
|
|
21
|
+
*/
|
|
22
|
+
getArtifact(context: LH.Gatherer.Context): Promise<LH.Artifacts.WebMcpSchemaIssue[]>;
|
|
23
|
+
}
|
|
24
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
25
|
+
//# sourceMappingURL=webmcp-schema.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
8
|
+
import {resolveNodeIdToObjectId} from '../driver/dom.js';
|
|
9
|
+
import {pageFunctions} from '../../lib/page-functions.js';
|
|
10
|
+
import {ExecutionContext} from '../driver/execution-context.js';
|
|
11
|
+
|
|
12
|
+
class WebMcpSchemaIssues extends BaseGatherer {
|
|
13
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
14
|
+
meta = {
|
|
15
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
/** @type {LH.Artifacts.WebMcpSchemaIssue[]} */
|
|
21
|
+
this._issues = [];
|
|
22
|
+
this._onIssueAdded = this.onIssueAdded.bind(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Record<string, any>} event
|
|
27
|
+
*/
|
|
28
|
+
onIssueAdded(event) {
|
|
29
|
+
const issue = event.issue;
|
|
30
|
+
if (!issue || issue.code !== 'GenericIssue') return;
|
|
31
|
+
|
|
32
|
+
const details = issue.details?.genericIssueDetails;
|
|
33
|
+
if (!details) return;
|
|
34
|
+
|
|
35
|
+
const errorType = details.errorType;
|
|
36
|
+
if (errorType && (
|
|
37
|
+
errorType === 'FormModelContextMissingToolName' ||
|
|
38
|
+
errorType === 'FormModelContextMissingToolDescription' ||
|
|
39
|
+
errorType === 'FormModelContextRequiredParameterMissingName' ||
|
|
40
|
+
errorType === 'FormModelContextParameterMissingTitleAndDescription' ||
|
|
41
|
+
errorType === 'FormModelContextParameterMissingName'
|
|
42
|
+
)) {
|
|
43
|
+
this._issues.push(details);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {LH.Gatherer.Context} passContext
|
|
49
|
+
*/
|
|
50
|
+
async startInstrumentation(passContext) {
|
|
51
|
+
const session = passContext.driver.defaultSession;
|
|
52
|
+
session.on('Audits.issueAdded', this._onIssueAdded);
|
|
53
|
+
await session.sendCommand('Audits.enable');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {LH.Gatherer.Context} passContext
|
|
58
|
+
*/
|
|
59
|
+
async stopInstrumentation(passContext) {
|
|
60
|
+
const session = passContext.driver.defaultSession;
|
|
61
|
+
session.off('Audits.issueAdded', this._onIssueAdded);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {LH.Gatherer.Context} context
|
|
66
|
+
* @return {Promise<LH.Artifacts.WebMcpSchemaIssue[]>}
|
|
67
|
+
*/
|
|
68
|
+
async getArtifact(context) {
|
|
69
|
+
const session = context.driver.defaultSession;
|
|
70
|
+
|
|
71
|
+
const deps = ExecutionContext.serializeDeps([
|
|
72
|
+
pageFunctions.getNodeDetails,
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const promises = this._issues.map(async (issue) => {
|
|
76
|
+
const processedIssue = {...issue};
|
|
77
|
+
if (issue.violatingNodeId) {
|
|
78
|
+
try {
|
|
79
|
+
const objectId = await resolveNodeIdToObjectId(session, issue.violatingNodeId);
|
|
80
|
+
if (objectId) {
|
|
81
|
+
const response = await session.sendCommand('Runtime.callFunctionOn', {
|
|
82
|
+
objectId,
|
|
83
|
+
functionDeclaration: `function () {
|
|
84
|
+
${deps}
|
|
85
|
+
return getNodeDetails(this);
|
|
86
|
+
}`,
|
|
87
|
+
returnByValue: true,
|
|
88
|
+
awaitPromise: true,
|
|
89
|
+
});
|
|
90
|
+
if (response && response.result && response.result.value) {
|
|
91
|
+
processedIssue.nodeDetails = response.result.value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// Ignore error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return processedIssue;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return Promise.all(promises);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default WebMcpSchemaIssues;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export default WebMCP;
|
|
2
|
+
export type WebMCPTool = {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, any>;
|
|
6
|
+
frameId: string;
|
|
7
|
+
backendNodeId?: number | undefined;
|
|
8
|
+
stackTrace?: any;
|
|
9
|
+
nodeDetails?: import("../../index.js").Artifacts.NodeDetails | undefined;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} WebMCPTool
|
|
13
|
+
* @property {string} name
|
|
14
|
+
* @property {string} description
|
|
15
|
+
* @property {Record<string, any>} inputSchema
|
|
16
|
+
* @property {string} frameId
|
|
17
|
+
* @property {number} [backendNodeId]
|
|
18
|
+
* @property {any} [stackTrace]
|
|
19
|
+
* @property {LH.Artifacts.NodeDetails} [nodeDetails]
|
|
20
|
+
*/
|
|
21
|
+
declare class WebMCP extends BaseGatherer {
|
|
22
|
+
/** @type {WebMCPTool[]} */
|
|
23
|
+
_tools: WebMCPTool[];
|
|
24
|
+
_isSupported: boolean;
|
|
25
|
+
_onToolsAdded: (event: {
|
|
26
|
+
tools: WebMCPTool[];
|
|
27
|
+
}) => void;
|
|
28
|
+
_onToolsRemoved: (event: {
|
|
29
|
+
tools: WebMCPTool[];
|
|
30
|
+
}) => void;
|
|
31
|
+
/**
|
|
32
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
33
|
+
*/
|
|
34
|
+
onToolsAdded(event: {
|
|
35
|
+
tools: WebMCPTool[];
|
|
36
|
+
}): void;
|
|
37
|
+
/**
|
|
38
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
39
|
+
*/
|
|
40
|
+
onToolsRemoved(event: {
|
|
41
|
+
tools: WebMCPTool[];
|
|
42
|
+
}): void;
|
|
43
|
+
/**
|
|
44
|
+
* @param {LH.Gatherer.Context} passContext
|
|
45
|
+
*/
|
|
46
|
+
startInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* @param {LH.Gatherer.Context} passContext
|
|
49
|
+
*/
|
|
50
|
+
stopInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* @param {LH.Gatherer.Context} context
|
|
53
|
+
* @return {Promise<LH.Artifacts['WebMCP']>}
|
|
54
|
+
*/
|
|
55
|
+
getArtifact(context: LH.Gatherer.Context): Promise<LH.Artifacts["WebMCP"]>;
|
|
56
|
+
}
|
|
57
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
58
|
+
//# sourceMappingURL=webmcp.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Capture WebMCP data
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
12
|
+
import {resolveNodeIdToObjectId} from '../driver/dom.js';
|
|
13
|
+
import {pageFunctions} from '../../lib/page-functions.js';
|
|
14
|
+
import {ExecutionContext} from '../driver/execution-context.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} WebMCPTool
|
|
18
|
+
* @property {string} name
|
|
19
|
+
* @property {string} description
|
|
20
|
+
* @property {Record<string, any>} inputSchema
|
|
21
|
+
* @property {string} frameId
|
|
22
|
+
* @property {number} [backendNodeId]
|
|
23
|
+
* @property {any} [stackTrace]
|
|
24
|
+
* @property {LH.Artifacts.NodeDetails} [nodeDetails]
|
|
25
|
+
*/
|
|
26
|
+
class WebMCP extends BaseGatherer {
|
|
27
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
28
|
+
meta = {
|
|
29
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
/** @type {WebMCPTool[]} */
|
|
35
|
+
this._tools = [];
|
|
36
|
+
this._isSupported = true;
|
|
37
|
+
this._onToolsAdded = this.onToolsAdded.bind(this);
|
|
38
|
+
this._onToolsRemoved = this.onToolsRemoved.bind(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
43
|
+
*/
|
|
44
|
+
// TODO: Handle WebMCP tools per frame.
|
|
45
|
+
onToolsAdded(event) {
|
|
46
|
+
// Note that as of M148, there is a bug in WebMCP CDP.
|
|
47
|
+
// While WebMCP is enabled, any newly registered tool will
|
|
48
|
+
// have an empty schema.
|
|
49
|
+
if (event.tools) {
|
|
50
|
+
this._tools.push(...event.tools);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
56
|
+
*/
|
|
57
|
+
onToolsRemoved(event) {
|
|
58
|
+
if (event.tools) {
|
|
59
|
+
const removedNames = new Set(event.tools.map(t => t.name));
|
|
60
|
+
this._tools = this._tools.filter(t => !removedNames.has(t.name));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {LH.Gatherer.Context} passContext
|
|
66
|
+
*/
|
|
67
|
+
async startInstrumentation(passContext) {
|
|
68
|
+
const session = passContext.driver.defaultSession;
|
|
69
|
+
|
|
70
|
+
// @ts-expect-error - WebMCP domain might not be in types yet.
|
|
71
|
+
session.on('WebMCP.toolsAdded', this._onToolsAdded);
|
|
72
|
+
// @ts-expect-error
|
|
73
|
+
session.on('WebMCP.toolsRemoved', this._onToolsRemoved);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await session.sendCommand('WebMCP.enable');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.message.includes('\'WebMCP.enable\' wasn\'t found')) {
|
|
79
|
+
this._isSupported = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {LH.Gatherer.Context} passContext
|
|
88
|
+
*/
|
|
89
|
+
async stopInstrumentation(passContext) {
|
|
90
|
+
const session = passContext.driver.defaultSession;
|
|
91
|
+
// @ts-expect-error
|
|
92
|
+
session.off('WebMCP.toolsAdded', this._onToolsAdded);
|
|
93
|
+
// @ts-expect-error
|
|
94
|
+
session.off('WebMCP.toolsRemoved', this._onToolsRemoved);
|
|
95
|
+
try {
|
|
96
|
+
await session.sendCommand('WebMCP.disable');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// Ignore errors
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {LH.Gatherer.Context} context
|
|
104
|
+
* @return {Promise<LH.Artifacts['WebMCP']>}
|
|
105
|
+
*/
|
|
106
|
+
async getArtifact(context) {
|
|
107
|
+
const isSupported = await context.driver.executionContext.evaluate(
|
|
108
|
+
// @ts-expect-error - modelContext is not in types
|
|
109
|
+
() => typeof navigator.modelContext !== 'undefined',
|
|
110
|
+
{args: [], useIsolation: true}
|
|
111
|
+
);
|
|
112
|
+
if (!isSupported || !this._isSupported) {
|
|
113
|
+
return {isSupported: false, tools: []};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const session = context.driver.defaultSession;
|
|
117
|
+
|
|
118
|
+
// Remove duplicates based on name, keeping the latest occurrence.
|
|
119
|
+
const toolMap = new Map();
|
|
120
|
+
for (const tool of this._tools) {
|
|
121
|
+
toolMap.set(tool.name, tool);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const resolvedTools = [];
|
|
125
|
+
for (const tool of toolMap.values()) {
|
|
126
|
+
if (tool.backendNodeId) {
|
|
127
|
+
try {
|
|
128
|
+
const objectId = await resolveNodeIdToObjectId(session, tool.backendNodeId);
|
|
129
|
+
if (objectId) {
|
|
130
|
+
const deps = ExecutionContext.serializeDeps([
|
|
131
|
+
pageFunctions.getNodeDetails,
|
|
132
|
+
]);
|
|
133
|
+
const response = await session.sendCommand('Runtime.callFunctionOn', {
|
|
134
|
+
objectId,
|
|
135
|
+
functionDeclaration: `function () {
|
|
136
|
+
${deps}
|
|
137
|
+
return getNodeDetails(this);
|
|
138
|
+
}`,
|
|
139
|
+
returnByValue: true,
|
|
140
|
+
awaitPromise: true,
|
|
141
|
+
});
|
|
142
|
+
if (response && response.result && response.result.value) {
|
|
143
|
+
tool.nodeDetails = response.result.value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Ignore error
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
resolvedTools.push(tool);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
isSupported: true,
|
|
154
|
+
tools: resolvedTools,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default WebMCP;
|
|
@@ -381,7 +381,7 @@ class SourceMap {
|
|
|
381
381
|
let nameIndex = 0;
|
|
382
382
|
const names = map.names ?? [];
|
|
383
383
|
const tokenIter = new TokenIterator(map.mappings);
|
|
384
|
-
let sourceURL = this.#sourceInfos[sourceIndex]
|
|
384
|
+
let sourceURL = this.#sourceInfos[sourceIndex]?.sourceURL;
|
|
385
385
|
while (true) {
|
|
386
386
|
if (tokenIter.peek() === ',') {
|
|
387
387
|
tokenIter.next();
|
|
@@ -404,7 +404,7 @@ class SourceMap {
|
|
|
404
404
|
const sourceIndexDelta = tokenIter.nextVLQ();
|
|
405
405
|
if (sourceIndexDelta) {
|
|
406
406
|
sourceIndex += sourceIndexDelta;
|
|
407
|
-
sourceURL = this.#sourceInfos[sourceIndex]
|
|
407
|
+
sourceURL = this.#sourceInfos[sourceIndex]?.sourceURL;
|
|
408
408
|
}
|
|
409
409
|
sourceLineNumber += tokenIter.nextVLQ();
|
|
410
410
|
sourceColumnNumber += tokenIter.nextVLQ();
|