lighthouse 12.2.3-dev.20241209 → 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.
@@ -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'},
@@ -52,6 +52,7 @@ declare class FontSize extends BaseGatherer {
52
52
  nodeIndex: number;
53
53
  backendNodeId: number;
54
54
  fontSize: number;
55
+ visibility: string;
55
56
  textLength: number;
56
57
  parentNode: {
57
58
  parentNode: {
@@ -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
- if (textNodeData.fontSize < MINIMAL_LEGIBLE_FONT_SIZE_PX) {
262
- // Once a bad TextNode is identified, its parent Node is needed.
263
- failingTextLength += textNodeData.textLength;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "12.2.3-dev.20241209",
4
+ "version": "12.2.3-dev.20241211",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -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
  },