lighthouse 12.6.1-dev.20250603 → 12.6.1-dev.20250605

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.
@@ -61,6 +61,7 @@ import screenshot from './test-definitions/screenshot.js';
61
61
  import seoFailing from './test-definitions/seo-failing.js';
62
62
  import seoPassing from './test-definitions/seo-passing.js';
63
63
  import seoStatus403 from './test-definitions/seo-status-403.js';
64
+ import seoMixedLanguage from './test-definitions/seo-mixed-language.js';
64
65
  import serviceWorkerReloaded from './test-definitions/service-worker-reloaded.js';
65
66
  import shiftAttribution from './test-definitions/shift-attribution.js';
66
67
  import sourceMaps from './test-definitions/source-maps.js';
@@ -125,6 +126,7 @@ const smokeTests = [
125
126
  seoFailing,
126
127
  seoPassing,
127
128
  seoStatus403,
129
+ seoMixedLanguage,
128
130
  serviceWorkerReloaded,
129
131
  shiftAttribution,
130
132
  sourceMaps,
@@ -114,7 +114,7 @@ class CLSCulpritsInsight extends Audit {
114
114
  /** @type {LH.Audit.Details.Table['items']} */
115
115
  const items = events.map(event => {
116
116
  const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
117
- event.args.data.impacted_nodes || [], impactByNodeId, event);
117
+ event.args.data.impacted_nodes || [], impactByNodeId);
118
118
  return {
119
119
  node: makeNodeItemForNodeId(artifacts.TraceElements, biggestImpactNodeId),
120
120
  score: event.args.data?.weighted_score_delta,
@@ -1,11 +1,11 @@
1
1
  export type CreateDetailsExtras = {
2
2
  insights: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightSet;
3
- parsedTrace: LH.Artifacts.TraceEngineResult["data"];
3
+ parsedTrace: LH.Artifacts.TraceEngineResult["parsedTrace"];
4
4
  };
5
5
  /**
6
6
  * @typedef CreateDetailsExtras
7
7
  * @property {import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet} insights
8
- * @property {LH.Artifacts.TraceEngineResult['data']} parsedTrace
8
+ * @property {LH.Artifacts.TraceEngineResult['parsedTrace']} parsedTrace
9
9
  */
10
10
  /**
11
11
  * @param {LH.Artifacts} artifacts
@@ -16,7 +16,7 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, {});
16
16
  /**
17
17
  * @param {LH.Artifacts} artifacts
18
18
  * @param {LH.Audit.Context} context
19
- * @return {Promise<{insights: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined, parsedTrace: LH.Artifacts.TraceEngineResult['data']}>}
19
+ * @return {Promise<{insights: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined, parsedTrace: LH.Artifacts.TraceEngineResult['parsedTrace']}>}
20
20
  */
21
21
  async function getInsightSet(artifacts, context) {
22
22
  const settings = context.settings;
@@ -29,13 +29,13 @@ async function getInsightSet(artifacts, context) {
29
29
  const key = navigationId ?? NO_NAVIGATION;
30
30
  const insights = traceEngineResult.insights.get(key);
31
31
 
32
- return {insights, parsedTrace: traceEngineResult.data};
32
+ return {insights, parsedTrace: traceEngineResult.parsedTrace};
33
33
  }
34
34
 
35
35
  /**
36
36
  * @typedef CreateDetailsExtras
37
37
  * @property {import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet} insights
38
- * @property {LH.Artifacts.TraceEngineResult['data']} parsedTrace
38
+ * @property {LH.Artifacts.TraceEngineResult['parsedTrace']} parsedTrace
39
39
  */
40
40
 
41
41
  /**
@@ -62,7 +62,7 @@ class LayoutShifts extends Audit {
62
62
  const SourceMaps = artifacts.SourceMaps;
63
63
  const traceEngineResult =
64
64
  await TraceEngineResult.request({trace, settings, SourceMaps}, context);
65
- const clusters = traceEngineResult.data.LayoutShifts.clusters ?? [];
65
+ const clusters = traceEngineResult.parsedTrace.LayoutShifts.clusters ?? [];
66
66
  const {cumulativeLayoutShift: clsSavings, impactByNodeId} =
67
67
  await CumulativeLayoutShiftComputed.request(trace, context);
68
68
  const traceElements = artifacts.TraceElements
@@ -89,7 +89,7 @@ class LayoutShifts extends Audit {
89
89
  .slice(0, MAX_LAYOUT_SHIFTS);
90
90
  for (const event of topLayoutShiftEvents) {
91
91
  const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
92
- event.args.data.impacted_nodes || [], impactByNodeId, event);
92
+ event.args.data.impacted_nodes || [], impactByNodeId);
93
93
  const biggestImpactElement = traceElements.find(t => t.nodeId === biggestImpactNodeId);
94
94
 
95
95
  // Turn root causes into sub-items.
@@ -8,95 +8,123 @@ import {Audit} from '../audit.js';
8
8
  import UrlUtils from '../../lib/url-utils.js';
9
9
  import * as i18n from '../../lib/i18n/i18n.js';
10
10
 
11
- const BLOCKLIST = new Set([
11
+ /** @type {Record<string, Set<string>>} */
12
+ const nonDescriptiveLinkTexts = {
12
13
  // English
13
- 'click here',
14
- 'click this',
15
- 'go',
16
- 'here',
17
- 'information',
18
- 'learn more',
19
- 'more',
20
- 'more info',
21
- 'more information',
22
- 'right here',
23
- 'read more',
24
- 'see more',
25
- 'start',
26
- 'this',
14
+ 'en': new Set([
15
+ 'click here',
16
+ 'click this',
17
+ 'go',
18
+ 'here',
19
+ 'information',
20
+ 'learn more',
21
+ 'more',
22
+ 'more info',
23
+ 'more information',
24
+ 'right here',
25
+ 'read more',
26
+ 'see more',
27
+ 'start',
28
+ 'this',
29
+ ]),
27
30
  // Japanese
28
- 'ここをクリック',
29
- 'こちらをクリック',
30
- 'リンク',
31
- '続きを読む',
32
- '続く',
33
- '全文表示',
31
+ 'ja': new Set([
32
+ 'ここをクリック',
33
+ 'こちらをクリック',
34
+ 'リンク',
35
+ '続きを読む',
36
+ '続く',
37
+ '全文表示',
38
+ ]),
34
39
  // Spanish
35
- 'click aquí',
36
- 'click aqui',
37
- 'clicka aquí',
38
- 'clicka aqui',
39
- 'pincha aquí',
40
- 'pincha aqui',
41
- 'aquí',
42
- 'aqui',
43
- 'más',
44
- 'mas',
45
- 'más información',
46
- 'más informacion',
47
- 'mas información',
48
- 'mas informacion',
49
- 'este',
50
- 'enlace',
51
- 'este enlace',
52
- 'empezar',
40
+ 'es': new Set([
41
+ 'click aquí',
42
+ 'click aqui',
43
+ 'clicka aquí',
44
+ 'clicka aqui',
45
+ 'pincha aquí',
46
+ 'pincha aqui',
47
+ 'aquí',
48
+ 'aqui',
49
+ 'más',
50
+ 'mas',
51
+ 'más información',
52
+ 'más informacion',
53
+ 'mas información',
54
+ 'mas informacion',
55
+ 'este',
56
+ 'enlace',
57
+ 'este enlace',
58
+ 'empezar',
59
+ ]),
53
60
  // Portuguese
54
- 'clique aqui',
55
- 'ir',
56
- 'mais informação',
57
- 'mais informações',
58
- 'mais',
59
- 'veja mais',
61
+ 'pt': new Set([
62
+ 'clique aqui',
63
+ 'ir',
64
+ 'mais informação',
65
+ 'mais informações',
66
+ 'mais',
67
+ 'veja mais',
68
+ ]),
60
69
  // Korean
61
- '여기',
62
- '여기를 클릭',
63
- '클릭',
64
- '링크',
65
- '자세히',
66
- '자세히 보기',
67
- '계속',
68
- '이동',
69
- '전체 보기',
70
+ 'ko': new Set([
71
+ '여기',
72
+ '여기를 클릭',
73
+ '클릭',
74
+ '링크',
75
+ '자세히',
76
+ '자세히 보기',
77
+ '계속',
78
+ '이동',
79
+ '전체 보기',
80
+ ]),
70
81
  // Swedish
71
- 'här',
72
- 'klicka här',
73
- 'läs mer',
74
- 'mer',
75
- 'mer info',
76
- 'mer information',
82
+ 'sv': new Set([
83
+ 'här',
84
+ 'klicka här',
85
+ 'läs mer',
86
+ 'mer',
87
+ 'mer info',
88
+ 'mer information',
89
+ ]),
90
+ // German
91
+ 'de': new Set([
92
+ 'klicke hier',
93
+ 'hier klicken',
94
+ 'hier',
95
+ 'mehr',
96
+ 'siehe',
97
+ 'dies',
98
+ 'das',
99
+ 'weiterlesen',
100
+ ]),
77
101
  // Tamil
78
- 'அடுத்த பக்கம்',
79
- 'மறுபக்கம்',
80
- 'முந்தைய பக்கம்',
81
- 'முன்பக்கம்',
82
- 'மேலும் அறிக',
83
- 'மேலும் தகவலுக்கு',
84
- 'மேலும் தரவுகளுக்கு',
85
- 'தயவுசெய்து இங்கே அழுத்தவும்',
86
- 'இங்கே கிளிக் செய்யவும்',
102
+ 'ta': new Set([
103
+ 'அடுத்த பக்கம்',
104
+ 'மறுபக்கம்',
105
+ 'முந்தைய பக்கம்',
106
+ 'முன்பக்கம்',
107
+ 'மேலும் அறிக',
108
+ 'மேலும் தகவலுக்கு',
109
+ 'மேலும் தரவுகளுக்கு',
110
+ 'தயவுசெய்து இங்கே அழுத்தவும்',
111
+ 'இங்கே கிளிக் செய்யவும்',
112
+ ]),
87
113
  // Persian
88
- 'اطلاعات بیشتر',
89
- 'اطلاعات',
90
- 'این',
91
- 'اینجا بزنید',
92
- 'اینجا کلیک کنید',
93
- 'اینجا',
94
- 'برو',
95
- 'بیشتر بخوانید',
96
- 'بیشتر بدانید',
97
- 'بیشتر',
98
- 'شروع',
99
- ]);
114
+ 'fa': new Set([
115
+ 'اطلاعات بیشتر',
116
+ 'اطلاعات',
117
+ 'این',
118
+ 'اینجا بزنید',
119
+ 'اینجا کلیک کنید',
120
+ 'اینجا',
121
+ 'برو',
122
+ 'بیشتر بخوانید',
123
+ 'بیشتر بدانید',
124
+ 'بیشتر',
125
+ 'شروع',
126
+ ]),
127
+ };
100
128
 
101
129
  const UIStrings = {
102
130
  /** Title of a Lighthouse audit that tests if each link on a page contains a sufficient description of what a user will find when they click it. Generic, non-descriptive text like "click here" doesn't give an indication of what the link leads to. This descriptive title is shown when all links on the page have sufficient textual descriptions. */
@@ -135,8 +163,9 @@ class LinkText extends Audit {
135
163
  */
136
164
  static audit(artifacts) {
137
165
  const failingLinks = artifacts.AnchorElements
138
- .filter(link => link.href && !link.rel.includes('nofollow'))
139
166
  .filter(link => {
167
+ if (!link.href || link.rel.includes('nofollow')) return false;
168
+
140
169
  const href = link.href.toLowerCase();
141
170
  if (
142
171
  href.startsWith('javascript:') ||
@@ -148,12 +177,30 @@ class LinkText extends Audit {
148
177
  return false;
149
178
  }
150
179
 
151
- return BLOCKLIST.has(link.text.trim().toLowerCase());
180
+ const searchTerm = link.text.trim().toLowerCase();
181
+ if (searchTerm) {
182
+ // Use language if detected, otherwise look at everything.
183
+ if (link.textLang) {
184
+ const lang = link.textLang.split('-')[0];
185
+ if (nonDescriptiveLinkTexts[lang] && nonDescriptiveLinkTexts[lang].has(searchTerm)) {
186
+ return true;
187
+ }
188
+ } else {
189
+ for (const texts of Object.values(nonDescriptiveLinkTexts)) {
190
+ if (texts.has(searchTerm)) {
191
+ return true;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ return false;
152
198
  })
153
199
  .map(link => {
154
200
  return {
155
201
  href: link.href,
156
202
  text: link.text.trim(),
203
+ textLang: link.textLang,
157
204
  };
158
205
  });
159
206
 
@@ -38,14 +38,15 @@ async function getComputationDataParamsFromTrace(data, context) {
38
38
 
39
39
  const graph = await PageDependencyGraph.request({...data, fromTrace: true}, context);
40
40
  const traceEngineResult = await TraceEngineResult.request(data, context);
41
- const frameId = traceEngineResult.data.Meta.mainFrameId;
42
- const navigationId = traceEngineResult.data.Meta.mainFrameNavigations[0].args.data?.navigationId;
41
+ const frameId = traceEngineResult.parsedTrace.Meta.mainFrameId;
42
+ const navigationId =
43
+ traceEngineResult.parsedTrace.Meta.mainFrameNavigations[0].args.data?.navigationId;
43
44
  if (!navigationId) {
44
45
  throw new Error(`Lantern metrics could not be calculated due to missing navigation id`);
45
46
  }
46
47
 
47
48
  const processedNavigation = Lantern.TraceEngineComputationData.createProcessedNavigation(
48
- traceEngineResult.data, frameId, navigationId);
49
+ traceEngineResult.parsedTrace, frameId, navigationId);
49
50
  const simulator = data.simulator || (await LoadSimulator.request(data, context));
50
51
 
51
52
  return {simulator, graph, processedNavigation};
@@ -27,11 +27,11 @@ class PageDependencyGraph {
27
27
  if (data.fromTrace) {
28
28
  const traceEngineResult =
29
29
  await TraceEngineResult.request({trace, settings, SourceMaps}, context);
30
- const traceEngineData = traceEngineResult.data;
30
+ const parsedTrace = traceEngineResult.parsedTrace;
31
31
  const requests =
32
- Lantern.TraceEngineComputationData.createNetworkRequests(trace, traceEngineData);
32
+ Lantern.TraceEngineComputationData.createNetworkRequests(trace, parsedTrace);
33
33
  const graph =
34
- Lantern.TraceEngineComputationData.createGraph(requests, trace, traceEngineData, URL);
34
+ Lantern.TraceEngineComputationData.createGraph(requests, trace, parsedTrace, URL);
35
35
  // @ts-expect-error for now, ignore that this is a SyntheticNetworkEvent instead of LH's NetworkEvent.
36
36
  return graph;
37
37
  }
@@ -63,7 +63,7 @@ class TraceEngineResult {
63
63
  if (!processor.parsedTrace) throw new Error('No data');
64
64
  if (!processor.insights) throw new Error('No insights');
65
65
  this.localizeInsights(processor.insights);
66
- return {data: processor.parsedTrace, insights: processor.insights};
66
+ return {parsedTrace: processor.parsedTrace, insights: processor.insights};
67
67
  }
68
68
 
69
69
  /**
@@ -38,10 +38,49 @@ function collectAnchorElements() {
38
38
  return onclick.slice(0, 1024);
39
39
  }
40
40
 
41
+ /**
42
+ * @param {HTMLElement|SVGElement} node
43
+ * @return {string|null}
44
+ */
45
+ function getLangOfInnerText(node) {
46
+ let curNodeLang = null;
47
+
48
+ // If we find multiple languages within this element, return null.
49
+ for (const child of node.querySelectorAll('*')) {
50
+ if (!child.textContent) continue;
51
+
52
+ const childLang = child.closest('[lang]')?.getAttribute('lang');
53
+ if (!childLang) continue;
54
+
55
+ if (!curNodeLang) {
56
+ curNodeLang = childLang;
57
+ continue;
58
+ }
59
+
60
+ if (curNodeLang.split('-')[0] !== childLang.split('-')[0]) {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ return curNodeLang ?? node.closest('[lang]')?.getAttribute('lang') ?? null;
66
+ }
67
+
41
68
  /** @type {Array<HTMLAnchorElement|SVGAElement>} */
42
69
  // @ts-expect-error - put into scope via stringification
43
70
  const anchorElements = getElementsInDocument('a'); // eslint-disable-line no-undef
44
71
 
72
+ // Check, if document has only one lang attribute in opening html or in body tag. If so,
73
+ // there is no need to run the `getLangOfInnerText()` function with multiple
74
+ // possible DOM traversals
75
+ /** @type {Array<HTMLElement|SVGElement>} */
76
+ // @ts-expect-error - put into scope via stringification
77
+ const langElements = getElementsInDocument('[lang]'); // eslint-disable-line no-undef
78
+ const documentHasSingleLang = langElements.length === 1 &&
79
+ (langElements[0].nodeName === 'BODY' || langElements[0].nodeName === 'HTML');
80
+ const singleLang = documentHasSingleLang ? langElements[0].getAttribute('lang') : null;
81
+
82
+ // TODO: consider Content-Language.
83
+
45
84
  return anchorElements.map(node => {
46
85
  if (node instanceof HTMLAnchorElement) {
47
86
  return {
@@ -51,6 +90,7 @@ function collectAnchorElements() {
51
90
  role: node.getAttribute('role') || '',
52
91
  name: node.name,
53
92
  text: node.innerText, // we don't want to return hidden text, so use innerText
93
+ textLang: singleLang ?? getLangOfInnerText(node) ?? undefined,
54
94
  rel: node.rel,
55
95
  target: node.target,
56
96
  id: node.getAttribute('id') || '',
@@ -65,6 +105,7 @@ function collectAnchorElements() {
65
105
  onclick: getTruncatedOnclick(node),
66
106
  role: node.getAttribute('role') || '',
67
107
  text: node.textContent || '',
108
+ textLang: singleLang ?? getLangOfInnerText(node) ?? undefined,
68
109
  rel: '',
69
110
  target: node.target.baseVal || '',
70
111
  id: node.getAttribute('id') || '',
@@ -23,21 +23,20 @@ declare class TraceElements extends BaseGatherer {
23
23
  *
24
24
  * @param {LH.Artifacts.TraceImpactedNode[]} impactedNodes
25
25
  * @param {Map<number, number>} impactByNodeId
26
- * @param {import('../../lib/trace-engine.js').SaneSyntheticLayoutShift} event Only for debugging
27
26
  * @return {number|undefined}
28
27
  */
29
- static getBiggestImpactNodeForShiftEvent(impactedNodes: LH.Artifacts.TraceImpactedNode[], impactByNodeId: Map<number, number>, event: import("../../lib/trace-engine.js").SaneSyntheticLayoutShift): number | undefined;
28
+ static getBiggestImpactNodeForShiftEvent(impactedNodes: LH.Artifacts.TraceImpactedNode[], impactByNodeId: Map<number, number>): number | undefined;
30
29
  /**
31
30
  * This function finds the top (up to 15) layout shifts on the page, and returns
32
31
  * the id of the largest impacted node of each shift, along with any related nodes
33
32
  * that may have caused the shift.
34
33
  *
35
34
  * @param {LH.Trace} trace
36
- * @param {LH.Artifacts.TraceEngineResult['data']} traceEngineResult
35
+ * @param {LH.Artifacts.TraceEngineResult['parsedTrace']} traceEngineResult
37
36
  * @param {LH.Gatherer.Context} context
38
37
  * @return {Promise<Array<{nodeId: number}>>}
39
38
  */
40
- static getTopLayoutShifts(trace: LH.Trace, traceEngineResult: LH.Artifacts.TraceEngineResult["data"], context: LH.Gatherer.Context): Promise<Array<{
39
+ static getTopLayoutShifts(trace: LH.Trace, traceEngineResult: LH.Artifacts.TraceEngineResult["parsedTrace"], context: LH.Gatherer.Context): Promise<Array<{
41
40
  nodeId: number;
42
41
  }>>;
43
42
  /**
@@ -152,49 +152,19 @@ class TraceElements extends BaseGatherer {
152
152
  *
153
153
  * @param {LH.Artifacts.TraceImpactedNode[]} impactedNodes
154
154
  * @param {Map<number, number>} impactByNodeId
155
- * @param {import('../../lib/trace-engine.js').SaneSyntheticLayoutShift} event Only for debugging
156
155
  * @return {number|undefined}
157
156
  */
158
- static getBiggestImpactNodeForShiftEvent(impactedNodes, impactByNodeId, event) {
159
- try {
160
- let biggestImpactNodeId;
161
- let biggestImpactNodeScore = Number.NEGATIVE_INFINITY;
162
- for (const node of impactedNodes) {
163
- const impactScore = impactByNodeId.get(node.node_id);
164
- if (impactScore !== undefined && impactScore > biggestImpactNodeScore) {
165
- biggestImpactNodeId = node.node_id;
166
- biggestImpactNodeScore = impactScore;
167
- }
157
+ static getBiggestImpactNodeForShiftEvent(impactedNodes, impactByNodeId) {
158
+ let biggestImpactNodeId;
159
+ let biggestImpactNodeScore = Number.NEGATIVE_INFINITY;
160
+ for (const node of impactedNodes) {
161
+ const impactScore = impactByNodeId.get(node.node_id);
162
+ if (impactScore !== undefined && impactScore > biggestImpactNodeScore) {
163
+ biggestImpactNodeId = node.node_id;
164
+ biggestImpactNodeScore = impactScore;
168
165
  }
169
- return biggestImpactNodeId;
170
- } catch (err) {
171
- // See https://github.com/GoogleChrome/lighthouse/issues/15870
172
- // `impactedNodes` should always be an array here, but it can randomly be something else for
173
- // currently unknown reasons. This exception handling will help us identify what
174
- // `impactedNodes` really is and also prevent the error from being fatal.
175
-
176
- // It's possible `impactedNodes` is not JSON serializable, so let's add more supplemental
177
- // fields just in case.
178
- const impactedNodesType = typeof impactedNodes;
179
- const impactedNodesClassName = impactedNodes?.constructor?.name;
180
-
181
- let impactedNodesJson;
182
- let eventJson;
183
- try {
184
- impactedNodesJson = JSON.parse(JSON.stringify(impactedNodes));
185
- eventJson = JSON.parse(JSON.stringify(event));
186
- } catch {}
187
-
188
- Sentry.captureException(err, {
189
- extra: {
190
- impactedNodes: impactedNodesJson,
191
- event: eventJson,
192
- impactedNodesType,
193
- impactedNodesClassName,
194
- },
195
- });
196
- return;
197
166
  }
167
+ return biggestImpactNodeId;
198
168
  }
199
169
 
200
170
  /**
@@ -203,7 +173,7 @@ class TraceElements extends BaseGatherer {
203
173
  * that may have caused the shift.
204
174
  *
205
175
  * @param {LH.Trace} trace
206
- * @param {LH.Artifacts.TraceEngineResult['data']} traceEngineResult
176
+ * @param {LH.Artifacts.TraceEngineResult['parsedTrace']} traceEngineResult
207
177
  * @param {LH.Gatherer.Context} context
208
178
  * @return {Promise<Array<{nodeId: number}>>}
209
179
  */
@@ -222,7 +192,7 @@ class TraceElements extends BaseGatherer {
222
192
  const nodeIds = [];
223
193
  const impactedNodes = event.args.data.impacted_nodes || [];
224
194
  const biggestImpactedNodeId =
225
- this.getBiggestImpactNodeForShiftEvent(impactedNodes, impactByNodeId, event);
195
+ this.getBiggestImpactNodeForShiftEvent(impactedNodes, impactByNodeId);
226
196
  if (biggestImpactedNodeId !== undefined) {
227
197
  nodeIds.push(biggestImpactedNodeId);
228
198
  }
@@ -399,7 +369,7 @@ class TraceElements extends BaseGatherer {
399
369
  traceEngineResult, navigationId);
400
370
  const lcpNodeData = await TraceElements.getLcpElement(trace, context);
401
371
  const shiftsData = await TraceElements.getTopLayoutShifts(
402
- trace, traceEngineResult.data, context);
372
+ trace, traceEngineResult.parsedTrace, context);
403
373
  const animatedElementData = await this.getAnimatedElements(mainThreadEvents);
404
374
  const responsivenessElementData = await TraceElements.getResponsivenessElement(trace, context);
405
375
 
@@ -102,5 +102,5 @@ export function normalizeTimingEntries(timings: LH.Result.MeasureEntry[]): void;
102
102
  /**
103
103
  * @param {LH.Result} lhr
104
104
  */
105
- export function elideAuditErrorStacks(lhr: LH.Result): void;
105
+ export function elideLhrErrorStacks(lhr: LH.Result): void;
106
106
  //# sourceMappingURL=asset-saver.d.ts.map
@@ -515,19 +515,31 @@ function normalizeTimingEntries(timings) {
515
515
  }
516
516
 
517
517
  /**
518
- * @param {LH.Result} lhr
518
+ * @param {string} errorStack
519
+ * @return {string}
519
520
  */
520
- function elideAuditErrorStacks(lhr) {
521
+ function elideErrorStack(errorStack) {
521
522
  const baseCallFrameUrl = url.pathToFileURL(LH_ROOT);
523
+ return errorStack
524
+ // Make paths relative to the repo root.
525
+ .replaceAll(baseCallFrameUrl.pathname, '')
526
+ // Remove line/col info.
527
+ .replaceAll(/:\d+:\d+/g, '');
528
+ }
529
+
530
+ /**
531
+ * @param {LH.Result} lhr
532
+ */
533
+ function elideLhrErrorStacks(lhr) {
522
534
  for (const auditResult of Object.values(lhr.audits)) {
523
535
  if (auditResult.errorStack) {
524
- auditResult.errorStack = auditResult.errorStack
525
- // Make paths relative to the repo root.
526
- .replaceAll(baseCallFrameUrl.pathname, '')
527
- // Remove line/col info.
528
- .replaceAll(/:\d+:\d+/g, '');
536
+ auditResult.errorStack = elideErrorStack(auditResult.errorStack);
529
537
  }
530
538
  }
539
+
540
+ if (lhr.runtimeError?.errorStack) {
541
+ lhr.runtimeError.errorStack = elideErrorStack(lhr.runtimeError.errorStack);
542
+ }
531
543
  }
532
544
 
533
545
  export {
@@ -543,5 +555,5 @@ export {
543
555
  saveLanternNetworkData,
544
556
  stringifyReplacer,
545
557
  normalizeTimingEntries,
546
- elideAuditErrorStacks,
558
+ elideLhrErrorStacks,
547
559
  };
package/core/runner.js CHANGED
@@ -312,10 +312,8 @@ class Runner {
312
312
  if (!isEqual(normalizedGatherSettings[k], normalizedAuditSettings[k])) {
313
313
  throw new Error(
314
314
  `Cannot change settings between gathering and auditing…
315
- Difference found at: \`${k}\`
316
- ${normalizedGatherSettings[k]}
317
- vs
318
- ${normalizedAuditSettings[k]}`);
315
+ Difference found at: \`${k}\`: ${JSON.stringify(normalizedGatherSettings[k], null, 2)}
316
+ vs: ${JSON.stringify(normalizedAuditSettings[k], null, 2)}`);
319
317
  }
320
318
  }
321
319
 
@@ -451,21 +449,27 @@ vs
451
449
  * @return {LH.RawIcu<LH.Result['runtimeError']>|undefined}
452
450
  */
453
451
  static getArtifactRuntimeError(artifacts) {
452
+ /** @type {Array<[string, LighthouseError|object]>} */
454
453
  const possibleErrorArtifacts = [
455
- artifacts.PageLoadError, // Preferentially use `PageLoadError`, if it exists.
456
- ...Object.values(artifacts), // Otherwise check amongst all artifacts.
454
+ ['PageLoadError', artifacts.PageLoadError], // Preferentially use `PageLoadError`, if it exists.
455
+ ...Object.entries(artifacts), // Otherwise check amongst all artifacts.
457
456
  ];
458
457
 
459
- for (const possibleErrorArtifact of possibleErrorArtifacts) {
458
+ for (const [artifactKey, possibleErrorArtifact] of possibleErrorArtifacts) {
460
459
  const isError = possibleErrorArtifact instanceof LighthouseError;
461
460
 
462
461
  // eslint-disable-next-line max-len
463
462
  if (isError && possibleErrorArtifact.lhrRuntimeError) {
464
463
  const errorMessage = possibleErrorArtifact.friendlyMessage || possibleErrorArtifact.message;
464
+ // Prefer the stack trace closest to the error.
465
+ const stack =
466
+ /** @type {any} */ (possibleErrorArtifact.cause)?.stack ?? possibleErrorArtifact.stack;
465
467
 
466
468
  return {
467
469
  code: possibleErrorArtifact.code,
468
470
  message: errorMessage,
471
+ errorStack: stack,
472
+ artifactKey,
469
473
  };
470
474
  }
471
475
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "12.6.1-dev.20250603",
4
+ "version": "12.6.1-dev.20250605",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -106,7 +106,7 @@
106
106
  "@esbuild-kit/esm-loader": "^2.1.1",
107
107
  "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
108
108
  "@formatjs/icu-messageformat-parser": "^2.6.2",
109
- "@jest/fake-timers": "^28.1.0",
109
+ "@jest/fake-timers": "^29.7.0",
110
110
  "@testing-library/preact": "^3.1.1",
111
111
  "@testing-library/preact-hooks": "^1.1.0",
112
112
  "@types/archiver": "^2.1.2",
@@ -160,8 +160,8 @@
160
160
  "gh-pages": "^2.0.1",
161
161
  "glob": "^7.1.3",
162
162
  "idb-keyval": "2.2.0",
163
- "jest-mock": "^27.3.0",
164
- "jest-snapshot": "^28.1.0",
163
+ "jest-mock": "^29.7.0",
164
+ "jest-snapshot": "^29.7.0",
165
165
  "jsdom": "^12.2.0",
166
166
  "lighthouse-plugin-soft-navigation": "^1.0.1",
167
167
  "magic-string": "^0.25.7",
@@ -357,6 +357,7 @@ declare module Artifacts {
357
357
  rawHref: string
358
358
  name?: string
359
359
  text: string
360
+ textLang?: string
360
361
  role: string
361
362
  target: string
362
363
  node: NodeDetails
@@ -507,7 +508,7 @@ declare module Artifacts {
507
508
  }
508
509
 
509
510
  interface TraceEngineResult {
510
- data: TraceEngine.Handlers.Types.ParsedTrace;
511
+ parsedTrace: TraceEngine.Handlers.Types.ParsedTrace;
511
512
  insights: TraceEngine.Insights.Types.TraceInsightSets;
512
513
  }
513
514
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  declare global {
8
8
  var expect: import('expect').Expect;
9
- type Mock<T, Y extends unknown[]> = import('jest-mock').Mock<T, Y>;
9
+ type Mock<T> = import('jest-mock').Mock<(...args: any) => T>;
10
10
  }
11
11
 
12
12
  declare module 'expect' {
@@ -41,7 +41,14 @@ interface Result {
41
41
  /** List of top-level warnings for this Lighthouse run. */
42
42
  runWarnings: string[];
43
43
  /** A top-level error message that, if present, indicates a serious enough problem that this Lighthouse result may need to be discarded. */
44
- runtimeError?: {code: string, message: string};
44
+ runtimeError?: {
45
+ code: string;
46
+ message: string;
47
+ /** Error stack from any fatal error. */
48
+ errorStack?: string;
49
+ /** Artifact the threw the fatal error. */
50
+ artifactKey?: string;
51
+ };
45
52
  /** The User-Agent string of the browser used run Lighthouse for these results. */
46
53
  userAgent: string;
47
54
  /** Information about the environment in which Lighthouse was run. */