lighthouse 13.1.0 → 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.
Files changed (110) hide show
  1. package/cli/bin.js +5 -0
  2. package/core/audits/accessibility/autocomplete-valid.d.ts +10 -0
  3. package/core/audits/accessibility/autocomplete-valid.js +44 -0
  4. package/core/audits/accessibility/presentation-role-conflict.d.ts +10 -0
  5. package/core/audits/accessibility/presentation-role-conflict.js +46 -0
  6. package/core/audits/accessibility/svg-img-alt.d.ts +10 -0
  7. package/core/audits/accessibility/svg-img-alt.js +44 -0
  8. package/core/audits/agentic/agent-accessibility-tree.d.ts +19 -0
  9. package/core/audits/agentic/agent-accessibility-tree.js +115 -0
  10. package/core/audits/agentic/llms-txt.d.ts +20 -0
  11. package/core/audits/agentic/llms-txt.js +111 -0
  12. package/core/audits/insights/insight-audit.d.ts +2 -2
  13. package/core/audits/insights/insight-audit.js +16 -6
  14. package/core/audits/layout-shifts.js +1 -1
  15. package/core/audits/server-response-time.js +3 -3
  16. package/core/audits/webmcp-form-coverage.d.ts +16 -0
  17. package/core/audits/webmcp-form-coverage.js +90 -0
  18. package/core/audits/webmcp-registered-tools.d.ts +21 -0
  19. package/core/audits/webmcp-registered-tools.js +149 -0
  20. package/core/audits/webmcp-schema-validity.d.ts +22 -0
  21. package/core/audits/webmcp-schema-validity.js +141 -0
  22. package/core/computed/document-urls.js +0 -1
  23. package/core/computed/main-resource.js +0 -2
  24. package/core/computed/metrics/lantern-metric.js +4 -4
  25. package/core/computed/metrics/lcp-breakdown.js +1 -1
  26. package/core/computed/metrics/time-to-first-byte.js +1 -1
  27. package/core/computed/navigation-insights.js +2 -1
  28. package/core/computed/network-analysis.js +0 -1
  29. package/core/config/agentic-browsing-config.d.ts +12 -0
  30. package/core/config/agentic-browsing-config.js +73 -0
  31. package/core/config/default-config.js +6 -0
  32. package/core/gather/gatherers/accessibility.js +5 -1
  33. package/core/gather/gatherers/agentic/llms-txt.d.ts +10 -0
  34. package/core/gather/gatherers/agentic/llms-txt.js +28 -0
  35. package/core/gather/gatherers/inputs.js +2 -0
  36. package/core/gather/gatherers/meta-elements.js +1 -1
  37. package/core/gather/gatherers/trace-elements.js +1 -1
  38. package/core/gather/gatherers/webmcp-schema.d.ts +25 -0
  39. package/core/gather/gatherers/webmcp-schema.js +105 -0
  40. package/core/gather/gatherers/webmcp.d.ts +58 -0
  41. package/core/gather/gatherers/webmcp.js +159 -0
  42. package/core/index.d.ts +1 -0
  43. package/core/index.js +1 -0
  44. package/core/lib/cdt/generated/SourceMap.js +2 -2
  45. package/core/lib/navigation-error.js +0 -6
  46. package/core/lib/network-request.js +0 -1
  47. package/core/lib/page-functions.d.ts +3 -3
  48. package/core/lib/page-functions.js +11 -4
  49. package/core/lib/tracehouse/trace-processor.d.ts +5 -4
  50. package/core/lib/tracehouse/trace-processor.js +85 -19
  51. package/core/runner.js +3 -0
  52. package/core/scoring.d.ts +1 -0
  53. package/dist/report/bundle.esm.js +1 -1
  54. package/dist/report/flow.js +3 -3
  55. package/dist/report/standalone.js +1 -1
  56. package/flow-report/src/summary/category.tsx +1 -1
  57. package/package.json +11 -11
  58. package/report/renderer/category-renderer.js +1 -1
  59. package/report/renderer/report-utils.d.ts +2 -1
  60. package/report/renderer/report-utils.js +7 -2
  61. package/shared/localization/locales/ar-XB.json +72 -36
  62. package/shared/localization/locales/ar.json +72 -36
  63. package/shared/localization/locales/bg.json +72 -36
  64. package/shared/localization/locales/ca.json +72 -36
  65. package/shared/localization/locales/cs.json +72 -36
  66. package/shared/localization/locales/da.json +74 -38
  67. package/shared/localization/locales/de.json +72 -36
  68. package/shared/localization/locales/el.json +73 -37
  69. package/shared/localization/locales/en-GB.json +74 -38
  70. package/shared/localization/locales/en-US.json +245 -17
  71. package/shared/localization/locales/en-XL.json +245 -17
  72. package/shared/localization/locales/es-419.json +72 -36
  73. package/shared/localization/locales/es.json +73 -37
  74. package/shared/localization/locales/fi.json +72 -36
  75. package/shared/localization/locales/fil.json +74 -38
  76. package/shared/localization/locales/fr.json +162 -126
  77. package/shared/localization/locales/he.json +74 -38
  78. package/shared/localization/locales/hi.json +73 -37
  79. package/shared/localization/locales/hr.json +72 -36
  80. package/shared/localization/locales/hu.json +73 -37
  81. package/shared/localization/locales/id.json +74 -38
  82. package/shared/localization/locales/it.json +72 -36
  83. package/shared/localization/locales/ja.json +72 -36
  84. package/shared/localization/locales/ko.json +72 -36
  85. package/shared/localization/locales/lt.json +72 -36
  86. package/shared/localization/locales/lv.json +72 -36
  87. package/shared/localization/locales/nl.json +73 -37
  88. package/shared/localization/locales/no.json +72 -36
  89. package/shared/localization/locales/pl.json +72 -36
  90. package/shared/localization/locales/pt-PT.json +72 -36
  91. package/shared/localization/locales/pt.json +74 -38
  92. package/shared/localization/locales/ro.json +72 -36
  93. package/shared/localization/locales/ru.json +72 -36
  94. package/shared/localization/locales/sk.json +72 -36
  95. package/shared/localization/locales/sl.json +72 -36
  96. package/shared/localization/locales/sr-Latn.json +73 -37
  97. package/shared/localization/locales/sr.json +73 -37
  98. package/shared/localization/locales/sv.json +75 -39
  99. package/shared/localization/locales/ta.json +73 -37
  100. package/shared/localization/locales/te.json +72 -36
  101. package/shared/localization/locales/th.json +73 -37
  102. package/shared/localization/locales/tr.json +72 -36
  103. package/shared/localization/locales/uk.json +72 -36
  104. package/shared/localization/locales/vi.json +74 -38
  105. package/shared/localization/locales/zh-HK.json +72 -36
  106. package/shared/localization/locales/zh-TW.json +74 -38
  107. package/shared/localization/locales/zh.json +75 -39
  108. package/types/artifacts.d.ts +33 -0
  109. package/types/config.d.ts +1 -0
  110. package/types/lhr/lhr.d.ts +11 -0
package/cli/bin.js CHANGED
@@ -107,6 +107,11 @@ async function begin() {
107
107
  cliFlags.precomputedLanternData = data;
108
108
  }
109
109
 
110
+ if (!Array.isArray(cliFlags.chromeFlags)) {
111
+ cliFlags.chromeFlags = [cliFlags.chromeFlags];
112
+ }
113
+ cliFlags.chromeFlags.push('--enable-features=DevToolsWebMCPSupport');
114
+
110
115
  // By default, cliFlags.enableErrorReporting is undefined so the user is
111
116
  // prompted. This can be overridden with an explicit flag or by the cached
112
117
  // answer returned by askPermission().
@@ -0,0 +1,10 @@
1
+ export default AutocompleteValid;
2
+ declare class AutocompleteValid extends AxeAudit {
3
+ }
4
+ export namespace UIStrings {
5
+ let title: string;
6
+ let failureTitle: string;
7
+ let description: string;
8
+ }
9
+ import AxeAudit from './axe-audit.js';
10
+ //# sourceMappingURL=autocomplete-valid.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Ensures the autocomplete attribute is correct and suitable for the form field.
9
+ * See base class in axe-audit.js for audit() implementation.
10
+ */
11
+
12
+ import AxeAudit from './axe-audit.js';
13
+ import * as i18n from '../../lib/i18n/i18n.js';
14
+
15
+ const UIStrings = {
16
+ /** Title of an accessibility audit that evaluates if all form fields have valid autocomplete attributes. This title is descriptive of the successful state and is shown to users when no user action is required. */
17
+ title: '`autocomplete` attributes are used correctly',
18
+ /** Title of an accessibility audit that evaluates if all form fields have valid autocomplete attributes. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
19
+ failureTitle: '`autocomplete` attributes are not used correctly',
20
+ /** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
21
+ description: 'The `autocomplete` attribute values must be valid and correctly applied for ' +
22
+ 'screen readers to function correctly. ' +
23
+ '[Learn more about valid autocomplete values](https://dequeuniversity.com/rules/axe/4.11/autocomplete-valid).',
24
+ };
25
+
26
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
27
+
28
+ class AutocompleteValid extends AxeAudit {
29
+ /**
30
+ * @return {LH.Audit.Meta}
31
+ */
32
+ static get meta() {
33
+ return {
34
+ id: 'autocomplete-valid',
35
+ title: str_(UIStrings.title),
36
+ failureTitle: str_(UIStrings.failureTitle),
37
+ description: str_(UIStrings.description),
38
+ requiredArtifacts: ['Accessibility'],
39
+ };
40
+ }
41
+ }
42
+
43
+ export default AutocompleteValid;
44
+ export {UIStrings};
@@ -0,0 +1,10 @@
1
+ export default PresentationRoleConflict;
2
+ declare class PresentationRoleConflict extends AxeAudit {
3
+ }
4
+ export namespace UIStrings {
5
+ let title: string;
6
+ let failureTitle: string;
7
+ let description: string;
8
+ }
9
+ import AxeAudit from './axe-audit.js';
10
+ //# sourceMappingURL=presentation-role-conflict.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Ensures elements which are marked to be removed from the accessibility tree are in fact removed.
9
+ * See base class in axe-audit.js for audit() implementation.
10
+ */
11
+
12
+ import AxeAudit from './axe-audit.js';
13
+ import * as i18n from '../../lib/i18n/i18n.js';
14
+
15
+ const UIStrings = {
16
+ /** Title of an accessibility audit that evaluates if elements with presentation role have conflicts. This title is descriptive of the successful state and is shown to users when no user action is required. */
17
+ title: 'Elements with `role="none"` or `role="presentation"` do not have conflicts',
18
+ /** Title of an accessibility audit that evaluates if elements with presentation role have conflicts. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
19
+ failureTitle: 'Elements with `role="none"` or `role="presentation"` have conflicts',
20
+ /** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with \'Learn\' becomes link text to additional documentation. */
21
+ description: 'There are certain cases where the semantic role of an element with `role="none"` ' +
22
+ 'or `role="presentation"` does not resolve to none or presentation. To ensure the ' +
23
+ 'element remains removed from the accessibility tree, you should not add any global ' +
24
+ 'ARIA attributes to the element or make it focusable. ' +
25
+ '[Learn more about presentation role conflict](https://dequeuniversity.com/rules/axe/4.11/presentation-role-conflict).',
26
+ };
27
+
28
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
29
+
30
+ class PresentationRoleConflict extends AxeAudit {
31
+ /**
32
+ * @return {LH.Audit.Meta}
33
+ */
34
+ static get meta() {
35
+ return {
36
+ id: 'presentation-role-conflict',
37
+ title: str_(UIStrings.title),
38
+ failureTitle: str_(UIStrings.failureTitle),
39
+ description: str_(UIStrings.description),
40
+ requiredArtifacts: ['Accessibility'],
41
+ };
42
+ }
43
+ }
44
+
45
+ export default PresentationRoleConflict;
46
+ export {UIStrings};
@@ -0,0 +1,10 @@
1
+ export default SvgImgAlt;
2
+ declare class SvgImgAlt extends AxeAudit {
3
+ }
4
+ export namespace UIStrings {
5
+ let title: string;
6
+ let failureTitle: string;
7
+ let description: string;
8
+ }
9
+ import AxeAudit from './axe-audit.js';
10
+ //# sourceMappingURL=svg-img-alt.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Ensures SVG elements with an img role have an accessible text alternative.
9
+ * See base class in axe-audit.js for audit() implementation.
10
+ */
11
+
12
+ import AxeAudit from './axe-audit.js';
13
+ import * as i18n from '../../lib/i18n/i18n.js';
14
+
15
+ const UIStrings = {
16
+ /** Title of an accessibility audit that evaluates if SVG elements with an img role have an accessible text alternative. This title is descriptive of the successful state and is shown to users when no user action is required. */
17
+ title: 'SVG elements with an `img` role have an accessible text alternative',
18
+ /** Title of an accessibility audit that evaluates if SVG elements with an img role have an accessible text alternative. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
19
+ failureTitle: 'SVG elements with an `img` role do not have an accessible text alternative',
20
+ /** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with \'Learn\' becomes link text to additional documentation. */
21
+ description: 'Ensures SVG elements with an `img`, `graphics-document` or `graphics-symbol` ' +
22
+ 'role have an accessible text alternative. ' +
23
+ '[Learn more about SVG alt text](https://dequeuniversity.com/rules/axe/4.11/svg-img-alt).',
24
+ };
25
+
26
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
27
+
28
+ class SvgImgAlt extends AxeAudit {
29
+ /**
30
+ * @return {LH.Audit.Meta}
31
+ */
32
+ static get meta() {
33
+ return {
34
+ id: 'svg-img-alt',
35
+ title: str_(UIStrings.title),
36
+ failureTitle: str_(UIStrings.failureTitle),
37
+ description: str_(UIStrings.description),
38
+ requiredArtifacts: ['Accessibility'],
39
+ };
40
+ }
41
+ }
42
+
43
+ export default SvgImgAlt;
44
+ export {UIStrings};
@@ -0,0 +1,19 @@
1
+ export default AgentAccessibilityTree;
2
+ declare class AgentAccessibilityTree extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @return {LH.Audit.Product}
6
+ */
7
+ static audit(artifacts: LH.Artifacts): LH.Audit.Product;
8
+ }
9
+ export namespace UIStrings {
10
+ let title: string;
11
+ let failureTitle: string;
12
+ let description: string;
13
+ let columnRule: string;
14
+ let columnElement: string;
15
+ let failedSectionTitle: string;
16
+ let displayValuePassed: string;
17
+ }
18
+ import { Audit } from '../audit.js';
19
+ //# sourceMappingURL=agent-accessibility-tree.d.ts.map
@@ -0,0 +1,115 @@
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 shown when all agent accessibility checks pass. */
12
+ title: 'Accessibility tree is well-formed',
13
+ /** Title shown when one or more agent accessibility checks fail. */
14
+ failureTitle: 'Accessibility tree is not well-formed',
15
+ /** Description of a Lighthouse audit that tells the user *why* they need a well-formed accessibility tree. */
16
+ description: 'A well-formed accessibility tree helps AI agents to ' +
17
+ 'navigate and interact with the page.',
18
+ /** Label of a table column that identifies the accessibility rule that failed. */
19
+ columnRule: 'Failing Rule',
20
+ /** Label of a table column that identifies the HTML element that failed the rule. */
21
+ columnElement: 'Failing Element',
22
+ /** Title of the section containing failed accessibility checks. */
23
+ failedSectionTitle: 'Failed Audits',
24
+ /** Message shown when all accessibility checks pass. */
25
+ displayValuePassed: 'All audits passed',
26
+ };
27
+
28
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
29
+
30
+ const TARGET_RULES = new Set([
31
+ 'button-name',
32
+ 'input-button-name',
33
+ 'input-image-alt',
34
+ 'label',
35
+ 'link-name',
36
+ 'select-name',
37
+ 'document-title',
38
+ 'aria-allowed-attr',
39
+ 'aria-allowed-role',
40
+ 'aria-command-name',
41
+ 'aria-conditional-attr',
42
+ 'aria-dialog-name',
43
+ 'aria-hidden-body',
44
+ 'aria-hidden-focus',
45
+ 'aria-input-field-name',
46
+ 'aria-prohibited-attr',
47
+ 'aria-required-attr',
48
+ 'aria-required-children',
49
+ 'aria-required-parent',
50
+ 'aria-roles',
51
+ 'aria-text',
52
+ 'aria-toggle-field-name',
53
+ 'aria-tooltip-name',
54
+ 'aria-treeitem-name',
55
+ 'aria-valid-attr',
56
+ 'aria-valid-attr-value',
57
+ 'duplicate-id-aria',
58
+ 'definition-list',
59
+ 'table-duplicate-name',
60
+ 'tabindex',
61
+ 'autocomplete-valid',
62
+ 'presentation-role-conflict',
63
+ 'svg-img-alt',
64
+ ]);
65
+
66
+ class AgentAccessibilityTree extends Audit {
67
+ /**
68
+ * @return {LH.Audit.Meta}
69
+ */
70
+ static get meta() {
71
+ return {
72
+ id: 'agent-accessibility-tree',
73
+ title: str_(UIStrings.title),
74
+ failureTitle: str_(UIStrings.failureTitle),
75
+ description: str_(UIStrings.description),
76
+ requiredArtifacts: ['Accessibility'],
77
+ };
78
+ }
79
+
80
+ /**
81
+ * @param {LH.Artifacts} artifacts
82
+ * @return {LH.Audit.Product}
83
+ */
84
+ static audit(artifacts) {
85
+ const violations = (artifacts.Accessibility && artifacts.Accessibility.violations) || [];
86
+ const failedRules = violations.filter(v => TARGET_RULES.has(v.id));
87
+
88
+ /** @type {LH.Audit.Details.Table['headings']} */
89
+ const headings = [
90
+ {key: 'description', valueType: 'text', label: str_(i18n.UIStrings.columnDescription)},
91
+ {key: 'node', valueType: 'node', label: str_(UIStrings.columnElement)},
92
+ ];
93
+
94
+ const items = failedRules.map(rule => ({
95
+ description: rule.help || rule.description,
96
+ node: rule.nodes?.[0] ? Audit.makeNodeItem(rule.nodes[0].node) : undefined,
97
+ }));
98
+
99
+ const listItems = [];
100
+
101
+ if (items.length > 0) {
102
+ const table = Audit.makeTableDetails(headings, items);
103
+ listItems.push(Audit.makeListDetailSectionItem(table, str_(UIStrings.failedSectionTitle)));
104
+ }
105
+
106
+ return {
107
+ score: Number(items.length === 0),
108
+ details: listItems.length > 0 ? Audit.makeListDetails(listItems) : undefined,
109
+ displayValue: items.length === 0 ? str_(UIStrings.displayValuePassed) : undefined,
110
+ };
111
+ }
112
+ }
113
+
114
+ export default AgentAccessibilityTree;
115
+ export {UIStrings};
@@ -0,0 +1,20 @@
1
+ export default LlmsTxt;
2
+ declare class LlmsTxt extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @return {LH.Audit.Product}
6
+ */
7
+ static audit(artifacts: LH.Artifacts): LH.Audit.Product;
8
+ }
9
+ export namespace UIStrings {
10
+ let title: string;
11
+ let failureTitle: string;
12
+ let description: string;
13
+ let displayValueHttpBadCode: string;
14
+ let explanation: string;
15
+ let missingH1: string;
16
+ let tooShort: string;
17
+ let missingLinks: string;
18
+ }
19
+ import { Audit } from '../audit.js';
20
+ //# sourceMappingURL=llms-txt.d.ts.map
@@ -0,0 +1,111 @@
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 HTTP_CLIENT_ERROR_CODE_LOW = 400;
11
+ const HTTP_SERVER_ERROR_CODE_LOW = 500;
12
+
13
+ const UIStrings = {
14
+ /** Title of a Lighthouse audit that provides detail on the site's llms.txt file. Note: "llms.txt" is a canonical filename and should not be translated. This descriptive title is shown when the llms.txt file is present and configured correctly. */
15
+ title: 'llms.txt is present',
16
+ /** Title of a Lighthouse audit that provides detail on the site's llms.txt file. Note: "llms.txt" is a canonical filename and should not be translated. This descriptive title is shown when the llms.txt file is misconfigured. */
17
+ failureTitle: 'llms.txt is missing or incomplete',
18
+ /** Description of a Lighthouse audit that tells the user *why* they need to have a valid llms.txt file. Note: "llms.txt" is a canonical filename and should not be translated. This is displayed after a user expands the section to see more. No character length limits. */
19
+ description: 'If your llms.txt file is malformed, large language models may not be able to ' +
20
+ 'understand how you want your website to be crawled or used for training. A valid ' +
21
+ 'llms.txt should be a Markdown file containing at least one H1 header. ' +
22
+ '[Learn more about llms.txt](https://llmstxt.org/).',
23
+ /**
24
+ * @description Label for the audit identifying that the request failed with a specific HTTP status code.
25
+ * @example {500} statusCode
26
+ * */
27
+ displayValueHttpBadCode: 'Failed with HTTP status {statusCode}',
28
+ /** Explanatory message stating that there was a failure in an audit caused by Lighthouse not being able to download the llms.txt file for the site. Note: "llms.txt" is a canonical filename and should not be translated. */
29
+ explanation: 'Fetch of llms.txt failed',
30
+ /** Message indicating that the file is missing a required H1 header. */
31
+ missingH1: 'File is missing a required H1 header (e.g., "# Title").',
32
+ /** Message indicating that the file is suspiciously short. */
33
+ tooShort: 'File is suspiciously short.',
34
+ /** Message indicating that the file is missing links. */
35
+ missingLinks: 'File does not appear to contain any links.',
36
+ };
37
+
38
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
39
+
40
+ class LlmsTxt extends Audit {
41
+ /**
42
+ * @return {LH.Audit.Meta}
43
+ */
44
+ static get meta() {
45
+ return {
46
+ id: 'llms-txt',
47
+ title: str_(UIStrings.title),
48
+ failureTitle: str_(UIStrings.failureTitle),
49
+ description: str_(UIStrings.description),
50
+ requiredArtifacts: ['LlmsTxt'],
51
+ };
52
+ }
53
+
54
+ /**
55
+ * @param {LH.Artifacts} artifacts
56
+ * @return {LH.Audit.Product}
57
+ */
58
+ static audit(artifacts) {
59
+ const {
60
+ status,
61
+ content,
62
+ } = artifacts.LlmsTxt;
63
+
64
+ if (!status) {
65
+ return {
66
+ score: 0,
67
+ explanation: str_(UIStrings.explanation),
68
+ };
69
+ }
70
+
71
+ if (status >= HTTP_SERVER_ERROR_CODE_LOW) {
72
+ return {
73
+ score: 0,
74
+ displayValue: str_(UIStrings.displayValueHttpBadCode, {statusCode: status}),
75
+ };
76
+ } else if (status >= HTTP_CLIENT_ERROR_CODE_LOW) {
77
+ return {
78
+ score: 1,
79
+ notApplicable: true,
80
+ };
81
+ }
82
+
83
+ if (content === null) {
84
+ throw new Error(`Status ${status} was valid, but content was null`);
85
+ }
86
+
87
+ const hasH1 = /^#\s+.+/m.test(content);
88
+ const hasLink = /\[.+\]\(.+\)/.test(content);
89
+ const isTooShort = content.length < 50;
90
+
91
+ const errors = [];
92
+ if (!hasH1) errors.push(str_(UIStrings.missingH1));
93
+ if (!hasLink) errors.push(str_(UIStrings.missingLinks));
94
+ if (isTooShort) errors.push(str_(UIStrings.tooShort));
95
+
96
+ /** @type {LH.Audit.Details.Table['headings']} */
97
+ const headings = [
98
+ {key: 'message', valueType: 'text', label: 'Error'},
99
+ ];
100
+
101
+ const details = Audit.makeTableDetails(headings, errors.map(m => ({message: m})));
102
+
103
+ return {
104
+ score: Number(errors.length === 0),
105
+ details: errors.length ? details : undefined,
106
+ };
107
+ }
108
+ }
109
+
110
+ export default LlmsTxt;
111
+ export {UIStrings};
@@ -11,11 +11,11 @@ export type CreateDetailsExtras = {
11
11
  * @param {LH.Artifacts} artifacts
12
12
  * @param {LH.Audit.Context} context
13
13
  * @param {T} insightName
14
- * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
14
+ * @param {(insight: NonNullable<import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T]>, extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
15
15
  * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
16
16
  * @return {Promise<LH.Audit.Product>}
17
17
  */
18
- export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModelsType>(artifacts: LH.Artifacts, context: LH.Audit.Context, insightName: T, createDetails: (insight: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModels[T], extras: CreateDetailsExtras) => {
18
+ export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModelsType>(artifacts: LH.Artifacts, context: LH.Audit.Context, insightName: T, createDetails: (insight: NonNullable<import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModels[T]>, extras: CreateDetailsExtras) => {
19
19
  details: LH.Audit.Details;
20
20
  warnings?: Array<string | LH.IcuMessage>;
21
21
  numericValue?: number;
@@ -27,8 +27,10 @@ async function getInsightSet(artifacts, context) {
27
27
  await TraceEngineResult.request({trace, settings, SourceMaps, HostDPR}, context);
28
28
 
29
29
  const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
30
- const key = navigationId ?? NO_NAVIGATION;
31
- const insights = traceEngineResult.insights.get(key);
30
+ const insights = navigationId ?
31
+ [...traceEngineResult.insights.values()]
32
+ .find(insightSet => insightSet.navigation?.args.data?.navigationId) :
33
+ traceEngineResult.insights.get(NO_NAVIGATION);
32
34
 
33
35
  return {insights, data: traceEngineResult.data};
34
36
  }
@@ -43,7 +45,7 @@ async function getInsightSet(artifacts, context) {
43
45
  * @param {LH.Artifacts} artifacts
44
46
  * @param {LH.Audit.Context} context
45
47
  * @param {T} insightName
46
- * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
48
+ * @param {(insight: NonNullable<import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T]>, extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
47
49
  * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
48
50
  * @return {Promise<LH.Audit.Product>}
49
51
  */
@@ -56,11 +58,19 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
56
58
  };
57
59
  }
58
60
 
61
+ const error = insights.modelErrors[insightName];
62
+ if (error) {
63
+ return {
64
+ errorMessage: error.message,
65
+ errorStack: error.stack,
66
+ score: null,
67
+ };
68
+ }
69
+
59
70
  const insight = insights.model[insightName];
60
- if (insight instanceof Error) {
71
+ if (!insight) {
61
72
  return {
62
- errorMessage: insight.message,
63
- errorStack: insight.stack,
73
+ scoreDisplayMode: Audit.SCORING_MODES.NOT_APPLICABLE,
64
74
  score: null,
65
75
  };
66
76
  }
@@ -73,7 +73,7 @@ class LayoutShifts extends Audit {
73
73
  layoutShifts: new Map(),
74
74
  };
75
75
  for (const insightSet of traceEngineResult.insights.values()) {
76
- for (const [shift, reasons] of insightSet.model.CLSCulprits.shifts) {
76
+ for (const [shift, reasons] of insightSet.model.CLSCulprits?.shifts ?? []) {
77
77
  allRootCauses.layoutShifts.set(shift, reasons);
78
78
  }
79
79
  }
@@ -51,15 +51,15 @@ class ServerResponseTime extends Audit {
51
51
  const {SourceMaps, HostDPR} = artifacts;
52
52
  const navInsights =
53
53
  await NavigationInsights.request({trace, settings, SourceMaps, HostDPR}, context);
54
- const responseTime = navInsights.model.DocumentLatency.data?.serverResponseTime;
55
- const url = navInsights.model.DocumentLatency.data?.documentRequest?.args.data.url;
54
+ const responseTime = navInsights.model.DocumentLatency?.data?.serverResponseTime;
55
+ const url = navInsights.model.DocumentLatency?.data?.documentRequest?.args.data.url;
56
56
 
57
57
  if (responseTime === undefined || !url) {
58
58
  throw new Error('no timing found for main resource');
59
59
  }
60
60
 
61
61
  const passed =
62
- Boolean(navInsights.model.DocumentLatency.data?.checklist.serverResponseIsFast.value);
62
+ Boolean(navInsights.model.DocumentLatency?.data?.checklist.serverResponseIsFast.value);
63
63
  const displayValue = str_(UIStrings.displayValue, {timeInMs: responseTime});
64
64
 
65
65
  /** @type {LH.Audit.Details.Opportunity['headings']} */
@@ -0,0 +1,16 @@
1
+ export default WebMcpFormCoverage;
2
+ declare class WebMcpFormCoverage extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @return {LH.Audit.Product}
6
+ */
7
+ static audit(artifacts: LH.Artifacts): LH.Audit.Product;
8
+ }
9
+ export namespace UIStrings {
10
+ let title: string;
11
+ let description: string;
12
+ let displayValue: string;
13
+ let columnForm: string;
14
+ }
15
+ import { Audit } from './audit.js';
16
+ //# sourceMappingURL=webmcp-form-coverage.d.ts.map
@@ -0,0 +1,90 @@
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 lists forms found in the page for WebMCP coverage. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */
12
+ title: 'WebMCP form coverage',
13
+ /** Description of a Lighthouse audit that lists forms found in the page and indicates whether they have WebMCP declarative tool annotations. 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. */
14
+ description: 'Consider adding WebMCP annotations to the forms listed below. This helps AI ' +
15
+ 'agents identify and interact with these forms more reliably.', // TODO: add a learn more link once available
16
+ /** [ICU Syntax] Label for the audit identifying the number of forms missing annotations. "WebMCP" stands for "Web Model Context Protocol" and should not be translated. */
17
+ displayValue: `{itemCount, plural,
18
+ =1 {1 form missing annotations}
19
+ other {# forms missing annotations}
20
+ }`,
21
+ /** Label for a column in a data table; entries will be the form element found on the page. */
22
+ columnForm: 'Form',
23
+ };
24
+
25
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
26
+
27
+ class WebMcpFormCoverage extends Audit {
28
+ /**
29
+ * @return {LH.Audit.Meta}
30
+ */
31
+ static get meta() {
32
+ return {
33
+ id: 'webmcp-form-coverage',
34
+ scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
35
+ title: str_(UIStrings.title),
36
+ description: str_(UIStrings.description),
37
+ requiredArtifacts: ['Inputs', 'WebMCP'],
38
+ supportedModes: ['navigation', 'snapshot'],
39
+ };
40
+ }
41
+
42
+ /**
43
+ * @param {LH.Artifacts} artifacts
44
+ * @return {LH.Audit.Product}
45
+ */
46
+ static audit(artifacts) {
47
+ const forms = artifacts.Inputs.forms;
48
+ if (forms.length === 0 || !artifacts.WebMCP.isSupported) {
49
+ return {
50
+ notApplicable: true,
51
+ score: 1,
52
+ };
53
+ }
54
+ const withoutTools = [];
55
+
56
+ for (const form of forms) {
57
+ const hasToolName = !!form.webMcpToolname;
58
+ const hasToolDescription = !!form.webMcpTooldescription;
59
+
60
+ if (!hasToolName && !hasToolDescription) {
61
+ withoutTools.push({
62
+ node: Audit.makeNodeItem(form.node),
63
+ });
64
+ }
65
+ }
66
+
67
+ if (withoutTools.length === 0) {
68
+ return {
69
+ notApplicable: true,
70
+ score: 1,
71
+ };
72
+ }
73
+
74
+ /** @type {LH.Audit.Details.Table['headings']} */
75
+ const headings = [
76
+ {key: 'node', valueType: 'node', label: str_(UIStrings.columnForm)},
77
+ ];
78
+
79
+ const details = Audit.makeTableDetails(headings, withoutTools);
80
+
81
+ return {
82
+ score: 1,
83
+ displayValue: str_(UIStrings.displayValue, {itemCount: withoutTools.length}),
84
+ details,
85
+ };
86
+ }
87
+ }
88
+
89
+ export default WebMcpFormCoverage;
90
+ export {UIStrings};