lighthouse 13.4.1 → 13.5.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 (201) hide show
  1. package/build-tracker.config.js +1 -1
  2. package/cli/test/smokehouse/lighthouse-runners/bundle.js +3 -3
  3. package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.js +2 -1
  4. package/core/audits/accessibility/accesskeys.js +1 -1
  5. package/core/audits/accessibility/aria-allowed-attr.js +1 -1
  6. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  7. package/core/audits/accessibility/aria-command-name.js +1 -1
  8. package/core/audits/accessibility/aria-conditional-attr.js +1 -1
  9. package/core/audits/accessibility/aria-deprecated-role.js +1 -1
  10. package/core/audits/accessibility/aria-dialog-name.js +1 -1
  11. package/core/audits/accessibility/aria-hidden-body.js +1 -1
  12. package/core/audits/accessibility/aria-hidden-focus.js +1 -1
  13. package/core/audits/accessibility/aria-input-field-name.js +1 -1
  14. package/core/audits/accessibility/aria-meter-name.js +1 -1
  15. package/core/audits/accessibility/aria-progressbar-name.js +1 -1
  16. package/core/audits/accessibility/aria-prohibited-attr.js +1 -1
  17. package/core/audits/accessibility/aria-required-attr.js +1 -1
  18. package/core/audits/accessibility/aria-required-children.js +1 -1
  19. package/core/audits/accessibility/aria-required-parent.js +1 -1
  20. package/core/audits/accessibility/aria-roles.js +1 -1
  21. package/core/audits/accessibility/aria-text.js +1 -1
  22. package/core/audits/accessibility/aria-toggle-field-name.js +1 -1
  23. package/core/audits/accessibility/aria-tooltip-name.js +1 -1
  24. package/core/audits/accessibility/aria-treeitem-name.js +1 -1
  25. package/core/audits/accessibility/aria-valid-attr-value.js +1 -1
  26. package/core/audits/accessibility/aria-valid-attr.js +1 -1
  27. package/core/audits/accessibility/autocomplete-valid.js +1 -1
  28. package/core/audits/accessibility/button-name.js +1 -1
  29. package/core/audits/accessibility/bypass.js +1 -1
  30. package/core/audits/accessibility/color-contrast.js +1 -1
  31. package/core/audits/accessibility/definition-list.js +1 -1
  32. package/core/audits/accessibility/dlitem.js +1 -1
  33. package/core/audits/accessibility/document-title.js +1 -1
  34. package/core/audits/accessibility/duplicate-id-aria.js +1 -1
  35. package/core/audits/accessibility/empty-heading.js +1 -1
  36. package/core/audits/accessibility/form-field-multiple-labels.js +1 -1
  37. package/core/audits/accessibility/frame-title.js +1 -1
  38. package/core/audits/accessibility/heading-order.js +1 -1
  39. package/core/audits/accessibility/html-has-lang.js +1 -1
  40. package/core/audits/accessibility/html-lang-valid.js +1 -1
  41. package/core/audits/accessibility/html-xml-lang-mismatch.js +1 -1
  42. package/core/audits/accessibility/identical-links-same-purpose.js +1 -1
  43. package/core/audits/accessibility/image-alt.js +1 -1
  44. package/core/audits/accessibility/image-redundant-alt.js +1 -1
  45. package/core/audits/accessibility/input-button-name.js +1 -1
  46. package/core/audits/accessibility/input-image-alt.js +1 -1
  47. package/core/audits/accessibility/label-content-name-mismatch.js +1 -1
  48. package/core/audits/accessibility/label.js +1 -1
  49. package/core/audits/accessibility/landmark-one-main.js +1 -1
  50. package/core/audits/accessibility/link-in-text-block.js +1 -1
  51. package/core/audits/accessibility/link-name.js +1 -1
  52. package/core/audits/accessibility/list.js +1 -1
  53. package/core/audits/accessibility/listitem.js +1 -1
  54. package/core/audits/accessibility/meta-refresh.js +1 -1
  55. package/core/audits/accessibility/meta-viewport.js +1 -1
  56. package/core/audits/accessibility/object-alt.js +1 -1
  57. package/core/audits/accessibility/presentation-role-conflict.js +1 -1
  58. package/core/audits/accessibility/select-name.js +1 -1
  59. package/core/audits/accessibility/skip-link.js +1 -1
  60. package/core/audits/accessibility/svg-img-alt.js +1 -1
  61. package/core/audits/accessibility/tabindex.js +1 -1
  62. package/core/audits/accessibility/table-duplicate-name.js +1 -1
  63. package/core/audits/accessibility/table-fake-caption.js +1 -1
  64. package/core/audits/accessibility/target-size.js +1 -1
  65. package/core/audits/accessibility/td-has-header.js +1 -1
  66. package/core/audits/accessibility/td-headers-attr.js +1 -1
  67. package/core/audits/accessibility/th-has-data-cells.js +1 -1
  68. package/core/audits/accessibility/valid-lang.js +1 -1
  69. package/core/audits/accessibility/video-caption.js +1 -1
  70. package/core/audits/agentic/ard-schema.d.ts +19 -0
  71. package/core/audits/agentic/ard-schema.js +135 -0
  72. package/core/audits/baseline.d.ts +23 -10
  73. package/core/audits/deprecations.js +1 -2
  74. package/core/audits/webmcp-form-coverage.js +3 -1
  75. package/core/audits/webmcp-registered-tools.d.ts +2 -0
  76. package/core/audits/webmcp-registered-tools.js +16 -1
  77. package/core/audits/webmcp-schema-validity.js +1 -1
  78. package/core/computed/metrics/interactive.js +1 -1
  79. package/core/computed/metrics/lantern-metric.js +1 -1
  80. package/core/computed/trace-engine-result.d.ts +3 -3
  81. package/core/computed/trace-engine-result.js +4 -4
  82. package/core/computed/unused-javascript-summary.d.ts +0 -1
  83. package/core/computed/unused-javascript-summary.js +0 -1
  84. package/core/config/agentic-browsing-config.d.ts +2 -0
  85. package/core/config/agentic-browsing-config.js +13 -1
  86. package/core/config/default-config.js +13 -1
  87. package/core/config/validation.js +6 -0
  88. package/core/gather/driver/execution-context.js +0 -2
  89. package/core/gather/driver/navigation.d.ts +1 -0
  90. package/core/gather/driver/navigation.js +35 -7
  91. package/core/gather/driver/wait-for-condition.d.ts +5 -7
  92. package/core/gather/driver/wait-for-condition.js +15 -9
  93. package/core/gather/fetcher.d.ts +9 -2
  94. package/core/gather/fetcher.js +30 -4
  95. package/core/gather/gatherers/accessibility.js +1 -3
  96. package/core/gather/gatherers/agentic/ard.d.ts +30 -0
  97. package/core/gather/gatherers/agentic/ard.js +125 -0
  98. package/core/gather/gatherers/anchor-elements.js +0 -2
  99. package/core/gather/gatherers/full-page-screenshot.js +0 -4
  100. package/core/gather/gatherers/iframe-elements.js +0 -2
  101. package/core/gather/gatherers/image-elements.js +1 -13
  102. package/core/gather/gatherers/inputs.js +0 -2
  103. package/core/gather/gatherers/link-elements.js +0 -2
  104. package/core/gather/gatherers/meta-elements.js +0 -3
  105. package/core/gather/gatherers/seo/robots-txt.d.ts +1 -0
  106. package/core/gather/gatherers/seo/robots-txt.js +3 -0
  107. package/core/gather/gatherers/stacks.d.ts +1 -1
  108. package/core/gather/gatherers/stacks.js +1 -3
  109. package/core/gather/gatherers/trace-elements.js +1 -3
  110. package/core/gather/gatherers/viewport-dimensions.js +0 -2
  111. package/core/gather/navigation-runner.d.ts +3 -1
  112. package/core/gather/navigation-runner.js +6 -3
  113. package/core/gather/session.js +1 -0
  114. package/core/index.cjs +5 -5
  115. package/core/index.d.cts +5 -5
  116. package/core/lib/arbitrary-equality-map.d.ts +2 -2
  117. package/core/lib/arbitrary-equality-map.js +1 -1
  118. package/core/lib/baseline/web-features-data.json +52 -28
  119. package/core/lib/baseline/web-features-metadata.json +1 -1
  120. package/core/lib/deprecation-description.js +1 -1
  121. package/core/lib/page-functions.d.ts +0 -3
  122. package/core/lib/page-functions.js +0 -3
  123. package/core/lib/rect-helpers.js +0 -8
  124. package/core/runner.js +21 -0
  125. package/core/user-flow.d.ts +3 -0
  126. package/core/user-flow.js +12 -0
  127. package/dist/report/bundle.esm.js +10 -3
  128. package/dist/report/flow.js +20 -13
  129. package/dist/report/standalone.js +10 -3
  130. package/package.json +19 -14
  131. package/report/assets/styles.css +7 -0
  132. package/report/renderer/components.js +1 -1
  133. package/report/renderer/details-renderer.js +7 -5
  134. package/report/renderer/drop-down-menu.d.ts +2 -2
  135. package/report/renderer/drop-down-menu.js +1 -1
  136. package/report/renderer/report-renderer.js +24 -3
  137. package/report/renderer/report-utils.js +1 -1
  138. package/report/renderer/snippet-renderer.d.ts +4 -4
  139. package/report/renderer/snippet-renderer.js +3 -3
  140. package/report/renderer/text-encoding.js +2 -2
  141. package/shared/localization/locales/ar-XB.json +111 -72
  142. package/shared/localization/locales/ar.json +113 -74
  143. package/shared/localization/locales/bg.json +112 -73
  144. package/shared/localization/locales/ca.json +112 -73
  145. package/shared/localization/locales/cs.json +112 -73
  146. package/shared/localization/locales/da.json +112 -73
  147. package/shared/localization/locales/de.json +112 -73
  148. package/shared/localization/locales/el.json +112 -73
  149. package/shared/localization/locales/en-GB.json +111 -72
  150. package/shared/localization/locales/en-US.json +102 -66
  151. package/shared/localization/locales/en-XA.json +111 -72
  152. package/shared/localization/locales/en-XL.json +102 -66
  153. package/shared/localization/locales/es-419.json +112 -73
  154. package/shared/localization/locales/es.json +112 -73
  155. package/shared/localization/locales/fi.json +112 -73
  156. package/shared/localization/locales/fil.json +112 -73
  157. package/shared/localization/locales/fr.json +112 -73
  158. package/shared/localization/locales/he.json +112 -73
  159. package/shared/localization/locales/hi.json +112 -73
  160. package/shared/localization/locales/hr.json +112 -73
  161. package/shared/localization/locales/hu.json +112 -73
  162. package/shared/localization/locales/id.json +112 -73
  163. package/shared/localization/locales/it.json +113 -74
  164. package/shared/localization/locales/ja.json +112 -73
  165. package/shared/localization/locales/ko.json +112 -73
  166. package/shared/localization/locales/lt.json +112 -73
  167. package/shared/localization/locales/lv.json +112 -73
  168. package/shared/localization/locales/nl.json +112 -73
  169. package/shared/localization/locales/no.json +112 -73
  170. package/shared/localization/locales/pl.json +112 -73
  171. package/shared/localization/locales/pt-PT.json +112 -73
  172. package/shared/localization/locales/pt.json +112 -73
  173. package/shared/localization/locales/ro.json +112 -73
  174. package/shared/localization/locales/ru.json +112 -73
  175. package/shared/localization/locales/sk.json +112 -73
  176. package/shared/localization/locales/sl.json +112 -73
  177. package/shared/localization/locales/sr-Latn.json +112 -73
  178. package/shared/localization/locales/sr.json +112 -73
  179. package/shared/localization/locales/sv.json +124 -85
  180. package/shared/localization/locales/ta.json +112 -73
  181. package/shared/localization/locales/te.json +112 -73
  182. package/shared/localization/locales/th.json +112 -73
  183. package/shared/localization/locales/tr.json +113 -74
  184. package/shared/localization/locales/uk.json +112 -73
  185. package/shared/localization/locales/vi.json +112 -73
  186. package/shared/localization/locales/zh-HK.json +112 -73
  187. package/shared/localization/locales/zh-TW.json +113 -74
  188. package/shared/localization/locales/zh.json +113 -74
  189. package/shared/util.d.ts +6 -0
  190. package/shared/util.js +9 -0
  191. package/third-party/ard/LICENSE +201 -0
  192. package/third-party/ard/README.md +27 -0
  193. package/third-party/ard/ard-test.d.ts +2 -0
  194. package/third-party/ard/ard-test.js +358 -0
  195. package/third-party/ard/ard.d.ts +34 -0
  196. package/third-party/ard/ard.js +204 -0
  197. package/third-party/ard/schema-validator.d.ts +20 -0
  198. package/third-party/ard/schema-validator.js +1700 -0
  199. package/third-party/ard/spec/schemas/ai-catalog.schema.json +292 -0
  200. package/tsconfig.json +1 -0
  201. package/types/artifacts.d.ts +14 -0
@@ -76,7 +76,7 @@ class TraceEngineResult {
76
76
  *
77
77
  * @template {any[]} Args
78
78
  * @template {import('../lib/trace-engine.js').DevToolsIcuMessage} Ret
79
- * @param {ReturnType<i18n.createIcuMessageFn>} str_
79
+ * @param {ReturnType<typeof i18n.createIcuMessageFn>} str_
80
80
  * @param {(...args: Args) => Ret} fn
81
81
  * @return {(...args: Args) => LH.IcuMessage}
82
82
  */
@@ -88,7 +88,7 @@ class TraceEngineResult {
88
88
  * Converts the input parameters given to `i18nString` usages in DevTools to a
89
89
  * LH.IcuMessage.
90
90
  *
91
- * @param {ReturnType<i18n.createIcuMessageFn>} str_
91
+ * @param {ReturnType<typeof i18n.createIcuMessageFn>} str_
92
92
  * @param {import('../lib/trace-engine.js').DevToolsIcuMessage} traceEngineI18nObject
93
93
  * @return {LH.IcuMessage}
94
94
  */
@@ -121,7 +121,7 @@ class TraceEngineResult {
121
121
  /**
122
122
  * Recursively finds all DevToolsIcuMessage objects and replaces them with LH.IcuMessage.
123
123
  *
124
- * @param {ReturnType<i18n.createIcuMessageFn>} str_
124
+ * @param {ReturnType<typeof i18n.createIcuMessageFn>} str_
125
125
  * @param {object} object
126
126
  */
127
127
  static localizeObject(str_, object) {
@@ -133,7 +133,7 @@ class TraceEngineResult {
133
133
  * @param {Set<object>} seen
134
134
  */
135
135
  function recursiveReplaceLocalizableStrings(obj, cb, seen) {
136
- if (seen.has(seen)) {
136
+ if (seen.has(obj)) {
137
137
  return;
138
138
  }
139
139
 
@@ -39,7 +39,6 @@ declare const UnusedJavascriptSummaryComputed: typeof UnusedJavascriptSummary &
39
39
  * @property {string} scriptId
40
40
  * @property {number} wastedBytes
41
41
  * @property {number} totalBytes
42
- * @property {number} wastedBytes
43
42
  * @property {number=} wastedPercent
44
43
  * @property {Record<string, number>=} sourcesWastedBytes Keyed by file name. Includes (unmapped) key too.
45
44
  */
@@ -24,7 +24,6 @@ import {makeComputedArtifact} from './computed-artifact.js';
24
24
  * @property {string} scriptId
25
25
  * @property {number} wastedBytes
26
26
  * @property {number} totalBytes
27
- * @property {number} wastedBytes
28
27
  * @property {number=} wastedPercent
29
28
  * @property {Record<string, number>=} sourcesWastedBytes Keyed by file name. Includes (unmapped) key too.
30
29
  */
@@ -8,6 +8,8 @@ export namespace UIStrings {
8
8
  let webmcpGroupDescription: string;
9
9
  let agentAccessibilityGroupTitle: string;
10
10
  let agentAccessibilityGroupDescription: string;
11
+ let agentDiscoverabilityGroupTitle: string;
12
+ let agentDiscoverabilityGroupDescription: string;
11
13
  }
12
14
  import * as LH from '../../types/lh.js';
13
15
  //# sourceMappingURL=agentic-browsing-config.d.ts.map
@@ -23,6 +23,11 @@ const UIStrings = {
23
23
  /** Description of the Agent Accessibility group of audits. */
24
24
  agentAccessibilityGroupDescription: 'These audits highlight best practices for improving the ' +
25
25
  'accessibility of the website for AI agents.',
26
+ /** Title of the Agent Discoverability group of audits. */
27
+ agentDiscoverabilityGroupTitle: 'Agent Discoverability',
28
+ /** Description of the Agent Discoverability group of audits. */
29
+ agentDiscoverabilityGroupDescription: 'These audits validate that websites expose ' +
30
+ 'discoverable resources, documentation, and catalogs for AI agents.',
26
31
  };
27
32
 
28
33
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
@@ -36,11 +41,13 @@ const config = {
36
41
  'webmcp-form-coverage',
37
42
  'webmcp-schema-validity',
38
43
  'agentic/llms-txt',
44
+ 'agentic/ard-schema',
39
45
  ],
40
46
  artifacts: [
41
47
  {id: 'WebMCP', gatherer: 'webmcp'},
42
48
  {id: 'WebMcpSchemaIssues', gatherer: 'webmcp-schema'},
43
49
  {id: 'LlmsTxt', gatherer: 'agentic/llms-txt'},
50
+ {id: 'AgentResourceDiscovery', gatherer: 'agentic/ard'},
44
51
  ],
45
52
  groups: {
46
53
  'webmcp': {
@@ -51,6 +58,10 @@ const config = {
51
58
  title: str_(UIStrings.agentAccessibilityGroupTitle),
52
59
  description: str_(UIStrings.agentAccessibilityGroupDescription),
53
60
  },
61
+ 'agent-discoverability': {
62
+ title: str_(UIStrings.agentDiscoverabilityGroupTitle),
63
+ description: str_(UIStrings.agentDiscoverabilityGroupDescription),
64
+ },
54
65
  },
55
66
  categories: {
56
67
  'agentic-browsing': {
@@ -64,7 +75,8 @@ const config = {
64
75
  {id: 'webmcp-registered-tools', weight: 1, group: 'webmcp'},
65
76
  {id: 'webmcp-schema-validity', weight: 1, group: 'webmcp'},
66
77
  {id: 'cumulative-layout-shift', weight: 1, acronym: 'CLS'},
67
- {id: 'llms-txt', weight: 1, group: 'agent-accessibility'},
78
+ {id: 'llms-txt', weight: 1, group: 'agent-discoverability'},
79
+ {id: 'ard-schema', weight: 1, group: 'agent-discoverability'},
68
80
  ],
69
81
  },
70
82
  },
@@ -115,6 +115,11 @@ const UIStrings = {
115
115
  /** Description of the Agent Accessibility group of audits. */
116
116
  agentAccessibilityGroupDescription: 'These audits highlight best practices for improving the ' +
117
117
  'accessibility of the website for AI agents.',
118
+ /** Title of the Agent Discoverability group of audits. */
119
+ agentDiscoverabilityGroupTitle: 'Agent Discoverability',
120
+ /** Description of the Agent Discoverability group of audits. */
121
+ agentDiscoverabilityGroupDescription: 'These audits validate that websites expose discoverable resources, ' +
122
+ 'documentation, and catalogs for AI agents.',
118
123
  };
119
124
 
120
125
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
@@ -145,6 +150,7 @@ const defaultConfig = {
145
150
  {id: 'MetaElements', gatherer: 'meta-elements'},
146
151
  {id: 'NetworkUserAgent', gatherer: 'network-user-agent'},
147
152
  {id: 'RobotsTxt', gatherer: 'seo/robots-txt'},
153
+ {id: 'AgentResourceDiscovery', gatherer: 'agentic/ard'},
148
154
  {id: 'Scripts', gatherer: 'scripts'},
149
155
  {id: 'SourceMaps', gatherer: 'source-maps'},
150
156
  {id: 'Stacks', gatherer: 'stacks'},
@@ -302,6 +308,7 @@ const defaultConfig = {
302
308
  'webmcp-form-coverage',
303
309
  'webmcp-schema-validity',
304
310
  'agentic/llms-txt',
311
+ 'agentic/ard-schema',
305
312
  'bf-cache',
306
313
  'insights/cache-insight',
307
314
  'insights/cls-culprits-insight',
@@ -397,6 +404,10 @@ const defaultConfig = {
397
404
  title: str_(UIStrings.agentAccessibilityGroupTitle),
398
405
  description: str_(UIStrings.agentAccessibilityGroupDescription),
399
406
  },
407
+ 'agent-discoverability': {
408
+ title: str_(UIStrings.agentDiscoverabilityGroupTitle),
409
+ description: str_(UIStrings.agentDiscoverabilityGroupDescription),
410
+ },
400
411
  // Group for audits that should not be displayed.
401
412
  'hidden': {title: ''},
402
413
  },
@@ -643,7 +654,8 @@ const defaultConfig = {
643
654
  {id: 'webmcp-registered-tools', weight: 1, group: 'webmcp'},
644
655
  {id: 'webmcp-schema-validity', weight: 1, group: 'webmcp'},
645
656
  {id: 'cumulative-layout-shift', weight: 1, acronym: 'CLS'},
646
- {id: 'llms-txt', weight: 1, group: 'agent-accessibility'},
657
+ {id: 'llms-txt', weight: 1, group: 'agent-discoverability'},
658
+ {id: 'ard-schema', weight: 1, group: 'agent-discoverability'},
647
659
  ],
648
660
  },
649
661
  },
@@ -8,6 +8,7 @@ import * as LH from '../../types/lh.js';
8
8
  import {Audit} from '../audits/audit.js';
9
9
  import BaseGatherer from '../gather/base-gatherer.js';
10
10
  import * as i18n from '../lib/i18n/i18n.js';
11
+ import {Util} from '../../shared/util.js';
11
12
 
12
13
  /**
13
14
  * Determines if the artifact dependency direction is valid. The dependency's minimum supported mode
@@ -37,6 +38,7 @@ function isValidArtifactDependency(dependent, dependency) {
37
38
  * @param {string} pluginName
38
39
  */
39
40
  function assertValidPluginName(config, pluginName) {
41
+ const originalPluginName = pluginName;
40
42
  const parts = pluginName.split('/');
41
43
  if (parts.length === 2) {
42
44
  pluginName = parts[1];
@@ -48,6 +50,10 @@ function assertValidPluginName(config, pluginName) {
48
50
  if (config.categories?.[pluginName]) {
49
51
  throw new Error(`plugin name '${pluginName}' not allowed because it is the id of a category already found in config`); // eslint-disable-line max-len
50
52
  }
53
+
54
+ if (!Util.isPluginCategory(originalPluginName)) {
55
+ throw new Error(`plugin name '${originalPluginName}' has invalid npm package name characters`);
56
+ }
51
57
  }
52
58
 
53
59
  /**
@@ -320,14 +320,12 @@ class ExecutionContext {
320
320
  */
321
321
  async cacheNativesOnNewDocument() {
322
322
  await this.evaluateOnNewDocument(() => {
323
- /* c8 ignore start */
324
323
  window.__nativePromise = window.Promise;
325
324
  window.__nativeURL = window.URL;
326
325
  window.__nativePerformance = window.performance;
327
326
  window.__nativeFetch = window.fetch;
328
327
  window.__ElementMatches = window.Element.prototype.matches;
329
328
  window.__HTMLElementBoundingClientRect = window.HTMLElement.prototype.getBoundingClientRect;
330
- /* c8 ignore stop */
331
329
  }, {args: []});
332
330
  }
333
331
 
@@ -1,5 +1,6 @@
1
1
  export type NavigationOptions = {
2
2
  waitUntil: Array<"fcp" | "load" | "navigated">;
3
+ signal?: AbortSignal;
3
4
  } & Partial<LH.Config.Settings>;
4
5
  /**
5
6
  * Navigates to the given URL, assuming that the page is not already on this URL.
@@ -39,7 +39,7 @@ const DEFAULT_NETWORK_QUIET_THRESHOLD = 5000;
39
39
  // Controls how long to wait between longtasks before determining the CPU is idle, off by default
40
40
  const DEFAULT_CPU_QUIET_THRESHOLD = 0;
41
41
 
42
- /** @typedef {{waitUntil: Array<'fcp'|'load'|'navigated'>} & Partial<LH.Config.Settings>} NavigationOptions */
42
+ /** @typedef {{waitUntil: Array<'fcp'|'load'|'navigated'>, signal?: AbortSignal} & Partial<LH.Config.Settings>} NavigationOptions */
43
43
 
44
44
  /** @param {NavigationOptions} options */
45
45
  function resolveWaitForFullyLoadedOptions(options) {
@@ -107,23 +107,49 @@ async function gotoURL(driver, requestor, options) {
107
107
 
108
108
  /** @type {Array<Promise<{timedOut: boolean}>>} */
109
109
  const waitConditionPromises = [];
110
+ /** @type {Array<() => void>} */
111
+ const cleanups = [];
112
+
113
+ /** @type {(event: Event) => void} */
114
+ let onAbort = () => {};
115
+ /** @type {Promise<never>} */
116
+ const abortPromise = new Promise((_, reject) => {
117
+ if (!options.signal) return;
118
+ options.signal.throwIfAborted();
119
+ onAbort = () => reject(new Error('Navigation aborted by signal'));
120
+ options.signal.addEventListener('abort', onAbort, {once: true});
121
+ });
122
+ if (options.signal) cleanups.push(() => options.signal?.removeEventListener('abort', onAbort));
110
123
 
111
124
  if (waitForNavigated) {
112
- const navigatedPromise = waitForFrameNavigated(session).promise;
113
- waitConditionPromises.push(navigatedPromise.then(() => ({timedOut: false})));
125
+ const {promise, cancel} = waitForFrameNavigated(session);
126
+ cleanups.push(() => cancel());
127
+ waitConditionPromises.push(promise
128
+ .then(() => ({timedOut: false}))
129
+ .catch(() => ({timedOut: false})));
114
130
  }
115
131
 
116
132
  if (waitForLoad) {
117
133
  const waitOptions = resolveWaitForFullyLoadedOptions(options);
118
- waitConditionPromises.push(waitForFullyLoaded(session, networkMonitor, waitOptions));
134
+ waitConditionPromises.push(
135
+ waitForFullyLoaded(session, networkMonitor, waitOptions, options.signal)
136
+ );
119
137
  } else if (waitForFcp) {
120
138
  throw new Error('Cannot wait for FCP without waiting for page load');
121
139
  }
122
140
 
123
- const waitConditions = await Promise.race([
141
+ const waitConditionsRace = [
124
142
  session.onCrashPromise(),
125
143
  Promise.all(waitConditionPromises),
126
- ]);
144
+ ];
145
+ if (options.signal) waitConditionsRace.push(abortPromise);
146
+
147
+ let waitConditions;
148
+ try {
149
+ waitConditions = await Promise.race(waitConditionsRace);
150
+ } finally {
151
+ for (const cleanup of cleanups) cleanup();
152
+ }
127
153
  const timedOut = waitConditions.some(condition => condition.timedOut);
128
154
  const navigationUrls = await networkMonitor.getNavigationUrls();
129
155
 
@@ -142,7 +168,9 @@ async function gotoURL(driver, requestor, options) {
142
168
  const mainDocumentUrl = navigationUrls.mainDocumentUrl || requestedUrl;
143
169
 
144
170
  // Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
145
- await waitForNavigationTriggered;
171
+ await Promise.race(options.signal ?
172
+ [waitForNavigationTriggered, abortPromise] :
173
+ [waitForNavigationTriggered]);
146
174
 
147
175
  if (options.debugNavigation) {
148
176
  await waitForUserToContinue(driver);
@@ -18,7 +18,7 @@ export type WaitOptions = {
18
18
  waitForCPUIdle: typeof waitForCPUIdle;
19
19
  } | undefined;
20
20
  };
21
- /** @typedef {InstanceType<import('./network-monitor.js')['NetworkMonitor']>} NetworkMonitor */
21
+ /** @typedef {InstanceType<typeof import('./network-monitor.js')['NetworkMonitor']>} NetworkMonitor */
22
22
  /** @typedef {import('./network-monitor.js').NetworkMonitorEvent} NetworkMonitorEvent */
23
23
  /**
24
24
  * @template [T=void]
@@ -40,12 +40,9 @@ export type WaitOptions = {
40
40
  * Returns a promise that resolves immediately.
41
41
  * Used for placeholder conditions that we don't want to start waiting for just yet, but still want
42
42
  * to satisfy the same interface.
43
- * @return {{promise: Promise<void>, cancel: function(): void}}
43
+ * @return {CancellableWait<void>}
44
44
  */
45
- export function waitForNothing(): {
46
- promise: Promise<void>;
47
- cancel: () => void;
48
- };
45
+ export function waitForNothing(): CancellableWait<void>;
49
46
  /**
50
47
  * Returns a promise that resolve when a frame has been navigated.
51
48
  * Used for detecting that our about:blank reset has been completed.
@@ -104,9 +101,10 @@ export function waitForCPUIdle(session: LH.Gatherer.ProtocolSession, waitForCPUQ
104
101
  * @param {LH.Gatherer.ProtocolSession} session
105
102
  * @param {NetworkMonitor} networkMonitor
106
103
  * @param {WaitOptions} options
104
+ * @param {AbortSignal} [signal]
107
105
  * @return {Promise<{timedOut: boolean}>}
108
106
  */
109
- export function waitForFullyLoaded(session: LH.Gatherer.ProtocolSession, networkMonitor: NetworkMonitor, options: WaitOptions): Promise<{
107
+ export function waitForFullyLoaded(session: LH.Gatherer.ProtocolSession, networkMonitor: NetworkMonitor, options: WaitOptions, signal?: AbortSignal): Promise<{
110
108
  timedOut: boolean;
111
109
  }>;
112
110
  /**
@@ -11,7 +11,7 @@ import log from 'lighthouse-logger';
11
11
  import {LighthouseError} from '../../lib/lh-error.js';
12
12
  import {ExecutionContext} from './execution-context.js';
13
13
 
14
- /** @typedef {InstanceType<import('./network-monitor.js')['NetworkMonitor']>} NetworkMonitor */
14
+ /** @typedef {InstanceType<typeof import('./network-monitor.js')['NetworkMonitor']>} NetworkMonitor */
15
15
  /** @typedef {import('./network-monitor.js').NetworkMonitorEvent} NetworkMonitorEvent */
16
16
 
17
17
  /**
@@ -36,7 +36,7 @@ import {ExecutionContext} from './execution-context.js';
36
36
  * Returns a promise that resolves immediately.
37
37
  * Used for placeholder conditions that we don't want to start waiting for just yet, but still want
38
38
  * to satisfy the same interface.
39
- * @return {{promise: Promise<void>, cancel: function(): void}}
39
+ * @return {CancellableWait<void>}
40
40
  */
41
41
  function waitForNothing() {
42
42
  return {promise: Promise.resolve(), cancel() {}};
@@ -277,8 +277,6 @@ function waitForCPUIdle(session, waitForCPUQuiet) {
277
277
  };
278
278
  }
279
279
 
280
- /* c8 ignore start */
281
-
282
280
  /**
283
281
  * This function is executed in the page itself when the document is first loaded.
284
282
  *
@@ -338,8 +336,6 @@ function checkTimeSinceLastLongTaskInPage() {
338
336
  });
339
337
  }
340
338
 
341
- /* c8 ignore stop */
342
-
343
339
  /**
344
340
  * Return a promise that resolves `pauseAfterLoadMs` after the load event
345
341
  * fires and a method to cancel internal listeners and timeout.
@@ -422,9 +418,10 @@ const DEFAULT_WAIT_FUNCTIONS = {waitForFcp, waitForLoadEvent, waitForCPUIdle, wa
422
418
  * @param {LH.Gatherer.ProtocolSession} session
423
419
  * @param {NetworkMonitor} networkMonitor
424
420
  * @param {WaitOptions} options
421
+ * @param {AbortSignal} [signal]
425
422
  * @return {Promise<{timedOut: boolean}>}
426
423
  */
427
- async function waitForFullyLoaded(session, networkMonitor, options) {
424
+ async function waitForFullyLoaded(session, networkMonitor, options, signal) {
428
425
  const {pauseAfterFcpMs, pauseAfterLoadMs, networkQuietThresholdMs,
429
426
  cpuQuietThresholdMs, maxWaitForLoadedMs, maxWaitForFcpMs} = options;
430
427
  const {waitForFcp, waitForLoadEvent, waitForNetworkIdle, waitForCPUIdle} =
@@ -528,12 +525,23 @@ async function waitForFullyLoaded(session, networkMonitor, options) {
528
525
  };
529
526
  });
530
527
 
528
+ /** @type {(event: Event) => void} */
529
+ let onAbort = () => {};
530
+ /** @type {Promise<() => Promise<{timedOut: boolean}>>} */
531
+ const abortPromise = new Promise((resolve) => {
532
+ signal?.throwIfAborted();
533
+ onAbort = () => resolve(async () => ({timedOut: false}));
534
+ signal?.addEventListener('abort', onAbort, {once: true});
535
+ });
536
+
531
537
  // Wait for load or timeout and run the cleanup function the winner returns.
532
538
  const cleanupFn = await Promise.race([
533
539
  loadPromise,
534
540
  maxTimeoutPromise,
541
+ abortPromise,
535
542
  ]);
536
543
 
544
+ signal?.removeEventListener('abort', onAbort);
537
545
  maxTimeoutHandle && clearTimeout(maxTimeoutHandle);
538
546
  resolveOnFcp.cancel();
539
547
  resolveOnLoadEvent.cancel();
@@ -547,7 +555,6 @@ async function waitForFullyLoaded(session, networkMonitor, options) {
547
555
  * @param {LH.Gatherer.Driver} driver
548
556
  */
549
557
  function waitForUserToContinue(driver) {
550
- /* c8 ignore start */
551
558
  function createInPagePromise() {
552
559
  let resolve = () => {};
553
560
  /** @type {Promise<void>} */
@@ -563,7 +570,6 @@ function waitForUserToContinue(driver) {
563
570
  window.continueLighthouseRun = resolve;
564
571
  return promise;
565
572
  }
566
- /* c8 ignore stop */
567
573
 
568
574
  driver.defaultSession.setNextProtocolTimeout(Infinity);
569
575
  return driver.executionContext.evaluate(createInPagePromise, {args: []});
@@ -1,12 +1,13 @@
1
1
  export type FetchResponse = {
2
2
  content: string | null;
3
3
  status: number | null;
4
+ headers?: Record<string, string> | null;
4
5
  };
5
6
  /**
6
7
  * @fileoverview Fetcher is a utility for making requests to any arbitrary resource,
7
8
  * ignoring normal browser constraints such as CORS.
8
9
  */
9
- /** @typedef {{content: string|null, status: number|null}} FetchResponse */
10
+ /** @typedef {{content: string|null, status: number|null, headers?: Record<string, string>|null}} FetchResponse */
10
11
  export class Fetcher {
11
12
  /**
12
13
  * @param {LH.Gatherer.ProtocolSession} session
@@ -23,6 +24,11 @@ export class Fetcher {
23
24
  fetchResource(url: string, options?: {
24
25
  timeout: number;
25
26
  } | undefined): Promise<FetchResponse>;
27
+ /**
28
+ * @param {Headers|Record<string, string|number|boolean>|null|undefined} rawHeaders
29
+ * @return {Record<string, string>|null}
30
+ */
31
+ _normalizeHeaders(rawHeaders: Headers | Record<string, string | number | boolean> | null | undefined): Record<string, string> | null;
26
32
  /**
27
33
  * @param {string} url
28
34
  * @return {Promise<FetchResponse>}
@@ -38,11 +44,12 @@ export class Fetcher {
38
44
  } | undefined): Promise<string>;
39
45
  /**
40
46
  * @param {string} url
41
- * @return {Promise<{stream: LH.Crdp.IO.StreamHandle|null, status: number|null}>}
47
+ * @return {Promise<{stream: LH.Crdp.IO.StreamHandle|null, status: number|null, headers?: Record<string, string>|null}>}
42
48
  */
43
49
  _loadNetworkResource(url: string): Promise<{
44
50
  stream: LH.Crdp.IO.StreamHandle | null;
45
51
  status: number | null;
52
+ headers?: Record<string, string> | null;
46
53
  }>;
47
54
  /**
48
55
  * @param {string} url
@@ -10,7 +10,7 @@ import * as LH from '../../types/lh.js';
10
10
  * ignoring normal browser constraints such as CORS.
11
11
  */
12
12
 
13
- /** @typedef {{content: string|null, status: number|null}} FetchResponse */
13
+ /** @typedef {{content: string|null, status: number|null, headers?: Record<string, string>|null}} FetchResponse */
14
14
 
15
15
  class Fetcher {
16
16
  /**
@@ -37,6 +37,26 @@ class Fetcher {
37
37
  return this._fetchResourceOverProtocol(url, options);
38
38
  }
39
39
 
40
+ /**
41
+ * @param {Headers|Record<string, string|number|boolean>|null|undefined} rawHeaders
42
+ * @return {Record<string, string>|null}
43
+ */
44
+ _normalizeHeaders(rawHeaders) {
45
+ if (!rawHeaders) return null;
46
+
47
+ /** @type {Record<string, string>} */
48
+ const headers = {};
49
+ const entries = typeof /** @type {Headers} */ (rawHeaders).entries === 'function' ?
50
+ /** @type {Headers} */ (rawHeaders).entries() :
51
+ Object.entries(rawHeaders);
52
+
53
+ for (const [key, value] of entries) {
54
+ headers[key.toLowerCase()] = String(value);
55
+ }
56
+
57
+ return headers;
58
+ }
59
+
40
60
  /**
41
61
  * @param {string} url
42
62
  * @return {Promise<FetchResponse>}
@@ -49,9 +69,12 @@ class Fetcher {
49
69
  content = await response.text();
50
70
  } catch {}
51
71
 
72
+ const headers = this._normalizeHeaders(response.headers);
73
+
52
74
  return {
53
75
  content,
54
76
  status: response.status,
77
+ headers,
55
78
  };
56
79
  }
57
80
 
@@ -82,7 +105,7 @@ class Fetcher {
82
105
 
83
106
  /**
84
107
  * @param {string} url
85
- * @return {Promise<{stream: LH.Crdp.IO.StreamHandle|null, status: number|null}>}
108
+ * @return {Promise<{stream: LH.Crdp.IO.StreamHandle|null, status: number|null, headers?: Record<string, string>|null}>}
86
109
  */
87
110
  async _loadNetworkResource(url) {
88
111
  const frameTreeResponse = await this.session.sendCommand('Page.getFrameTree');
@@ -98,6 +121,7 @@ class Fetcher {
98
121
  return {
99
122
  stream: networkResponse.resource.success ? (networkResponse.resource.stream || null) : null,
100
123
  status: networkResponse.resource.httpStatusCode || null,
124
+ headers: networkResponse.resource.headers || null,
101
125
  };
102
126
  }
103
127
 
@@ -110,12 +134,14 @@ class Fetcher {
110
134
  const startTime = Date.now();
111
135
  const response = await this._wrapWithTimeout(this._loadNetworkResource(url), options.timeout);
112
136
 
137
+ const headers = this._normalizeHeaders(response.headers);
138
+
113
139
  const isOk = response.status && response.status >= 200 && response.status <= 299;
114
- if (!response.stream || !isOk) return {status: response.status, content: null};
140
+ if (!response.stream || !isOk) return {status: response.status, content: null, headers};
115
141
 
116
142
  const timeout = options.timeout - (Date.now() - startTime);
117
143
  const content = await this._readIOStream(response.stream, {timeout});
118
- return {status: response.status, content};
144
+ return {status: response.status, content, headers};
119
145
  }
120
146
 
121
147
  /**
@@ -13,9 +13,8 @@ import {pageFunctions} from '../../lib/page-functions.js';
13
13
  /**
14
14
  * @return {Promise<LH.Artifacts.Accessibility>}
15
15
  */
16
- /* c8 ignore start */
17
16
  async function runA11yChecks() {
18
- /** @type {import('axe-core/axe')} */
17
+ /** @type {typeof import('axe-core/axe')} */
19
18
  // @ts-expect-error - axe defined by axeLibSource
20
19
  const axe = window.axe;
21
20
  const application = `lighthouse-${Math.random()}`;
@@ -183,7 +182,6 @@ function createAxeRuleResultArtifact(result) {
183
182
  description: result.description,
184
183
  };
185
184
  }
186
- /* c8 ignore stop */
187
185
 
188
186
  class Accessibility extends BaseGatherer {
189
187
  /** @type {LH.Gatherer.GathererMeta} */
@@ -0,0 +1,30 @@
1
+ export default AgentResourceDiscovery;
2
+ declare class AgentResourceDiscovery extends BaseGatherer {
3
+ /**
4
+ * @param {LH.Artifacts['RobotsTxt']|null|undefined} robotsTxt
5
+ * @param {string} finalDisplayedUrl
6
+ * @return {string|null}
7
+ */
8
+ static getRobotsTxtAgentmap(robotsTxt: LH.Artifacts["RobotsTxt"] | null | undefined, finalDisplayedUrl: string): string | null;
9
+ /**
10
+ * @param {LH.Gatherer.Context} context
11
+ * @param {string} finalDisplayedUrl
12
+ * @return {Promise<string|null>}
13
+ */
14
+ static getHtmlLinkFromDom(context: LH.Gatherer.Context, finalDisplayedUrl: string): Promise<string | null>;
15
+ /**
16
+ * @param {LH.Gatherer.Context} context
17
+ * @param {string} finalDisplayedUrl
18
+ * @return {Promise<string|null>}
19
+ */
20
+ static getHttpHeaderLink(context: LH.Gatherer.Context, finalDisplayedUrl: string): Promise<string | null>;
21
+ /** @type {LH.Gatherer.GathererMeta<'RobotsTxt'>} */
22
+ meta: LH.Gatherer.GathererMeta<"RobotsTxt">;
23
+ /**
24
+ * @param {LH.Gatherer.Context<'RobotsTxt'>} context
25
+ * @return {Promise<LH.Artifacts['AgentResourceDiscovery']>}
26
+ */
27
+ getArtifact(context: LH.Gatherer.Context<"RobotsTxt">): Promise<LH.Artifacts["AgentResourceDiscovery"]>;
28
+ }
29
+ import BaseGatherer from '../../base-gatherer.js';
30
+ //# sourceMappingURL=ard.d.ts.map