lighthouse 10.2.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +4 -6
  2. package/core/audits/accessibility/html-xml-lang-mismatch.d.ts +10 -0
  3. package/core/audits/accessibility/html-xml-lang-mismatch.js +47 -0
  4. package/core/audits/accessibility/input-button-name.d.ts +10 -0
  5. package/core/audits/accessibility/input-button-name.js +44 -0
  6. package/core/audits/accessibility/table-fake-caption.d.ts +10 -0
  7. package/core/audits/accessibility/table-fake-caption.js +47 -0
  8. package/core/audits/accessibility/td-has-header.d.ts +10 -0
  9. package/core/audits/accessibility/td-has-header.js +45 -0
  10. package/core/audits/audit.d.ts +2 -1
  11. package/core/audits/audit.js +5 -2
  12. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +3 -3
  13. package/core/audits/byte-efficiency/byte-efficiency-audit.js +14 -19
  14. package/core/audits/deprecations.js +1 -1
  15. package/core/audits/largest-contentful-paint-element.d.ts +14 -2
  16. package/core/audits/largest-contentful-paint-element.js +49 -31
  17. package/core/audits/layout-shift-elements.d.ts +3 -2
  18. package/core/audits/layout-shift-elements.js +10 -2
  19. package/core/audits/metrics.js +0 -2
  20. package/core/audits/network-requests.js +6 -0
  21. package/core/audits/seo/crawlable-anchors.js +4 -0
  22. package/core/audits/server-response-time.d.ts +2 -1
  23. package/core/audits/server-response-time.js +17 -3
  24. package/core/audits/uses-rel-preconnect.js +20 -4
  25. package/core/computed/entity-classification.d.ts +14 -1
  26. package/core/computed/entity-classification.js +61 -6
  27. package/core/computed/metrics/cumulative-layout-shift.d.ts +2 -9
  28. package/core/computed/metrics/cumulative-layout-shift.js +3 -16
  29. package/core/computed/metrics/time-to-first-byte.js +10 -7
  30. package/core/computed/metrics/timing-summary.js +0 -3
  31. package/core/config/default-config.js +8 -0
  32. package/core/gather/driver/execution-context.js +13 -5
  33. package/core/gather/driver/target-manager.d.ts +13 -0
  34. package/core/gather/driver/target-manager.js +49 -0
  35. package/core/gather/gatherers/accessibility.js +19 -20
  36. package/core/gather/gatherers/anchor-elements.js +2 -0
  37. package/core/gather/gatherers/global-listeners.js +38 -21
  38. package/core/gather/gatherers/inspector-issues.js +2 -1
  39. package/core/gather/gatherers/trace-elements.d.ts +3 -2
  40. package/core/gather/gatherers/trace-elements.js +10 -19
  41. package/core/legacy/config/config.d.ts +1 -1
  42. package/core/legacy/config/config.js +1 -1
  43. package/core/legacy/gather/driver.d.ts +1 -0
  44. package/core/legacy/gather/driver.js +11 -0
  45. package/core/lib/asset-saver.d.ts +4 -0
  46. package/core/lib/asset-saver.js +19 -0
  47. package/core/lib/bf-cache-strings.d.ts +3 -0
  48. package/core/lib/bf-cache-strings.js +248 -232
  49. package/core/lib/cdt/generated/SourceMap.d.ts +18 -31
  50. package/core/lib/cdt/generated/SourceMap.js +51 -101
  51. package/core/lib/dependency-graph/simulator/network-analyzer.d.ts +6 -2
  52. package/core/lib/dependency-graph/simulator/network-analyzer.js +45 -13
  53. package/core/lib/deprecation-description.d.ts +17 -0
  54. package/core/lib/deprecation-description.js +71 -0
  55. package/core/lib/deprecations-strings.d.ts +282 -63
  56. package/core/lib/deprecations-strings.js +282 -551
  57. package/core/lib/lh-error.d.ts +9 -4
  58. package/core/lib/lh-error.js +15 -11
  59. package/core/lib/network-request.d.ts +13 -6
  60. package/core/lib/network-request.js +39 -14
  61. package/core/lib/tracehouse/trace-processor.js +1 -1
  62. package/core/lib/url-utils.js +4 -0
  63. package/core/runner.js +4 -2
  64. package/dist/report/bundle.esm.js +24 -7
  65. package/dist/report/flow.js +3 -3
  66. package/dist/report/standalone.js +8 -8
  67. package/package.json +12 -12
  68. package/readme.md +7 -0
  69. package/report/assets/styles.css +18 -2
  70. package/report/assets/templates.html +4 -0
  71. package/report/renderer/category-renderer.js +5 -2
  72. package/report/renderer/components.js +2 -2
  73. package/report/renderer/dom.d.ts +4 -1
  74. package/report/renderer/dom.js +3 -2
  75. package/shared/localization/locales/ar-XB.json +20 -170
  76. package/shared/localization/locales/ar.json +20 -170
  77. package/shared/localization/locales/bg.json +19 -169
  78. package/shared/localization/locales/ca.json +19 -169
  79. package/shared/localization/locales/cs.json +19 -169
  80. package/shared/localization/locales/da.json +19 -169
  81. package/shared/localization/locales/de.json +19 -169
  82. package/shared/localization/locales/el.json +19 -169
  83. package/shared/localization/locales/en-GB.json +9 -159
  84. package/shared/localization/locales/en-US.json +151 -70
  85. package/shared/localization/locales/en-XA.json +20 -170
  86. package/shared/localization/locales/en-XL.json +151 -70
  87. package/shared/localization/locales/es-419.json +19 -169
  88. package/shared/localization/locales/es.json +19 -169
  89. package/shared/localization/locales/fi.json +19 -169
  90. package/shared/localization/locales/fil.json +17 -167
  91. package/shared/localization/locales/fr.json +18 -168
  92. package/shared/localization/locales/he.json +19 -169
  93. package/shared/localization/locales/hi.json +20 -170
  94. package/shared/localization/locales/hr.json +19 -169
  95. package/shared/localization/locales/hu.json +19 -169
  96. package/shared/localization/locales/id.json +19 -169
  97. package/shared/localization/locales/it.json +19 -169
  98. package/shared/localization/locales/ja.json +19 -169
  99. package/shared/localization/locales/ko.json +19 -169
  100. package/shared/localization/locales/lt.json +20 -170
  101. package/shared/localization/locales/lv.json +20 -170
  102. package/shared/localization/locales/nl.json +18 -168
  103. package/shared/localization/locales/no.json +19 -169
  104. package/shared/localization/locales/pl.json +19 -169
  105. package/shared/localization/locales/pt-PT.json +19 -169
  106. package/shared/localization/locales/pt.json +19 -169
  107. package/shared/localization/locales/ro.json +19 -169
  108. package/shared/localization/locales/ru.json +19 -169
  109. package/shared/localization/locales/sk.json +20 -170
  110. package/shared/localization/locales/sl.json +19 -169
  111. package/shared/localization/locales/sr-Latn.json +19 -169
  112. package/shared/localization/locales/sr.json +19 -169
  113. package/shared/localization/locales/sv.json +19 -169
  114. package/shared/localization/locales/ta.json +19 -169
  115. package/shared/localization/locales/te.json +19 -169
  116. package/shared/localization/locales/th.json +19 -169
  117. package/shared/localization/locales/tr.json +19 -169
  118. package/shared/localization/locales/uk.json +19 -169
  119. package/shared/localization/locales/vi.json +20 -170
  120. package/shared/localization/locales/zh-HK.json +19 -169
  121. package/shared/localization/locales/zh-TW.json +19 -169
  122. package/shared/localization/locales/zh.json +19 -169
  123. package/shared/util.d.ts +6 -0
  124. package/shared/util.js +14 -1
  125. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +2 -1
  126. package/tsconfig-base.json +1 -1
  127. package/types/artifacts.d.ts +10 -8
  128. package/types/audit.d.ts +5 -0
  129. package/types/gatherer.d.ts +1 -0
  130. package/types/lhr/audit-details.d.ts +1 -0
  131. package/types/lhr/audit-result.d.ts +10 -0
  132. package/types/internal/es-main.d.ts +0 -18
@@ -2,8 +2,9 @@ export default ServerResponseTime;
2
2
  declare class ServerResponseTime extends Audit {
3
3
  /**
4
4
  * @param {LH.Artifacts.NetworkRequest} record
5
+ * @return {number|null}
5
6
  */
6
- static calculateResponseTime(record: LH.Artifacts.NetworkRequest): number;
7
+ static calculateResponseTime(record: LH.Artifacts.NetworkRequest): number | null;
7
8
  /**
8
9
  * @param {LH.Artifacts} artifacts
9
10
  * @param {LH.Audit.Context} context
@@ -43,10 +43,15 @@ class ServerResponseTime extends Audit {
43
43
 
44
44
  /**
45
45
  * @param {LH.Artifacts.NetworkRequest} record
46
+ * @return {number|null}
46
47
  */
47
48
  static calculateResponseTime(record) {
48
- const timing = record.timing;
49
- return timing ? timing.receiveHeadersEnd - timing.sendEnd : 0;
49
+ // Lightrider does not have timings for sendEnd, but we do have this timing which should be
50
+ // close to the response time.
51
+ if (global.isLightrider && record.lrStatistics) return record.lrStatistics.requestMs;
52
+
53
+ if (!record.timing) return null;
54
+ return record.timing.receiveHeadersStart - record.timing.sendEnd;
50
55
  }
51
56
 
52
57
  /**
@@ -61,6 +66,10 @@ class ServerResponseTime extends Audit {
61
66
  const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
62
67
 
63
68
  const responseTime = ServerResponseTime.calculateResponseTime(mainResource);
69
+ if (responseTime === null) {
70
+ throw new Error('no timing found for main resource');
71
+ }
72
+
64
73
  const passed = responseTime < TOO_SLOW_THRESHOLD_MS;
65
74
  const displayValue = str_(UIStrings.displayValue, {timeInMs: responseTime});
66
75
 
@@ -70,10 +79,11 @@ class ServerResponseTime extends Audit {
70
79
  {key: 'responseTime', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnTimeSpent)},
71
80
  ];
72
81
 
82
+ const overallSavingsMs = Math.max(responseTime - TARGET_MS, 0);
73
83
  const details = Audit.makeOpportunityDetails(
74
84
  headings,
75
85
  [{url: mainResource.url, responseTime}],
76
- {overallSavingsMs: responseTime - TARGET_MS}
86
+ {overallSavingsMs}
77
87
  );
78
88
 
79
89
  return {
@@ -82,6 +92,10 @@ class ServerResponseTime extends Audit {
82
92
  score: Number(passed),
83
93
  displayValue,
84
94
  details,
95
+ metricSavings: {
96
+ FCP: overallSavingsMs,
97
+ LCP: overallSavingsMs,
98
+ },
85
99
  };
86
100
  }
87
101
  }
@@ -72,7 +72,7 @@ class UsesRelPreconnectAudit extends Audit {
72
72
  * @return {boolean}
73
73
  */
74
74
  static hasValidTiming(record) {
75
- return !!record.timing && record.timing.connectEnd > 0 && record.timing.connectStart > 0;
75
+ return !!record.timing && record.timing.connectEnd >= 0 && record.timing.connectStart >= 0;
76
76
  }
77
77
 
78
78
  /**
@@ -81,11 +81,27 @@ class UsesRelPreconnectAudit extends Audit {
81
81
  * @return {boolean}
82
82
  */
83
83
  static hasAlreadyConnectedToOrigin(record) {
84
- return (
85
- !!record.timing &&
84
+ if (!record.timing) return false;
85
+
86
+ // When these values are given as -1, that means the page has
87
+ // a connection for this origin and paid these costs already.
88
+ if (
89
+ record.timing.dnsStart === -1 && record.timing.dnsEnd === -1 &&
90
+ record.timing.connectStart === -1 && record.timing.connectEnd === -1
91
+ ) {
92
+ return true;
93
+ }
94
+
95
+ // Less understood: if the connection setup took no time at all, consider
96
+ // it the same as the above. It is unclear if this is correct, or is even possible.
97
+ if (
86
98
  record.timing.dnsEnd - record.timing.dnsStart === 0 &&
87
99
  record.timing.connectEnd - record.timing.connectStart === 0
88
- );
100
+ ) {
101
+ return true;
102
+ }
103
+
104
+ return false;
89
105
  }
90
106
 
91
107
  /**
@@ -10,12 +10,25 @@ declare const EntityClassificationComputed: typeof EntityClassification & {
10
10
  };
11
11
  /** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */
12
12
  declare class EntityClassification {
13
+ /**
14
+ * @param {EntityCache} entityCache
15
+ * @param {string} url
16
+ * @param {string=} extensionName
17
+ * @return {LH.Artifacts.Entity}
18
+ */
19
+ static makeupChromeExtensionEntity_(entityCache: EntityCache, url: string, extensionName?: string | undefined): LH.Artifacts.Entity;
13
20
  /**
14
21
  * @param {EntityCache} entityCache
15
22
  * @param {string} url
16
23
  * @return {LH.Artifacts.Entity | undefined}
17
24
  */
18
- static makeUpAnEntity(entityCache: EntityCache, url: string): LH.Artifacts.Entity | undefined;
25
+ static _makeUpAnEntity(entityCache: EntityCache, url: string): LH.Artifacts.Entity | undefined;
26
+ /**
27
+ * Preload Chrome extensions found in the devtoolsLog into cache.
28
+ * @param {EntityCache} entityCache
29
+ * @param {LH.DevtoolsLog} devtoolsLog
30
+ */
31
+ static _preloadChromeExtensionsToCache(entityCache: EntityCache, devtoolsLog: import("../index.js").DevtoolsLog): void;
19
32
  /**
20
33
  * @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
21
34
  * @param {LH.Artifacts.ComputedContext} context
@@ -13,16 +13,51 @@ import thirdPartyWeb from '../lib/third-party-web.js';
13
13
  /** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */
14
14
 
15
15
  class EntityClassification {
16
+ /**
17
+ * @param {EntityCache} entityCache
18
+ * @param {string} url
19
+ * @param {string=} extensionName
20
+ * @return {LH.Artifacts.Entity}
21
+ */
22
+ static makeupChromeExtensionEntity_(entityCache, url, extensionName) {
23
+ const origin = Util.getChromeExtensionOrigin(url);
24
+ const host = new URL(origin).host;
25
+ const name = extensionName || host;
26
+
27
+ const cachedEntity = entityCache.get(origin);
28
+ if (cachedEntity) return cachedEntity;
29
+
30
+ const chromeExtensionEntity = {
31
+ name,
32
+ company: name,
33
+ category: 'Chrome Extension',
34
+ homepage: 'https://chromewebstore.google.com/detail/' + host,
35
+ categories: [],
36
+ domains: [],
37
+ averageExecutionTime: 0,
38
+ totalExecutionTime: 0,
39
+ totalOccurrences: 0,
40
+ };
41
+
42
+ entityCache.set(origin, chromeExtensionEntity);
43
+ return chromeExtensionEntity;
44
+ }
45
+
16
46
  /**
17
47
  * @param {EntityCache} entityCache
18
48
  * @param {string} url
19
49
  * @return {LH.Artifacts.Entity | undefined}
20
50
  */
21
- static makeUpAnEntity(entityCache, url) {
51
+ static _makeUpAnEntity(entityCache, url) {
22
52
  if (!UrlUtils.isValid(url)) return;
23
- // We can make up an entity only for those URLs with a valid domain attached.
24
- // So we further restrict from allowed URLs to (http/https).
25
- if (!Util.createOrReturnURL(url).protocol.startsWith('http')) return;
53
+
54
+ const parsedUrl = Util.createOrReturnURL(url);
55
+ if (parsedUrl.protocol === 'chrome-extension:') {
56
+ return EntityClassification.makeupChromeExtensionEntity_(entityCache, url);
57
+ }
58
+
59
+ // Make up an entity only for valid http/https URLs.
60
+ if (!parsedUrl.protocol.startsWith('http')) return;
26
61
 
27
62
  const rootDomain = Util.getRootDomain(url);
28
63
  if (!rootDomain) return;
@@ -43,6 +78,24 @@ class EntityClassification {
43
78
  return unrecognizedEntity;
44
79
  }
45
80
 
81
+ /**
82
+ * Preload Chrome extensions found in the devtoolsLog into cache.
83
+ * @param {EntityCache} entityCache
84
+ * @param {LH.DevtoolsLog} devtoolsLog
85
+ */
86
+ static _preloadChromeExtensionsToCache(entityCache, devtoolsLog) {
87
+ for (const entry of devtoolsLog.values()) {
88
+ if (entry.method !== 'Runtime.executionContextCreated') continue;
89
+
90
+ const origin = entry.params.context.origin;
91
+ if (!origin.startsWith('chrome-extension:')) continue;
92
+ if (entityCache.has(origin)) continue;
93
+
94
+ EntityClassification.makeupChromeExtensionEntity_(entityCache, origin,
95
+ entry.params.context.name);
96
+ }
97
+ }
98
+
46
99
  /**
47
100
  * @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
48
101
  * @param {LH.Artifacts.ComputedContext} context
@@ -57,12 +110,14 @@ class EntityClassification {
57
110
  /** @type {Map<LH.Artifacts.Entity, Set<string>>} */
58
111
  const urlsByEntity = new Map();
59
112
 
113
+ EntityClassification._preloadChromeExtensionsToCache(madeUpEntityCache, data.devtoolsLog);
114
+
60
115
  for (const record of networkRecords) {
61
116
  const {url} = record;
62
117
  if (entityByUrl.has(url)) continue;
63
118
 
64
119
  const entity = thirdPartyWeb.getEntity(url) ||
65
- EntityClassification.makeUpAnEntity(madeUpEntityCache, url);
120
+ EntityClassification._makeUpAnEntity(madeUpEntityCache, url);
66
121
  if (!entity) continue;
67
122
 
68
123
  const entityURLs = urlsByEntity.get(entity) || new Set();
@@ -76,7 +131,7 @@ class EntityClassification {
76
131
  // See https://github.com/GoogleChrome/lighthouse/issues/13706
77
132
  const firstPartyUrl = data.URL.mainDocumentUrl || data.URL.finalDisplayedUrl;
78
133
  const firstParty = thirdPartyWeb.getEntity(firstPartyUrl) ||
79
- EntityClassification.makeUpAnEntity(madeUpEntityCache, firstPartyUrl);
134
+ EntityClassification._makeUpAnEntity(madeUpEntityCache, firstPartyUrl);
80
135
 
81
136
  /**
82
137
  * Convenience function to check if a URL belongs to first party.
@@ -3,6 +3,7 @@ export type LayoutShiftEvent = {
3
3
  ts: number;
4
4
  isMainFrame: boolean;
5
5
  weightedScore: number;
6
+ impactedNodes?: LH.Artifacts.TraceImpactedNode[];
6
7
  };
7
8
  declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
8
9
  request: (dependencies: import("../../index.js").Trace, context: import("../../../types/utility-types.js").default.ImmutableObject<{
@@ -10,7 +11,6 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
10
11
  }>) => Promise<{
11
12
  cumulativeLayoutShift: number;
12
13
  cumulativeLayoutShiftMainFrame: number;
13
- totalCumulativeLayoutShift: number;
14
14
  }>;
15
15
  };
16
16
  declare class CumulativeLayoutShift {
@@ -32,21 +32,14 @@ declare class CumulativeLayoutShift {
32
32
  * @return {number}
33
33
  */
34
34
  static calculate(layoutShiftEvents: Array<LayoutShiftEvent>): number;
35
- /**
36
- * Sum all layout shift events from the entire trace.
37
- * @param {Array<LayoutShiftEvent>} layoutShiftEvents
38
- * @return {number}
39
- */
40
- static calculateTotalCumulativeLayoutShift(layoutShiftEvents: Array<LayoutShiftEvent>): number;
41
35
  /**
42
36
  * @param {LH.Trace} trace
43
37
  * @param {LH.Artifacts.ComputedContext} context
44
- * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, totalCumulativeLayoutShift: number}>}
38
+ * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
45
39
  */
46
40
  static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
47
41
  cumulativeLayoutShift: number;
48
42
  cumulativeLayoutShiftMainFrame: number;
49
- totalCumulativeLayoutShift: number;
50
43
  }>;
51
44
  }
52
45
  import { ProcessedTrace } from '../processed-trace.js';
@@ -7,7 +7,7 @@
7
7
  import {makeComputedArtifact} from '../computed-artifact.js';
8
8
  import {ProcessedTrace} from '../processed-trace.js';
9
9
 
10
- /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
10
+ /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[]}} LayoutShiftEvent */
11
11
 
12
12
  const RECENT_INPUT_WINDOW = 500;
13
13
 
@@ -65,6 +65,7 @@ class CumulativeLayoutShift {
65
65
  ts: event.ts,
66
66
  isMainFrame: event.args.data.is_main_frame,
67
67
  weightedScore: event.args.data.weighted_score_delta,
68
+ impactedNodes: event.args.data.impacted_nodes,
68
69
  });
69
70
  }
70
71
 
@@ -100,19 +101,10 @@ class CumulativeLayoutShift {
100
101
  return maxScore;
101
102
  }
102
103
 
103
- /**
104
- * Sum all layout shift events from the entire trace.
105
- * @param {Array<LayoutShiftEvent>} layoutShiftEvents
106
- * @return {number}
107
- */
108
- static calculateTotalCumulativeLayoutShift(layoutShiftEvents) {
109
- return layoutShiftEvents.reduce((sum, e) => sum += e.weightedScore, 0);
110
- }
111
-
112
104
  /**
113
105
  * @param {LH.Trace} trace
114
106
  * @param {LH.Artifacts.ComputedContext} context
115
- * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, totalCumulativeLayoutShift: number}>}
107
+ * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
116
108
  */
117
109
  static async compute_(trace, context) {
118
110
  const processedTrace = await ProcessedTrace.request(trace, context);
@@ -121,14 +113,9 @@ class CumulativeLayoutShift {
121
113
  CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
122
114
  const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
123
115
 
124
- // The original Cumulative Layout Shift metric, the sum of all main-frame shift events.
125
- const totalCumulativeLayoutShift =
126
- CumulativeLayoutShift.calculateTotalCumulativeLayoutShift(mainFrameShiftEvents);
127
-
128
116
  return {
129
117
  cumulativeLayoutShift: CumulativeLayoutShift.calculate(allFrameShiftEvents),
130
118
  cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents),
131
- totalCumulativeLayoutShift,
132
119
  };
133
120
  }
134
121
  }
@@ -26,7 +26,8 @@ class TimeToFirstByte extends NavigationMetric {
26
26
 
27
27
  // Estimate when the connection is not warm.
28
28
  // TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
29
- let roundTrips = 3;
29
+ let roundTrips = 2;
30
+ if (!mainResource.protocol.startsWith('h3')) roundTrips += 1; // TCP
30
31
  if (mainResource.parsedURL.scheme === 'https') roundTrips += 1;
31
32
  const estimatedTTFB = data.settings.throttling.rttMs * roundTrips + observedResponseTime;
32
33
 
@@ -40,15 +41,17 @@ class TimeToFirstByte extends NavigationMetric {
40
41
  * @return {Promise<LH.Artifacts.Metric>}
41
42
  */
42
43
  static async computeObservedMetric(data, context) {
43
- const {processedNavigation} = data;
44
- const timeOriginTs = processedNavigation.timestamps.timeOrigin;
45
44
  const mainResource = await MainResource.request(data, context);
45
+ if (!mainResource.timing) {
46
+ throw new Error('missing timing for main resource');
47
+ }
46
48
 
47
- // Technically TTFB is the start of the response headers not the end.
48
- // That signal isn't available to us so we use header end time as a best guess.
49
- const timestamp = mainResource.responseHeadersEndTime * 1000;
49
+ const {processedNavigation} = data;
50
+ const timeOriginTs = processedNavigation.timestamps.timeOrigin;
51
+ const timestampMs =
52
+ mainResource.timing.requestTime * 1000 + mainResource.timing.receiveHeadersStart;
53
+ const timestamp = timestampMs * 1000;
50
54
  const timing = (timestamp - timeOriginTs) / 1000;
51
-
52
55
  return {timing, timestamp};
53
56
  }
54
57
  }
@@ -65,7 +65,6 @@ class TimingSummary {
65
65
  const {
66
66
  cumulativeLayoutShift,
67
67
  cumulativeLayoutShiftMainFrame,
68
- totalCumulativeLayoutShift,
69
68
  } = cumulativeLayoutShiftValues || {};
70
69
 
71
70
  /** @type {LH.Artifacts.TimingSummary} */
@@ -89,7 +88,6 @@ class TimingSummary {
89
88
  maxPotentialFID: maxPotentialFID?.timing,
90
89
  cumulativeLayoutShift,
91
90
  cumulativeLayoutShiftMainFrame,
92
- totalCumulativeLayoutShift,
93
91
 
94
92
  lcpLoadStart: lcpBreakdown?.loadStart,
95
93
  lcpLoadEnd: lcpBreakdown?.loadEnd,
@@ -123,7 +121,6 @@ class TimingSummary {
123
121
  observedDomContentLoadedTs: processedNavigation?.timestamps.domContentLoaded,
124
122
  observedCumulativeLayoutShift: cumulativeLayoutShift,
125
123
  observedCumulativeLayoutShiftMainFrame: cumulativeLayoutShiftMainFrame,
126
- observedTotalCumulativeLayoutShift: totalCumulativeLayoutShift,
127
124
 
128
125
  // Include some visual metrics from speedline
129
126
  observedFirstVisualChange: speedline.first,
@@ -263,7 +263,9 @@ const defaultConfig = {
263
263
  'accessibility/heading-order',
264
264
  'accessibility/html-has-lang',
265
265
  'accessibility/html-lang-valid',
266
+ 'accessibility/html-xml-lang-mismatch',
266
267
  'accessibility/image-alt',
268
+ 'accessibility/input-button-name',
267
269
  'accessibility/input-image-alt',
268
270
  'accessibility/label',
269
271
  'accessibility/link-name',
@@ -273,6 +275,8 @@ const defaultConfig = {
273
275
  'accessibility/meta-viewport',
274
276
  'accessibility/object-alt',
275
277
  'accessibility/tabindex',
278
+ 'accessibility/table-fake-caption',
279
+ 'accessibility/td-has-header',
276
280
  'accessibility/td-headers-attr',
277
281
  'accessibility/th-has-data-cells',
278
282
  'accessibility/valid-lang',
@@ -527,7 +531,9 @@ const defaultConfig = {
527
531
  {id: 'heading-order', weight: 2, group: 'a11y-navigation'},
528
532
  {id: 'html-has-lang', weight: 3, group: 'a11y-language'},
529
533
  {id: 'html-lang-valid', weight: 3, group: 'a11y-language'},
534
+ {id: 'html-xml-lang-mismatch', weight: 2, group: 'a11y-language'},
530
535
  {id: 'image-alt', weight: 10, group: 'a11y-names-labels'},
536
+ {id: 'input-button-name', weight: 10, group: 'a11y-names-labels'},
531
537
  {id: 'input-image-alt', weight: 10, group: 'a11y-names-labels'},
532
538
  {id: 'label', weight: 10, group: 'a11y-names-labels'},
533
539
  {id: 'link-name', weight: 3, group: 'a11y-names-labels'},
@@ -537,6 +543,8 @@ const defaultConfig = {
537
543
  {id: 'meta-viewport', weight: 10, group: 'a11y-best-practices'},
538
544
  {id: 'object-alt', weight: 3, group: 'a11y-names-labels'},
539
545
  {id: 'tabindex', weight: 3, group: 'a11y-navigation'},
546
+ {id: 'table-fake-caption', weight: 3, group: 'a11y-tables-lists'},
547
+ {id: 'td-has-header', weight: 10, group: 'a11y-tables-lists'},
540
548
  {id: 'td-headers-attr', weight: 3, group: 'a11y-tables-lists'},
541
549
  {id: 'th-has-data-cells', weight: 3, group: 'a11y-tables-lists'},
542
550
  {id: 'valid-lang', weight: 3, group: 'a11y-language'},
@@ -125,14 +125,22 @@ class ExecutionContext {
125
125
 
126
126
  this._session.setNextProtocolTimeout(timeout);
127
127
  const response = await this._session.sendCommand('Runtime.evaluate', evaluationParams);
128
- if (response.exceptionDetails) {
128
+
129
+ const ex = response.exceptionDetails;
130
+ if (ex) {
129
131
  // An error occurred before we could even create a Promise, should be *very* rare.
130
132
  // Also occurs when the expression is not valid JavaScript.
131
- const errorMessage = response.exceptionDetails.exception ?
132
- response.exceptionDetails.exception.description :
133
- response.exceptionDetails.text;
134
- return Promise.reject(new Error(`Evaluation exception: ${errorMessage}`));
133
+ const elidedExpression = expression.replace(/\s+/g, ' ').substring(0, 100);
134
+ const messageLines = [
135
+ 'Runtime.evaluate exception',
136
+ `Expression: ${elidedExpression}\n---- (elided)`,
137
+ !ex.stackTrace ? `Parse error at: ${ex.lineNumber + 1}:${ex.columnNumber + 1}` : null,
138
+ ex.exception?.description || ex.text,
139
+ ].filter(Boolean);
140
+ const evaluationError = new Error(messageLines.join('\n'));
141
+ return Promise.reject(evaluationError);
135
142
  }
143
+
136
144
  // Protocol should always return a 'result' object, but it is sometimes undefined. See #6026.
137
145
  if (response.result === undefined) {
138
146
  return Promise.reject(
@@ -18,12 +18,15 @@ export class TargetManager extends TargetManager_base {
18
18
  constructor(cdpSession: LH.Puppeteer.CDPSession);
19
19
  _enabled: boolean;
20
20
  _rootCdpSession: import("../../../types/puppeteer.js").default.CDPSession;
21
+ _mainFrameId: string;
21
22
  /**
22
23
  * A map of target id to target/session information. Used to ensure unique
23
24
  * attached targets.
24
25
  * @type {Map<string, TargetWithSession>}
25
26
  */
26
27
  _targetIdToTargets: Map<string, TargetWithSession>;
28
+ /** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
29
+ _executionContextIdToDescriptions: Map<string, LH.Crdp.Runtime.ExecutionContextDescription>;
27
30
  /**
28
31
  * @param {LH.Puppeteer.CDPSession} cdpSession
29
32
  */
@@ -32,6 +35,15 @@ export class TargetManager extends TargetManager_base {
32
35
  * @param {LH.Crdp.Page.FrameNavigatedEvent} frameNavigatedEvent
33
36
  */
34
37
  _onFrameNavigated(frameNavigatedEvent: LH.Crdp.Page.FrameNavigatedEvent): Promise<void>;
38
+ /**
39
+ * @param {LH.Crdp.Runtime.ExecutionContextCreatedEvent} event
40
+ */
41
+ _onExecutionContextCreated(event: LH.Crdp.Runtime.ExecutionContextCreatedEvent): void;
42
+ /**
43
+ * @param {LH.Crdp.Runtime.ExecutionContextDestroyedEvent} event
44
+ */
45
+ _onExecutionContextDestroyed(event: LH.Crdp.Runtime.ExecutionContextDestroyedEvent): void;
46
+ _onExecutionContextsCleared(): void;
35
47
  /**
36
48
  * @param {string} sessionId
37
49
  * @return {LH.Gatherer.FRProtocolSession}
@@ -42,6 +54,7 @@ export class TargetManager extends TargetManager_base {
42
54
  * @return {LH.Gatherer.FRProtocolSession}
43
55
  */
44
56
  rootSession(): LH.Gatherer.FRProtocolSession;
57
+ mainFrameExecutionContexts(): import("devtools-protocol").Protocol.Runtime.ExecutionContextDescription[];
45
58
  /**
46
59
  * Returns a listener for all protocol events from session, and augments the
47
60
  * event with the sessionId.
@@ -40,6 +40,7 @@ class TargetManager extends ProtocolEventEmitter {
40
40
 
41
41
  this._enabled = false;
42
42
  this._rootCdpSession = cdpSession;
43
+ this._mainFrameId = '';
43
44
 
44
45
  /**
45
46
  * A map of target id to target/session information. Used to ensure unique
@@ -47,9 +48,14 @@ class TargetManager extends ProtocolEventEmitter {
47
48
  * @type {Map<string, TargetWithSession>}
48
49
  */
49
50
  this._targetIdToTargets = new Map();
51
+ /** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
52
+ this._executionContextIdToDescriptions = new Map();
50
53
 
51
54
  this._onSessionAttached = this._onSessionAttached.bind(this);
52
55
  this._onFrameNavigated = this._onFrameNavigated.bind(this);
56
+ this._onExecutionContextCreated = this._onExecutionContextCreated.bind(this);
57
+ this._onExecutionContextDestroyed = this._onExecutionContextDestroyed.bind(this);
58
+ this._onExecutionContextsCleared = this._onExecutionContextsCleared.bind(this);
53
59
  }
54
60
 
55
61
  /**
@@ -97,6 +103,12 @@ class TargetManager extends ProtocolEventEmitter {
97
103
  return this._findSession(rootSessionId);
98
104
  }
99
105
 
106
+ mainFrameExecutionContexts() {
107
+ return [...this._executionContextIdToDescriptions.values()].filter(executionContext => {
108
+ return executionContext.auxData.frameId === this._mainFrameId;
109
+ });
110
+ }
111
+
100
112
  /**
101
113
  * @param {LH.Puppeteer.CDPSession} cdpSession
102
114
  */
@@ -153,6 +165,27 @@ class TargetManager extends ProtocolEventEmitter {
153
165
  }
154
166
  }
155
167
 
168
+ /**
169
+ * @param {LH.Crdp.Runtime.ExecutionContextCreatedEvent} event
170
+ */
171
+ _onExecutionContextCreated(event) {
172
+ if (event.context.name === '__puppeteer_utility_world__') return;
173
+ if (event.context.name === 'lighthouse_isolated_context') return;
174
+
175
+ this._executionContextIdToDescriptions.set(event.context.uniqueId, event.context);
176
+ }
177
+
178
+ /**
179
+ * @param {LH.Crdp.Runtime.ExecutionContextDestroyedEvent} event
180
+ */
181
+ _onExecutionContextDestroyed(event) {
182
+ this._executionContextIdToDescriptions.delete(event.executionContextUniqueId);
183
+ }
184
+
185
+ _onExecutionContextsCleared() {
186
+ this._executionContextIdToDescriptions.clear();
187
+ }
188
+
156
189
  /**
157
190
  * Returns a listener for all protocol events from session, and augments the
158
191
  * event with the sessionId.
@@ -183,10 +216,17 @@ class TargetManager extends ProtocolEventEmitter {
183
216
 
184
217
  this._enabled = true;
185
218
  this._targetIdToTargets = new Map();
219
+ this._executionContextIdToDescriptions = new Map();
186
220
 
187
221
  this._rootCdpSession.on('Page.frameNavigated', this._onFrameNavigated);
222
+ this._rootCdpSession.on('Runtime.executionContextCreated', this._onExecutionContextCreated);
223
+ this._rootCdpSession.on('Runtime.executionContextDestroyed', this._onExecutionContextDestroyed);
224
+ this._rootCdpSession.on('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
188
225
 
189
226
  await this._rootCdpSession.send('Page.enable');
227
+ await this._rootCdpSession.send('Runtime.enable');
228
+
229
+ this._mainFrameId = (await this._rootCdpSession.send('Page.getFrameTree')).frameTree.frame.id;
190
230
 
191
231
  // Start with the already attached root session.
192
232
  await this._onSessionAttached(this._rootCdpSession);
@@ -197,14 +237,23 @@ class TargetManager extends ProtocolEventEmitter {
197
237
  */
198
238
  async disable() {
199
239
  this._rootCdpSession.off('Page.frameNavigated', this._onFrameNavigated);
240
+ this._rootCdpSession.off('Runtime.executionContextCreated', this._onExecutionContextCreated);
241
+ this._rootCdpSession.off('Runtime.executionContextDestroyed',
242
+ this._onExecutionContextDestroyed);
243
+ this._rootCdpSession.off('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
200
244
 
201
245
  for (const {cdpSession, protocolListener} of this._targetIdToTargets.values()) {
202
246
  cdpSession.off('*', protocolListener);
203
247
  cdpSession.off('sessionattached', this._onSessionAttached);
204
248
  }
205
249
 
250
+ await this._rootCdpSession.send('Page.disable');
251
+ await this._rootCdpSession.send('Runtime.disable');
252
+
206
253
  this._enabled = false;
207
254
  this._targetIdToTargets = new Map();
255
+ this._executionContextIdToDescriptions = new Map();
256
+ this._mainFrameId = '';
208
257
  }
209
258
  }
210
259
 
@@ -39,34 +39,33 @@ async function runA11yChecks() {
39
39
  resultTypes: ['violations', 'inapplicable'],
40
40
  rules: {
41
41
  // Consider http://go/prcpg for expert review of the aXe rules.
42
- 'tabindex': {enabled: true},
43
42
  'accesskeys': {enabled: true},
44
- 'heading-order': {enabled: true},
45
- 'meta-viewport': {enabled: true},
46
- 'duplicate-id': {enabled: false},
47
- 'table-fake-caption': {enabled: false},
48
- 'td-has-header': {enabled: false},
49
- 'marquee': {enabled: false},
50
43
  'area-alt': {enabled: false},
51
- 'html-xml-lang-mismatch': {enabled: false},
44
+ 'aria-roledescription': {enabled: false},
45
+ 'aria-treeitem-name': {enabled: true},
46
+ 'audio-caption': {enabled: false},
52
47
  'blink': {enabled: false},
53
- 'server-side-image-map': {enabled: false},
48
+ 'duplicate-id': {enabled: false},
49
+ 'frame-focusable-content': {enabled: false},
50
+ 'frame-title-unique': {enabled: false},
51
+ 'heading-order': {enabled: true},
52
+ 'html-xml-lang-mismatch': {enabled: true},
54
53
  'identical-links-same-purpose': {enabled: false},
55
- 'no-autoplay-audio': {enabled: false},
56
- 'svg-img-alt': {enabled: false},
57
- 'audio-caption': {enabled: false},
58
- 'aria-treeitem-name': {enabled: true},
54
+ 'input-button-name': {enabled: true},
55
+ 'link-in-text-block': {enabled: false},
56
+ 'marquee': {enabled: false},
57
+ 'meta-viewport': {enabled: true},
59
58
  // https://github.com/dequelabs/axe-core/issues/2958
60
59
  'nested-interactive': {enabled: false},
61
- 'frame-focusable-content': {enabled: false},
62
- 'aria-roledescription': {enabled: false},
63
- 'scrollable-region-focusable': {enabled: false},
64
- // TODO(paulirish): create audits and enable these 5.
65
- 'input-button-name': {enabled: false},
60
+ 'no-autoplay-audio': {enabled: false},
66
61
  'role-img-alt': {enabled: false},
62
+ 'scrollable-region-focusable': {enabled: false},
67
63
  'select-name': {enabled: false},
68
- 'link-in-text-block': {enabled: false},
69
- 'frame-title-unique': {enabled: false},
64
+ 'server-side-image-map': {enabled: false},
65
+ 'svg-img-alt': {enabled: false},
66
+ 'tabindex': {enabled: true},
67
+ 'table-fake-caption': {enabled: true},
68
+ 'td-has-header': {enabled: true},
70
69
  },
71
70
  });
72
71