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.
- package/cli/test/smokehouse/core-tests.js +2 -0
- package/core/audits/insights/cls-culprits-insight.js +1 -1
- package/core/audits/insights/insight-audit.d.ts +2 -2
- package/core/audits/insights/insight-audit.js +3 -3
- package/core/audits/layout-shifts.js +2 -2
- package/core/audits/seo/link-text.js +130 -83
- package/core/computed/metrics/lantern-metric.js +4 -3
- package/core/computed/page-dependency-graph.js +3 -3
- package/core/computed/trace-engine-result.js +1 -1
- package/core/gather/gatherers/anchor-elements.js +41 -0
- package/core/gather/gatherers/trace-elements.d.ts +3 -4
- package/core/gather/gatherers/trace-elements.js +12 -42
- package/core/lib/asset-saver.d.ts +1 -1
- package/core/lib/asset-saver.js +20 -8
- package/core/runner.js +11 -7
- package/package.json +4 -4
- package/types/artifacts.d.ts +2 -1
- package/types/internal/test.d.ts +1 -1
- package/types/lhr/lhr.d.ts +8 -1
|
@@ -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
|
|
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["
|
|
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['
|
|
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['
|
|
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.
|
|
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['
|
|
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.
|
|
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
|
|
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
|
-
|
|
11
|
+
/** @type {Record<string, Set<string>>} */
|
|
12
|
+
const nonDescriptiveLinkTexts = {
|
|
12
13
|
// English
|
|
13
|
-
'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
'
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
42
|
-
const 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.
|
|
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
|
|
30
|
+
const parsedTrace = traceEngineResult.parsedTrace;
|
|
31
31
|
const requests =
|
|
32
|
-
Lantern.TraceEngineComputationData.createNetworkRequests(trace,
|
|
32
|
+
Lantern.TraceEngineComputationData.createNetworkRequests(trace, parsedTrace);
|
|
33
33
|
const graph =
|
|
34
|
-
Lantern.TraceEngineComputationData.createGraph(requests, trace,
|
|
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 {
|
|
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
|
|
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['
|
|
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["
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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['
|
|
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
|
|
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.
|
|
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
|
|
105
|
+
export function elideLhrErrorStacks(lhr: LH.Result): void;
|
|
106
106
|
//# sourceMappingURL=asset-saver.d.ts.map
|
package/core/lib/asset-saver.js
CHANGED
|
@@ -515,19 +515,31 @@ function normalizeTimingEntries(timings) {
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
/**
|
|
518
|
-
* @param {
|
|
518
|
+
* @param {string} errorStack
|
|
519
|
+
* @return {string}
|
|
519
520
|
*/
|
|
520
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": "^
|
|
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": "^
|
|
164
|
-
"jest-snapshot": "^
|
|
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",
|
package/types/artifacts.d.ts
CHANGED
|
@@ -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
|
-
|
|
511
|
+
parsedTrace: TraceEngine.Handlers.Types.ParsedTrace;
|
|
511
512
|
insights: TraceEngine.Insights.Types.TraceInsightSets;
|
|
512
513
|
}
|
|
513
514
|
|
package/types/internal/test.d.ts
CHANGED
package/types/lhr/lhr.d.ts
CHANGED
|
@@ -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?: {
|
|
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. */
|