lighthouse 12.2.3-dev.20241210 → 12.2.3-dev.20241211
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 +4 -0
- package/core/audits/origin-isolation.d.ts +40 -0
- package/core/audits/origin-isolation.js +155 -0
- package/core/config/default-config.js +2 -0
- package/core/gather/gatherers/seo/font-size.d.ts +1 -0
- package/core/gather/gatherers/seo/font-size.js +20 -11
- package/package.json +1 -1
- package/shared/localization/locales/en-US.json +18 -0
- package/shared/localization/locales/en-XL.json +18 -0
|
@@ -37,6 +37,8 @@ import metricsTrickyTti from './test-definitions/metrics-tricky-tti.js';
|
|
|
37
37
|
import metricsTrickyTtiLateFcp from './test-definitions/metrics-tricky-tti-late-fcp.js';
|
|
38
38
|
import oopifRequests from './test-definitions/oopif-requests.js';
|
|
39
39
|
import oopifScripts from './test-definitions/oopif-scripts.js';
|
|
40
|
+
import originIsolationCoopHeaderMissing from './test-definitions/origin-isolation-coop-header-missing.js';
|
|
41
|
+
import originIsolationCoopPresent from './test-definitions/origin-isolation-coop-present.js';
|
|
40
42
|
import perfDebug from './test-definitions/perf-debug.js';
|
|
41
43
|
import perfDiagnosticsAnimations from './test-definitions/perf-diagnostics-animations.js';
|
|
42
44
|
import perfDiagnosticsThirdParty from './test-definitions/perf-diagnostics-third-party.js';
|
|
@@ -97,6 +99,8 @@ const smokeTests = [
|
|
|
97
99
|
metricsTrickyTtiLateFcp,
|
|
98
100
|
oopifRequests,
|
|
99
101
|
oopifScripts,
|
|
102
|
+
originIsolationCoopHeaderMissing,
|
|
103
|
+
originIsolationCoopPresent,
|
|
100
104
|
perfDebug,
|
|
101
105
|
perfDiagnosticsAnimations,
|
|
102
106
|
perfDiagnosticsThirdParty,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export default OriginIsolation;
|
|
2
|
+
declare class OriginIsolation extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {LH.Artifacts} artifacts
|
|
5
|
+
* @param {LH.Audit.Context} context
|
|
6
|
+
* @return {Promise<string[]>}
|
|
7
|
+
*/
|
|
8
|
+
static getRawCoop(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* @param {string | undefined} coopDirective
|
|
11
|
+
* @param {LH.IcuMessage | string} findingDescription
|
|
12
|
+
* @param {LH.IcuMessage=} severity
|
|
13
|
+
* @return {LH.Audit.Details.TableItem}
|
|
14
|
+
*/
|
|
15
|
+
static findingToTableItem(coopDirective: string | undefined, findingDescription: LH.IcuMessage | string, severity?: LH.IcuMessage | undefined): LH.Audit.Details.TableItem;
|
|
16
|
+
/**
|
|
17
|
+
* @param {string[]} coopHeaders
|
|
18
|
+
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
|
|
19
|
+
*/
|
|
20
|
+
static constructResults(coopHeaders: string[]): {
|
|
21
|
+
score: number;
|
|
22
|
+
results: LH.Audit.Details.TableItem[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @param {LH.Artifacts} artifacts
|
|
26
|
+
* @param {LH.Audit.Context} context
|
|
27
|
+
* @return {Promise<LH.Audit.Product>}
|
|
28
|
+
*/
|
|
29
|
+
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
30
|
+
}
|
|
31
|
+
export namespace UIStrings {
|
|
32
|
+
let title: string;
|
|
33
|
+
let description: string;
|
|
34
|
+
let noCoop: string;
|
|
35
|
+
let invalidSyntax: string;
|
|
36
|
+
let columnDirective: string;
|
|
37
|
+
let columnSeverity: string;
|
|
38
|
+
}
|
|
39
|
+
import { Audit } from './audit.js';
|
|
40
|
+
//# sourceMappingURL=origin-isolation.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2024 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Audit} from './audit.js';
|
|
8
|
+
import {MainResource} from '../computed/main-resource.js';
|
|
9
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
10
|
+
|
|
11
|
+
const UIStrings = {
|
|
12
|
+
/** Title of a Lighthouse audit that evaluates the security of a page's COOP header for origin isolation. "COOP" stands for "Cross-Origin-Opener-Policy" and should not be translated. */
|
|
13
|
+
title: 'Ensure proper origin isolation with COOP',
|
|
14
|
+
/** Description of a Lighthouse audit that evaluates the security of a page's COOP header for origin isolation. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. "COOP" stands for "Cross-Origin-Opener-Policy", neither should be translated. */
|
|
15
|
+
description: 'The Cross-Origin-Opener-Policy (COOP) can be used to isolate the top-level window from other documents such as pop-ups. [Learn more about deploying the COOP header.](https://web.dev/articles/why-coop-coep#coop)',
|
|
16
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the COOP header for origin isolation. This is displayed if no COOP header is deployed. "COOP" stands for "Cross-Origin-Opener-Policy" and should not be translated. */
|
|
17
|
+
noCoop: 'No COOP header found',
|
|
18
|
+
/** Table item value calling out the presence of a syntax error. */
|
|
19
|
+
invalidSyntax: 'Invalid syntax',
|
|
20
|
+
/** Label for a column in a data table; entries will be a directive of the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */
|
|
21
|
+
columnDirective: 'Directive',
|
|
22
|
+
/** Label for a column in a data table; entries will be the severity of an issue with the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */
|
|
23
|
+
columnSeverity: 'Severity',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
27
|
+
|
|
28
|
+
class OriginIsolation extends Audit {
|
|
29
|
+
/**
|
|
30
|
+
* @return {LH.Audit.Meta}
|
|
31
|
+
*/
|
|
32
|
+
static get meta() {
|
|
33
|
+
return {
|
|
34
|
+
id: 'origin-isolation',
|
|
35
|
+
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
|
|
36
|
+
title: str_(UIStrings.title),
|
|
37
|
+
description: str_(UIStrings.description),
|
|
38
|
+
requiredArtifacts: ['devtoolsLogs', 'URL'],
|
|
39
|
+
supportedModes: ['navigation'],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {LH.Artifacts} artifacts
|
|
46
|
+
* @param {LH.Audit.Context} context
|
|
47
|
+
* @return {Promise<string[]>}
|
|
48
|
+
*/
|
|
49
|
+
static async getRawCoop(artifacts, context) {
|
|
50
|
+
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
51
|
+
const mainResource =
|
|
52
|
+
await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
53
|
+
|
|
54
|
+
let coopHeaders =
|
|
55
|
+
mainResource.responseHeaders
|
|
56
|
+
.filter(h => {
|
|
57
|
+
return h.name.toLowerCase() === 'cross-origin-opener-policy';
|
|
58
|
+
})
|
|
59
|
+
.flatMap(h => h.value);
|
|
60
|
+
|
|
61
|
+
// Sanitize the header value.
|
|
62
|
+
coopHeaders = coopHeaders.map(v => v.toLowerCase().replace(/\s/g, ''));
|
|
63
|
+
|
|
64
|
+
return coopHeaders;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string | undefined} coopDirective
|
|
69
|
+
* @param {LH.IcuMessage | string} findingDescription
|
|
70
|
+
* @param {LH.IcuMessage=} severity
|
|
71
|
+
* @return {LH.Audit.Details.TableItem}
|
|
72
|
+
*/
|
|
73
|
+
static findingToTableItem(coopDirective, findingDescription, severity) {
|
|
74
|
+
return {
|
|
75
|
+
directive: coopDirective,
|
|
76
|
+
description: findingDescription,
|
|
77
|
+
severity,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string[]} coopHeaders
|
|
83
|
+
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
|
|
84
|
+
*/
|
|
85
|
+
static constructResults(coopHeaders) {
|
|
86
|
+
const rawCoop = [...coopHeaders];
|
|
87
|
+
const allowedDirectives = [
|
|
88
|
+
'unsafe-none', 'same-origin-allow-popups', 'same-origin',
|
|
89
|
+
'noopener-allow-popups',
|
|
90
|
+
];
|
|
91
|
+
const violations = [];
|
|
92
|
+
const syntax = [];
|
|
93
|
+
|
|
94
|
+
if (!rawCoop.length) {
|
|
95
|
+
violations.push({
|
|
96
|
+
severity: str_(i18n.UIStrings.itemSeverityHigh),
|
|
97
|
+
description: str_(UIStrings.noCoop),
|
|
98
|
+
directive: undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const actualDirective of coopHeaders) {
|
|
103
|
+
// If there is a directive that's not an official COOP directive.
|
|
104
|
+
if (!allowedDirectives.includes(actualDirective)) {
|
|
105
|
+
syntax.push({
|
|
106
|
+
severity: str_(i18n.UIStrings.itemSeverityLow),
|
|
107
|
+
description: str_(UIStrings.invalidSyntax),
|
|
108
|
+
directive: actualDirective,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const results = [
|
|
114
|
+
...violations.map(
|
|
115
|
+
f => this.findingToTableItem(
|
|
116
|
+
f.directive, f.description,
|
|
117
|
+
str_(i18n.UIStrings.itemSeverityHigh))),
|
|
118
|
+
...syntax.map(
|
|
119
|
+
f => this.findingToTableItem(
|
|
120
|
+
f.directive, f.description,
|
|
121
|
+
str_(i18n.UIStrings.itemSeverityLow))),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
return {score: violations.length || syntax.length ? 0 : 1, results};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {LH.Artifacts} artifacts
|
|
129
|
+
* @param {LH.Audit.Context} context
|
|
130
|
+
* @return {Promise<LH.Audit.Product>}
|
|
131
|
+
*/
|
|
132
|
+
static async audit(artifacts, context) {
|
|
133
|
+
const coopHeaders = await this.getRawCoop(artifacts, context);
|
|
134
|
+
const {score, results} = this.constructResults(coopHeaders);
|
|
135
|
+
|
|
136
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
137
|
+
const headings = [
|
|
138
|
+
/* eslint-disable max-len */
|
|
139
|
+
{key: 'description', valueType: 'text', subItemsHeading: {key: 'description'}, label: str_(i18n.UIStrings.columnDescription)},
|
|
140
|
+
{key: 'directive', valueType: 'code', subItemsHeading: {key: 'directive'}, label: str_(UIStrings.columnDirective)},
|
|
141
|
+
{key: 'severity', valueType: 'text', subItemsHeading: {key: 'severity'}, label: str_(UIStrings.columnSeverity)},
|
|
142
|
+
/* eslint-enable max-len */
|
|
143
|
+
];
|
|
144
|
+
const details = Audit.makeTableDetails(headings, results);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
score,
|
|
148
|
+
notApplicable: !results.length,
|
|
149
|
+
details,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default OriginIsolation;
|
|
155
|
+
export {UIStrings};
|
|
@@ -193,6 +193,7 @@ const defaultConfig = {
|
|
|
193
193
|
'prioritize-lcp-image',
|
|
194
194
|
'csp-xss',
|
|
195
195
|
'has-hsts',
|
|
196
|
+
'origin-isolation',
|
|
196
197
|
'script-treemap-data',
|
|
197
198
|
'accessibility/accesskeys',
|
|
198
199
|
'accessibility/aria-allowed-attr',
|
|
@@ -543,6 +544,7 @@ const defaultConfig = {
|
|
|
543
544
|
{id: 'notification-on-start', weight: 1, group: 'best-practices-trust-safety'},
|
|
544
545
|
{id: 'csp-xss', weight: 0, group: 'best-practices-trust-safety'},
|
|
545
546
|
{id: 'has-hsts', weight: 0, group: 'best-practices-trust-safety'},
|
|
547
|
+
{id: 'origin-isolation', weight: 0, group: 'best-practices-trust-safety'},
|
|
546
548
|
// User Experience
|
|
547
549
|
{id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'},
|
|
548
550
|
{id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
|
|
@@ -221,8 +221,9 @@ class FontSize extends BaseGatherer {
|
|
|
221
221
|
|
|
222
222
|
const nodeIndex = doc.layout.nodeIndex[layoutIndex];
|
|
223
223
|
const styles = doc.layout.styles[layoutIndex];
|
|
224
|
-
const [fontSizeStringId] = styles;
|
|
224
|
+
const [fontSizeStringId, visibilityStringId] = styles;
|
|
225
225
|
const fontSize = getFloat(fontSizeStringId);
|
|
226
|
+
const visibility = getString(visibilityStringId);
|
|
226
227
|
|
|
227
228
|
const parentIndex = nodes.parentIndex[nodeIndex];
|
|
228
229
|
const grandParentIndex = nodes.parentIndex[parentIndex];
|
|
@@ -234,6 +235,7 @@ class FontSize extends BaseGatherer {
|
|
|
234
235
|
nodeIndex,
|
|
235
236
|
backendNodeId: nodes.backendNodeId[nodeIndex],
|
|
236
237
|
fontSize,
|
|
238
|
+
visibility,
|
|
237
239
|
textLength: getTextLength(text),
|
|
238
240
|
parentNode: {
|
|
239
241
|
...parentNode,
|
|
@@ -257,17 +259,24 @@ class FontSize extends BaseGatherer {
|
|
|
257
259
|
let failingTextLength = 0;
|
|
258
260
|
|
|
259
261
|
for (const textNodeData of this.getTextNodesInLayoutFromSnapshot(snapshot)) {
|
|
262
|
+
if (textNodeData.visibility === 'hidden') {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
260
266
|
totalTextLength += textNodeData.textLength;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
failingNodes.push({
|
|
265
|
-
nodeId: 0, // Set later in fetchFailingNodeSourceRules.
|
|
266
|
-
parentNode: textNodeData.parentNode,
|
|
267
|
-
textLength: textNodeData.textLength,
|
|
268
|
-
fontSize: textNodeData.fontSize,
|
|
269
|
-
});
|
|
267
|
+
|
|
268
|
+
if (textNodeData.fontSize >= MINIMAL_LEGIBLE_FONT_SIZE_PX) {
|
|
269
|
+
continue;
|
|
270
270
|
}
|
|
271
|
+
|
|
272
|
+
// Once a bad TextNode is identified, its parent Node is needed.
|
|
273
|
+
failingTextLength += textNodeData.textLength;
|
|
274
|
+
failingNodes.push({
|
|
275
|
+
nodeId: 0, // Set later in fetchFailingNodeSourceRules.
|
|
276
|
+
parentNode: textNodeData.parentNode,
|
|
277
|
+
textLength: textNodeData.textLength,
|
|
278
|
+
fontSize: textNodeData.fontSize,
|
|
279
|
+
});
|
|
271
280
|
}
|
|
272
281
|
|
|
273
282
|
return {totalTextLength, failingTextLength, failingNodes};
|
|
@@ -294,7 +303,7 @@ class FontSize extends BaseGatherer {
|
|
|
294
303
|
|
|
295
304
|
// Get the computed font-size style of every node.
|
|
296
305
|
const snapshot = await session.sendCommand('DOMSnapshot.captureSnapshot', {
|
|
297
|
-
computedStyles: ['font-size'],
|
|
306
|
+
computedStyles: ['font-size', 'visibility'],
|
|
298
307
|
});
|
|
299
308
|
|
|
300
309
|
const {
|
package/package.json
CHANGED
|
@@ -1217,6 +1217,24 @@
|
|
|
1217
1217
|
"core/audits/non-composited-animations.js | unsupportedTimingParameters": {
|
|
1218
1218
|
"message": "Effect has unsupported timing parameters"
|
|
1219
1219
|
},
|
|
1220
|
+
"core/audits/origin-isolation.js | columnDirective": {
|
|
1221
|
+
"message": "Directive"
|
|
1222
|
+
},
|
|
1223
|
+
"core/audits/origin-isolation.js | columnSeverity": {
|
|
1224
|
+
"message": "Severity"
|
|
1225
|
+
},
|
|
1226
|
+
"core/audits/origin-isolation.js | description": {
|
|
1227
|
+
"message": "The Cross-Origin-Opener-Policy (COOP) can be used to isolate the top-level window from other documents such as pop-ups. [Learn more about deploying the COOP header.](https://web.dev/articles/why-coop-coep#coop)"
|
|
1228
|
+
},
|
|
1229
|
+
"core/audits/origin-isolation.js | invalidSyntax": {
|
|
1230
|
+
"message": "Invalid syntax"
|
|
1231
|
+
},
|
|
1232
|
+
"core/audits/origin-isolation.js | noCoop": {
|
|
1233
|
+
"message": "No COOP header found"
|
|
1234
|
+
},
|
|
1235
|
+
"core/audits/origin-isolation.js | title": {
|
|
1236
|
+
"message": "Ensure proper origin isolation with COOP"
|
|
1237
|
+
},
|
|
1220
1238
|
"core/audits/preload-fonts.js | description": {
|
|
1221
1239
|
"message": "Preload `optional` fonts so first-time visitors may use them. [Learn more about preloading fonts](https://web.dev/articles/preload-optional-fonts)"
|
|
1222
1240
|
},
|
|
@@ -1217,6 +1217,24 @@
|
|
|
1217
1217
|
"core/audits/non-composited-animations.js | unsupportedTimingParameters": {
|
|
1218
1218
|
"message": "Êf́f̂éĉt́ ĥáŝ ún̂śûṕp̂ór̂t́êd́ t̂ím̂ín̂ǵ p̂ár̂ám̂ét̂ér̂ś"
|
|
1219
1219
|
},
|
|
1220
|
+
"core/audits/origin-isolation.js | columnDirective": {
|
|
1221
|
+
"message": "D̂ír̂éĉt́îv́ê"
|
|
1222
|
+
},
|
|
1223
|
+
"core/audits/origin-isolation.js | columnSeverity": {
|
|
1224
|
+
"message": "Ŝév̂ér̂ít̂ý"
|
|
1225
|
+
},
|
|
1226
|
+
"core/audits/origin-isolation.js | description": {
|
|
1227
|
+
"message": "T̂h́ê Ćr̂óŝś-Ôŕîǵîń-Ôṕêńêŕ-P̂ól̂íĉý (ĈÓÔṔ) ĉán̂ b́ê úŝéd̂ t́ô íŝól̂át̂é t̂h́ê t́ôṕ-l̂év̂él̂ ẃîńd̂óŵ f́r̂óm̂ ót̂h́êŕ d̂óĉúm̂én̂t́ŝ śûćĥ áŝ ṕôṕ-ûṕŝ. [Ĺêár̂ń m̂ór̂é âb́ôút̂ d́êṕl̂óŷín̂ǵ t̂h́ê ĆÔÓP̂ h́êád̂ér̂.](https://web.dev/articles/why-coop-coep#coop)"
|
|
1228
|
+
},
|
|
1229
|
+
"core/audits/origin-isolation.js | invalidSyntax": {
|
|
1230
|
+
"message": "Îńv̂ál̂íd̂ śŷńt̂áx̂"
|
|
1231
|
+
},
|
|
1232
|
+
"core/audits/origin-isolation.js | noCoop": {
|
|
1233
|
+
"message": "N̂ó ĈÓÔṔ ĥéâd́êŕ f̂óûńd̂"
|
|
1234
|
+
},
|
|
1235
|
+
"core/audits/origin-isolation.js | title": {
|
|
1236
|
+
"message": "Êńŝúr̂é p̂ŕôṕêŕ ôŕîǵîń îśôĺât́îón̂ ẃît́ĥ ĆÔÓP̂"
|
|
1237
|
+
},
|
|
1220
1238
|
"core/audits/preload-fonts.js | description": {
|
|
1221
1239
|
"message": "P̂ŕêĺôád̂ `optional` f́ôńt̂ś ŝó f̂ír̂śt̂-t́îḿê v́îśît́ôŕŝ ḿâý ûśê t́ĥém̂. [Ĺêár̂ń m̂ór̂é âb́ôút̂ ṕr̂él̂óâd́îńĝ f́ôńt̂ś](https://web.dev/articles/preload-optional-fonts)"
|
|
1222
1240
|
},
|