lighthouse 13.0.3 → 13.2.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/CONTRIBUTING.md +1 -1
- package/cli/bin.js +5 -0
- package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +10 -10
- package/cli/test/smokehouse/config/exclusions.js +44 -0
- package/cli/test/smokehouse/frontends/smokehouse-bin.js +5 -4
- package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.d.ts +14 -0
- package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.js +141 -0
- 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/baseline.d.ts +25 -0
- package/core/audits/baseline.js +190 -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 +4 -2
- package/core/computed/main-resource.js +5 -3
- 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 +3 -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 +8 -0
- package/core/gather/driver/wait-for-condition.js +11 -1
- 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/trace.js +3 -0
- 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/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/lib/baseline/web-features-metadata.json +3 -0
- package/core/lib/cdt/generated/SourceMap.js +2 -2
- package/core/lib/deprecations-strings.d.ts +169 -89
- package/core/lib/deprecations-strings.js +119 -24
- package/core/lib/navigation-error.js +5 -2
- package/core/lib/network-recorder.js +2 -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 +25 -0
- package/dist/report/bundle.esm.js +31 -3
- package/dist/report/flow.js +32 -4
- package/dist/report/standalone.js +32 -4
- package/flow-report/src/summary/category.tsx +1 -1
- package/package.json +12 -11
- package/report/assets/styles.css +28 -0
- package/report/renderer/category-renderer.js +1 -1
- package/report/renderer/components.js +1 -1
- package/report/renderer/details-renderer.d.ts +5 -0
- package/report/renderer/details-renderer.js +16 -0
- package/report/renderer/report-utils.d.ts +2 -1
- package/report/renderer/report-utils.js +7 -2
- package/report/types/report-renderer.d.ts +1 -1
- package/report/types/report-result.d.ts +1 -1
- 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 +257 -17
- package/shared/localization/locales/en-XL.json +257 -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/tsconfig.json +2 -0
- package/types/artifacts.d.ts +66 -30
- package/types/audit.d.ts +1 -1
- package/types/config.d.ts +2 -1
- package/types/gatherer.d.ts +1 -1
- package/types/lhr/audit-details.d.ts +10 -4
- package/types/lhr/flow-result.d.ts +1 -1
- package/types/lhr/lhr.d.ts +12 -1
- package/types/lhr/treemap.d.ts +1 -1
- package/types/protocol.d.ts +1 -1
- package/types/puppeteer.d.ts +1 -1
- package/types/user-flow.d.ts +1 -1
- package/types/utility-types.d.ts +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Audit that lists registered WebMCP tools.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {Audit} from './audit.js';
|
|
12
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
13
|
+
|
|
14
|
+
const UIStrings = {
|
|
15
|
+
/** Title of a Lighthouse audit that lists registered WebMCP tools. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */
|
|
16
|
+
title: 'WebMCP tools registered',
|
|
17
|
+
/** Description of a Lighthouse audit that lists registered WebMCP tools. This is displayed after a user expands the section to see more. No character length limits. "WebMCP" stands for "Web Model Context Protocol", neither should be translated. */
|
|
18
|
+
description: 'Lists the WebMCP tools registered at the time of analysis. ',
|
|
19
|
+
/** Label for a column in a data table; entries will be the name of a WebMCP tool. */
|
|
20
|
+
columnTool: 'Tool name',
|
|
21
|
+
/** Label for a column in a data table; entries will be the description of a WebMCP tool. */
|
|
22
|
+
columnDescription: 'Description',
|
|
23
|
+
/** Label for a column in a data table; entries will be the source location where an imperative WebMCP tool was registered. */
|
|
24
|
+
columnImperativeLocation: 'Source Location',
|
|
25
|
+
/** Label for a column in a data table; entries will be the DOM element associated with a declarative WebMCP tool. */
|
|
26
|
+
columnDeclarativeElement: 'Element',
|
|
27
|
+
/** Label for a column in a data table; entries will be the input schema of a WebMCP tool. */
|
|
28
|
+
columnInputSchema: 'Input schema',
|
|
29
|
+
/** Title for the table listing imperative WebMCP tools. */
|
|
30
|
+
titleImperativeTools: 'Imperative Tools',
|
|
31
|
+
/** Title for the table listing declarative WebMCP tools. */
|
|
32
|
+
titleDeclarativeTools: 'Declarative Tools',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
36
|
+
|
|
37
|
+
class WebMCPRegisteredTools extends Audit {
|
|
38
|
+
/**
|
|
39
|
+
* @return {LH.Audit.Meta}
|
|
40
|
+
*/
|
|
41
|
+
static get meta() {
|
|
42
|
+
return {
|
|
43
|
+
id: 'webmcp-registered-tools',
|
|
44
|
+
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
|
|
45
|
+
title: str_(UIStrings.title),
|
|
46
|
+
description: str_(UIStrings.description),
|
|
47
|
+
requiredArtifacts: ['WebMCP'],
|
|
48
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {LH.Artifacts} artifacts
|
|
54
|
+
* @return {LH.Audit.Product}
|
|
55
|
+
*/
|
|
56
|
+
static audit(artifacts) {
|
|
57
|
+
if (!artifacts.WebMCP.isSupported) {
|
|
58
|
+
return {
|
|
59
|
+
notApplicable: true,
|
|
60
|
+
score: 1,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const tools = artifacts.WebMCP.tools || [];
|
|
65
|
+
|
|
66
|
+
const imperativeResults = [];
|
|
67
|
+
const declarativeResults = [];
|
|
68
|
+
|
|
69
|
+
for (const tool of tools) {
|
|
70
|
+
const isDeclarative = typeof tool.backendNodeId === 'number';
|
|
71
|
+
|
|
72
|
+
let source;
|
|
73
|
+
let element;
|
|
74
|
+
|
|
75
|
+
if (isDeclarative && tool.nodeDetails) {
|
|
76
|
+
element = Audit.makeNodeItem(tool.nodeDetails);
|
|
77
|
+
} else if (isDeclarative) {
|
|
78
|
+
element = `Node ID: ${tool.backendNodeId}`;
|
|
79
|
+
} else if (tool.stackTrace) {
|
|
80
|
+
const callFrame = tool.stackTrace.callFrames?.[0];
|
|
81
|
+
if (callFrame) {
|
|
82
|
+
source =
|
|
83
|
+
Audit.makeSourceLocation(
|
|
84
|
+
callFrame.url, callFrame.lineNumber, callFrame.columnNumber || 0
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const item = {
|
|
90
|
+
tool: tool.name,
|
|
91
|
+
description: tool.description,
|
|
92
|
+
inputSchema: JSON.stringify(tool.inputSchema, null, 2),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (isDeclarative) {
|
|
96
|
+
declarativeResults.push({
|
|
97
|
+
...item,
|
|
98
|
+
element,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
imperativeResults.push({
|
|
102
|
+
...item,
|
|
103
|
+
source,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const list = [];
|
|
109
|
+
|
|
110
|
+
if (imperativeResults.length > 0) {
|
|
111
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
112
|
+
const headings = [
|
|
113
|
+
{key: 'tool', valueType: 'text', label: str_(UIStrings.columnTool)},
|
|
114
|
+
{key: 'description', valueType: 'text', label: str_(UIStrings.columnDescription)},
|
|
115
|
+
{key: 'source', valueType: 'source-location',
|
|
116
|
+
label: str_(UIStrings.columnImperativeLocation)},
|
|
117
|
+
{key: 'inputSchema', valueType: 'code', label: str_(UIStrings.columnInputSchema)},
|
|
118
|
+
];
|
|
119
|
+
const table = Audit.makeTableDetails(headings, imperativeResults);
|
|
120
|
+
list.push(Audit.makeListDetailSectionItem(table, str_(UIStrings.titleImperativeTools)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (declarativeResults.length > 0) {
|
|
124
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
125
|
+
const headings = [
|
|
126
|
+
{key: 'tool', valueType: 'text', label: str_(UIStrings.columnTool)},
|
|
127
|
+
{key: 'description', valueType: 'text', label: str_(UIStrings.columnDescription)},
|
|
128
|
+
{key: 'element', valueType: 'node', label: str_(UIStrings.columnDeclarativeElement)},
|
|
129
|
+
{key: 'inputSchema', valueType: 'code', label: str_(UIStrings.columnInputSchema)},
|
|
130
|
+
];
|
|
131
|
+
const table = Audit.makeTableDetails(headings, declarativeResults);
|
|
132
|
+
list.push(Audit.makeListDetailSectionItem(table, str_(UIStrings.titleDeclarativeTools)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (list.length === 0) {
|
|
136
|
+
return {
|
|
137
|
+
score: 1,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
score: 1,
|
|
143
|
+
details: Audit.makeListDetails(list),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default WebMCPRegisteredTools;
|
|
149
|
+
export {UIStrings};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default WebMcpSchemaValidity;
|
|
2
|
+
declare class WebMcpSchemaValidity extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {LH.Artifacts} artifacts
|
|
5
|
+
* @return {Promise<LH.Audit.Product>}
|
|
6
|
+
*/
|
|
7
|
+
static audit(artifacts: LH.Artifacts): Promise<LH.Audit.Product>;
|
|
8
|
+
}
|
|
9
|
+
export namespace UIStrings {
|
|
10
|
+
let title: string;
|
|
11
|
+
let failureTitle: string;
|
|
12
|
+
let description: string;
|
|
13
|
+
let columnElement: string;
|
|
14
|
+
let columnIssue: string;
|
|
15
|
+
let missingToolName: string;
|
|
16
|
+
let missingToolDescription: string;
|
|
17
|
+
let missingRequiredParamName: string;
|
|
18
|
+
let missingOptionalParamName: string;
|
|
19
|
+
let missingParamDescription: string;
|
|
20
|
+
}
|
|
21
|
+
import { Audit } from './audit.js';
|
|
22
|
+
//# sourceMappingURL=webmcp-schema-validity.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Audit} from './audit.js';
|
|
8
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
9
|
+
|
|
10
|
+
const UIStrings = {
|
|
11
|
+
/** Title of a Lighthouse audit that evaluates WebMCP schema validity. This descriptive title is shown to users when there are no schema validity issues. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */
|
|
12
|
+
title: 'WebMCP schemas are valid',
|
|
13
|
+
/** Title of a Lighthouse audit that provides detail on WebMCP schema validity. This descriptive title is shown to users when there are schema validity issues. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */
|
|
14
|
+
failureTitle: 'WebMCP schemas are invalid',
|
|
15
|
+
/** Description of a Lighthouse audit that tells the user why they should ensure WebMCP schemas are valid. This is displayed after a user expands the section to see more. No character length limits. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */
|
|
16
|
+
description: 'Valid WebMCP schemas are required for AI agents to ' +
|
|
17
|
+
' understand and interact with tools correctly. ' +
|
|
18
|
+
'Please fix any errors or warnings reported by the browser.',
|
|
19
|
+
/** Header of the table column which displays the element. */
|
|
20
|
+
columnElement: 'Element',
|
|
21
|
+
/** Header of the table column which displays the issue. */
|
|
22
|
+
columnIssue: 'Issue',
|
|
23
|
+
/** Descriptive reason for why a form fails WebMCP validation due to missing `toolname` attribute. */
|
|
24
|
+
missingToolName: 'Form level `toolname` attribute is missing. Add it to define the tool name.',
|
|
25
|
+
/** Descriptive reason for why a form fails WebMCP validation due to missing `tooldescription` attribute. */
|
|
26
|
+
missingToolDescription: 'Form level `tooldescription` attribute is missing. ' +
|
|
27
|
+
'Add it to describe the tool for AI agents.',
|
|
28
|
+
/** Descriptive reason for why a form field fails WebMCP validation due to missing `name` attribute for a required field. */
|
|
29
|
+
missingRequiredParamName: 'Missing `name` attribute for a required field. ' +
|
|
30
|
+
'Add it to define the parameter name.',
|
|
31
|
+
/** Descriptive reason for why a form field fails WebMCP validation due to missing `name` attribute for an optional field. */
|
|
32
|
+
missingOptionalParamName: 'Missing `name` attribute for an optional field. ' +
|
|
33
|
+
'Add it to define the parameter name.',
|
|
34
|
+
/** Descriptive reason for why a form field fails WebMCP validation due to missing description. */
|
|
35
|
+
missingParamDescription: 'Add a description to make this form more accessible for AI agents.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
39
|
+
|
|
40
|
+
class WebMcpSchemaValidity extends Audit {
|
|
41
|
+
/**
|
|
42
|
+
* @return {LH.Audit.Meta}
|
|
43
|
+
*/
|
|
44
|
+
static get meta() {
|
|
45
|
+
return {
|
|
46
|
+
id: 'webmcp-schema-validity',
|
|
47
|
+
title: str_(UIStrings.title),
|
|
48
|
+
failureTitle: str_(UIStrings.failureTitle),
|
|
49
|
+
description: str_(UIStrings.description),
|
|
50
|
+
requiredArtifacts: ['WebMCP', 'WebMcpSchemaIssues'],
|
|
51
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {LH.Artifacts} artifacts
|
|
57
|
+
* @return {Promise<LH.Audit.Product>}
|
|
58
|
+
*/
|
|
59
|
+
static async audit(artifacts) {
|
|
60
|
+
if (!artifacts.WebMCP.isSupported) {
|
|
61
|
+
return {
|
|
62
|
+
notApplicable: true,
|
|
63
|
+
score: 1,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @enum {number} */
|
|
68
|
+
const Severity = {
|
|
69
|
+
ERROR: 1,
|
|
70
|
+
WARNING: 2,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** @type {Record<string, {severity: Severity, description: LH.IcuMessage}>} */
|
|
74
|
+
const issueConfigs = {
|
|
75
|
+
'FormModelContextMissingToolName': {
|
|
76
|
+
severity: Severity.ERROR,
|
|
77
|
+
description: str_(UIStrings.missingToolName),
|
|
78
|
+
},
|
|
79
|
+
'FormModelContextMissingToolDescription': {
|
|
80
|
+
severity: Severity.ERROR,
|
|
81
|
+
description: str_(UIStrings.missingToolDescription),
|
|
82
|
+
},
|
|
83
|
+
'FormModelContextRequiredParameterMissingName': {
|
|
84
|
+
severity: Severity.ERROR,
|
|
85
|
+
description: str_(UIStrings.missingRequiredParamName),
|
|
86
|
+
},
|
|
87
|
+
'FormModelContextParameterMissingTitleAndDescription': {
|
|
88
|
+
severity: Severity.WARNING,
|
|
89
|
+
description: str_(UIStrings.missingParamDescription),
|
|
90
|
+
},
|
|
91
|
+
'FormModelContextParameterMissingName': {
|
|
92
|
+
severity: Severity.WARNING,
|
|
93
|
+
description: str_(UIStrings.missingOptionalParamName),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const rawIssues = artifacts.WebMcpSchemaIssues;
|
|
98
|
+
|
|
99
|
+
const uniqueIssues = [
|
|
100
|
+
...new Map(
|
|
101
|
+
rawIssues.map(issue => [`${issue.violatingNodeId}_${issue.errorType}`, issue])
|
|
102
|
+
).values(),
|
|
103
|
+
];
|
|
104
|
+
const sortedUniqueIssues = uniqueIssues.sort((a, b) => {
|
|
105
|
+
return (issueConfigs[a.errorType]?.severity || Severity.ERROR) -
|
|
106
|
+
(issueConfigs[b.errorType]?.severity || Severity.ERROR);
|
|
107
|
+
});
|
|
108
|
+
const items = sortedUniqueIssues.map(issue => {
|
|
109
|
+
return {
|
|
110
|
+
element: issue.nodeDetails ? Audit.makeNodeItem(issue.nodeDetails) : undefined,
|
|
111
|
+
issue: issueConfigs[issue.errorType]?.description || '',
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
116
|
+
const headings = [
|
|
117
|
+
{key: 'element', valueType: 'node', label: str_(UIStrings.columnElement)},
|
|
118
|
+
{key: 'issue', valueType: 'text', label: str_(UIStrings.columnIssue)},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const details = Audit.makeTableDetails(headings, items);
|
|
122
|
+
|
|
123
|
+
const hasErrors =
|
|
124
|
+
sortedUniqueIssues.some(issue => issueConfigs[issue.errorType]?.severity === Severity.ERROR);
|
|
125
|
+
|
|
126
|
+
if ((artifacts.WebMCP.tools?.length || 0) === 0 && rawIssues.length === 0) {
|
|
127
|
+
return {
|
|
128
|
+
notApplicable: true,
|
|
129
|
+
score: 1,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
score: hasErrors ? 0 : (items.length > 0 ? 0.5 : 1),
|
|
135
|
+
details: items.length > 0 ? details : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default WebMcpSchemaValidity;
|
|
141
|
+
export {UIStrings};
|
|
@@ -41,8 +41,10 @@ class DocumentUrls {
|
|
|
41
41
|
}
|
|
42
42
|
if (!requestedUrl || !mainDocumentUrl) throw new Error('No main frame navigations found');
|
|
43
43
|
|
|
44
|
-
const initialRequest =
|
|
45
|
-
|
|
44
|
+
const initialRequest = Lantern.Core.NetworkAnalyzer.findResourceForUrl(
|
|
45
|
+
networkRecords,
|
|
46
|
+
requestedUrl
|
|
47
|
+
);
|
|
46
48
|
if (initialRequest?.redirects?.length) requestedUrl = initialRequest.redirects[0].url;
|
|
47
49
|
|
|
48
50
|
return {requestedUrl, mainDocumentUrl};
|
|
@@ -23,13 +23,15 @@ class MainResource {
|
|
|
23
23
|
if (!mainDocumentUrl) throw new Error('mainDocumentUrl must exist to get the main resource');
|
|
24
24
|
const records = await NetworkRecords.request(data.devtoolsLog, context);
|
|
25
25
|
|
|
26
|
-
// We could have more than one record matching the main
|
|
26
|
+
// We could have more than one record matching the main document url,
|
|
27
27
|
// if the page did `location.reload()`. Since `mainDocumentUrl` refers to the _last_
|
|
28
28
|
// document request, we should return the last candidate here. Besides, the browser
|
|
29
29
|
// would have evicted the first request by the time `MainDocumentRequest` (a consumer
|
|
30
30
|
// of this computed artifact) attempts to fetch the contents, resulting in a protocol error.
|
|
31
|
-
const mainResource =
|
|
32
|
-
|
|
31
|
+
const mainResource = Lantern.Core.NetworkAnalyzer.findLastDocumentForUrl(
|
|
32
|
+
records,
|
|
33
|
+
mainDocumentUrl
|
|
34
|
+
);
|
|
33
35
|
if (!mainResource) {
|
|
34
36
|
throw new Error('Unable to identify the main resource');
|
|
35
37
|
}
|
|
@@ -39,14 +39,14 @@ async function getComputationDataParamsFromTrace(data, context) {
|
|
|
39
39
|
const graph = await PageDependencyGraph.request({...data, fromTrace: true}, context);
|
|
40
40
|
const traceEngineResult = await TraceEngineResult.request(data, context);
|
|
41
41
|
const frameId = traceEngineResult.data.Meta.mainFrameId;
|
|
42
|
-
const
|
|
43
|
-
traceEngineResult.data.Meta.mainFrameNavigations[0]
|
|
44
|
-
if (!
|
|
42
|
+
const navigation =
|
|
43
|
+
traceEngineResult.data.Meta.mainFrameNavigations[0];
|
|
44
|
+
if (!navigation) {
|
|
45
45
|
throw new Error(`Lantern metrics could not be calculated due to missing navigation id`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const processedNavigation = Lantern.TraceEngineComputationData.createProcessedNavigation(
|
|
49
|
-
traceEngineResult.data, frameId,
|
|
49
|
+
traceEngineResult.data, frameId, navigation);
|
|
50
50
|
const simulator = data.simulator || (await LoadSimulator.request(data, context));
|
|
51
51
|
|
|
52
52
|
return {simulator, graph, processedNavigation};
|
|
@@ -62,7 +62,7 @@ class LCPBreakdown {
|
|
|
62
62
|
throw new LighthouseError(LighthouseError.errors.NO_LCP, {}, {cause: lcpBreakdown});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
if (!lcpBreakdown.subparts) {
|
|
65
|
+
if (!lcpBreakdown || !lcpBreakdown.subparts) {
|
|
66
66
|
throw new LighthouseError(LighthouseError.errors.NO_LCP);
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -53,7 +53,7 @@ class TimeToFirstByte extends NavigationMetric {
|
|
|
53
53
|
const lcpBreakdown = navInsights.model.LCPBreakdown;
|
|
54
54
|
|
|
55
55
|
// Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
|
|
56
|
-
if (!(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
|
|
56
|
+
if (lcpBreakdown && !(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
|
|
57
57
|
return {
|
|
58
58
|
timing: lcpBreakdown.subparts.ttfb.range / 1000,
|
|
59
59
|
timestamp: lcpBreakdown.subparts.ttfb.max,
|
|
@@ -26,7 +26,8 @@ class NavigationInsights {
|
|
|
26
26
|
const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
|
|
27
27
|
if (!navigationId) throw new Error('No navigationId found');
|
|
28
28
|
|
|
29
|
-
const navInsights = traceEngineResult.insights.
|
|
29
|
+
const navInsights = [...traceEngineResult.insights.values()]
|
|
30
|
+
.find(insightSet => insightSet.navigation?.args.data?.navigationId === navigationId);
|
|
30
31
|
if (!navInsights) throw new Error('No navigations insights found');
|
|
31
32
|
|
|
32
33
|
return navInsights;
|
|
@@ -18,7 +18,9 @@ class NetworkAnalysis {
|
|
|
18
18
|
*/
|
|
19
19
|
static async compute_(devtoolsLog, context) {
|
|
20
20
|
const records = await NetworkRecords.request(devtoolsLog, context);
|
|
21
|
-
const analysis = Lantern.Core.NetworkAnalyzer.analyze(
|
|
21
|
+
const analysis = Lantern.Core.NetworkAnalyzer.analyze(
|
|
22
|
+
records
|
|
23
|
+
);
|
|
22
24
|
if (!analysis) {
|
|
23
25
|
log.error('NetworkAnalysis', 'Network analysis failed due to lack of transfer data');
|
|
24
26
|
return {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default config;
|
|
2
|
+
/** @type {LH.Config} */
|
|
3
|
+
declare const config: LH.Config;
|
|
4
|
+
export namespace UIStrings {
|
|
5
|
+
let agenticBrowsingCategoryTitle: string;
|
|
6
|
+
let agenticBrowsingCategoryDescription: string;
|
|
7
|
+
let webmcpGroupTitle: string;
|
|
8
|
+
let webmcpGroupDescription: string;
|
|
9
|
+
let agentAccessibilityGroupTitle: string;
|
|
10
|
+
let agentAccessibilityGroupDescription: string;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=agentic-browsing-config.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
8
|
+
|
|
9
|
+
const UIStrings = {
|
|
10
|
+
/** Title of the Agentic Browsing category of audits. */
|
|
11
|
+
agenticBrowsingCategoryTitle: 'Agentic Browsing',
|
|
12
|
+
/** Description of the Agentic Browsing category. */
|
|
13
|
+
agenticBrowsingCategoryDescription: 'These checks ensure high-quality, browsable ' +
|
|
14
|
+
'websites for AI agents and validate the correctness of WebMCP integrations. ' +
|
|
15
|
+
'This category is still under development and subject to change.',
|
|
16
|
+
/** Title of the WebMCP group of audits. */
|
|
17
|
+
webmcpGroupTitle: 'WebMCP',
|
|
18
|
+
/** Description of the WebMCP group. */
|
|
19
|
+
webmcpGroupDescription: 'Audits validating WebMCP integration.',
|
|
20
|
+
/** Title of the Agent Accessibility group of audits. */
|
|
21
|
+
agentAccessibilityGroupTitle: 'Agent Accessibility',
|
|
22
|
+
/** Description of the Agent Accessibility group of audits. */
|
|
23
|
+
agentAccessibilityGroupDescription: 'These audits highlight best practices for improving the ' +
|
|
24
|
+
'accessibility of the website for AI agents.',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
28
|
+
|
|
29
|
+
/** @type {LH.Config} */
|
|
30
|
+
const config = {
|
|
31
|
+
extends: 'lighthouse:default',
|
|
32
|
+
audits: [
|
|
33
|
+
'agentic/agent-accessibility-tree',
|
|
34
|
+
'webmcp-registered-tools',
|
|
35
|
+
'webmcp-form-coverage',
|
|
36
|
+
'webmcp-schema-validity',
|
|
37
|
+
'agentic/llms-txt',
|
|
38
|
+
],
|
|
39
|
+
artifacts: [
|
|
40
|
+
{id: 'WebMCP', gatherer: 'webmcp'},
|
|
41
|
+
{id: 'WebMcpSchemaIssues', gatherer: 'webmcp-schema'},
|
|
42
|
+
{id: 'LlmsTxt', gatherer: 'agentic/llms-txt'},
|
|
43
|
+
],
|
|
44
|
+
groups: {
|
|
45
|
+
'webmcp': {
|
|
46
|
+
title: str_(UIStrings.webmcpGroupTitle),
|
|
47
|
+
description: str_(UIStrings.webmcpGroupDescription),
|
|
48
|
+
},
|
|
49
|
+
'agent-accessibility': {
|
|
50
|
+
title: str_(UIStrings.agentAccessibilityGroupTitle),
|
|
51
|
+
description: str_(UIStrings.agentAccessibilityGroupDescription),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
categories: {
|
|
55
|
+
'agentic-browsing': {
|
|
56
|
+
title: str_(UIStrings.agenticBrowsingCategoryTitle),
|
|
57
|
+
description: str_(UIStrings.agenticBrowsingCategoryDescription),
|
|
58
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
59
|
+
categoryScoreDisplayMode: 'fraction',
|
|
60
|
+
auditRefs: [
|
|
61
|
+
{id: 'agent-accessibility-tree', weight: 1, group: 'agent-accessibility'},
|
|
62
|
+
{id: 'webmcp-form-coverage', weight: 1, group: 'webmcp'},
|
|
63
|
+
{id: 'webmcp-registered-tools', weight: 1, group: 'webmcp'},
|
|
64
|
+
{id: 'webmcp-schema-validity', weight: 1, group: 'webmcp'},
|
|
65
|
+
{id: 'cumulative-layout-shift', weight: 1, acronym: 'CLS'},
|
|
66
|
+
{id: 'llms-txt', weight: 1, group: 'agent-accessibility'},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default config;
|
|
73
|
+
export {UIStrings};
|
|
@@ -254,6 +254,9 @@ const defaultConfig = {
|
|
|
254
254
|
'accessibility/manual/offscreen-content-hidden',
|
|
255
255
|
'accessibility/manual/use-landmarks',
|
|
256
256
|
'accessibility/manual/visual-order-follows-dom',
|
|
257
|
+
'accessibility/autocomplete-valid',
|
|
258
|
+
'accessibility/presentation-role-conflict',
|
|
259
|
+
'accessibility/svg-img-alt',
|
|
257
260
|
'byte-efficiency/total-byte-weight',
|
|
258
261
|
'byte-efficiency/unminified-css',
|
|
259
262
|
'byte-efficiency/unminified-javascript',
|
|
@@ -266,6 +269,7 @@ const defaultConfig = {
|
|
|
266
269
|
'dobetterweb/js-libraries',
|
|
267
270
|
'dobetterweb/notification-on-start',
|
|
268
271
|
'dobetterweb/paste-preventing-inputs',
|
|
272
|
+
'baseline',
|
|
269
273
|
'seo/meta-description',
|
|
270
274
|
'seo/http-status-code',
|
|
271
275
|
'seo/link-text',
|
|
@@ -514,6 +518,9 @@ const defaultConfig = {
|
|
|
514
518
|
{id: 'valid-lang', weight: 7, group: 'a11y-language'}, // Serious, wcag2aa
|
|
515
519
|
{id: 'video-caption', weight: 10, group: 'a11y-audio-video'}, // Critical, wcag2a
|
|
516
520
|
{id: 'landmark-one-main', weight: 3, group: 'a11y-best-practices'}, // Moderate, best-practice
|
|
521
|
+
{id: 'autocomplete-valid', weight: 1, group: 'a11y-best-practices'}, // Informational
|
|
522
|
+
{id: 'presentation-role-conflict', weight: 1, group: 'a11y-best-practices'}, // Informational
|
|
523
|
+
{id: 'svg-img-alt', weight: 1, group: 'a11y-best-practices'}, // Informational
|
|
517
524
|
// Manual audits
|
|
518
525
|
{id: 'focusable-controls', weight: 0},
|
|
519
526
|
{id: 'interactive-element-affordance', weight: 0},
|
|
@@ -559,6 +566,7 @@ const defaultConfig = {
|
|
|
559
566
|
// Browser Compatibility
|
|
560
567
|
{id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
|
|
561
568
|
{id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
|
|
569
|
+
{id: 'baseline', weight: 0, group: 'best-practices-browser-compat'},
|
|
562
570
|
// General Group
|
|
563
571
|
{id: 'js-libraries', weight: 0, group: 'best-practices-general'},
|
|
564
572
|
{id: 'deprecations', weight: 5, group: 'best-practices-general'},
|
|
@@ -387,11 +387,21 @@ async function isPageHung(session) {
|
|
|
387
387
|
await session.sendCommand('Runtime.evaluate', {
|
|
388
388
|
expression: '"ping"',
|
|
389
389
|
returnByValue: true,
|
|
390
|
-
timeout: 1000,
|
|
391
390
|
});
|
|
392
391
|
|
|
393
392
|
return false;
|
|
394
393
|
} catch (err) {
|
|
394
|
+
// If the session has crashed, we want to rethrow that error instead of assuming it's a hang.
|
|
395
|
+
// session.sendCommand normally handles this, but if PROTOCOL_TIMEOUT wins the race,
|
|
396
|
+
// we might have missed the TARGET_CRASHED error.
|
|
397
|
+
try {
|
|
398
|
+
// Check if it's already crashed.
|
|
399
|
+
await Promise.race([session.onCrashPromise(), Promise.resolve()]);
|
|
400
|
+
} catch (crashErr) {
|
|
401
|
+
if (crashErr.code === 'TARGET_CRASHED') throw crashErr;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (err.code === 'TARGET_CRASHED') throw err;
|
|
395
405
|
return true;
|
|
396
406
|
}
|
|
397
407
|
}
|
|
@@ -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
|
|