diff --git a/cli/test/smokehouse/core-tests.js b/cli/test/smokehouse/core-tests.js index 294e73b036ab..7828504ac0a7 100644 --- a/cli/test/smokehouse/core-tests.js +++ b/cli/test/smokehouse/core-tests.js @@ -37,6 +37,8 @@ import metricsTrickyTti from './test-definitions/metrics-tricky-tti.js'; import metricsTrickyTtiLateFcp from './test-definitions/metrics-tricky-tti-late-fcp.js'; import oopifRequests from './test-definitions/oopif-requests.js'; import oopifScripts from './test-definitions/oopif-scripts.js'; +import originIsolationCoopHeaderMissing from './test-definitions/origin-isolation-coop-header-missing.js'; +import originIsolationCoopPresent from './test-definitions/origin-isolation-coop-present.js'; import perfDebug from './test-definitions/perf-debug.js'; import perfDiagnosticsAnimations from './test-definitions/perf-diagnostics-animations.js'; import perfDiagnosticsThirdParty from './test-definitions/perf-diagnostics-third-party.js'; @@ -97,6 +99,8 @@ const smokeTests = [ metricsTrickyTtiLateFcp, oopifRequests, oopifScripts, + originIsolationCoopHeaderMissing, + originIsolationCoopPresent, perfDebug, perfDiagnosticsAnimations, perfDiagnosticsThirdParty, diff --git a/cli/test/smokehouse/test-definitions/origin-isolation-coop-header-missing.js b/cli/test/smokehouse/test-definitions/origin-isolation-coop-header-missing.js new file mode 100644 index 000000000000..233280634f89 --- /dev/null +++ b/cli/test/smokehouse/test-definitions/origin-isolation-coop-header-missing.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @type {Smokehouse.ExpectedRunnerResult} + * Expected Lighthouse results for a site with a missing COOP header. + */ +const expectations = { + lhr: { + requestedUrl: 'https://example.com/', + finalDisplayedUrl: 'https://example.com/', + audits: { + 'origin-isolation': { + score: 1, + details: { + items: [ + { + description: 'No COOP header found', + severity: 'High', + }, + ], + }, + }, + }, + }, +}; + +export default { + id: 'origin-isolation-coop-header-missing', + expectations, +}; diff --git a/cli/test/smokehouse/test-definitions/origin-isolation-coop-present.js b/cli/test/smokehouse/test-definitions/origin-isolation-coop-present.js new file mode 100644 index 000000000000..7fdda83b8851 --- /dev/null +++ b/cli/test/smokehouse/test-definitions/origin-isolation-coop-present.js @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @type {Smokehouse.ExpectedRunnerResult} + * Expected Lighthouse results for a site with a configured COOP header. + */ +const expectations = { + lhr: { + requestedUrl: 'https://csp.withgoogle.com/docs/index.html', + finalDisplayedUrl: 'https://csp.withgoogle.com/docs/index.html', + audits: { + 'origin-isolation': { + score: null, + }, + }, + }, +}; + +export default { + id: 'origin-isolation-coop-present', + expectations, +}; diff --git a/core/audits/origin-isolation.js b/core/audits/origin-isolation.js new file mode 100644 index 000000000000..30962fa2bfd9 --- /dev/null +++ b/core/audits/origin-isolation.js @@ -0,0 +1,155 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Audit} from './audit.js'; +import {MainResource} from '../computed/main-resource.js'; +import * as i18n from '../lib/i18n/i18n.js'; + +const UIStrings = { + /** 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. */ + title: 'Ensure proper origin isolation with COOP', + /** 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. */ + 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)', + /** 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. */ + noCoop: 'No COOP header found', + /** Table item value calling out the presence of a syntax error. */ + invalidSyntax: 'Invalid syntax', + /** Label for a column in a data table; entries will be a directive of the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */ + columnDirective: 'Directive', + /** 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". */ + columnSeverity: 'Severity', +}; + +const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); + +class OriginIsolation extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'origin-isolation', + scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE, + title: str_(UIStrings.title), + description: str_(UIStrings.description), + requiredArtifacts: ['devtoolsLogs', 'URL'], + supportedModes: ['navigation'], + }; + } + + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async getRawCoop(artifacts, context) { + const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; + const mainResource = + await MainResource.request({devtoolsLog, URL: artifacts.URL}, context); + + let coopHeaders = + mainResource.responseHeaders + .filter(h => { + return h.name.toLowerCase() === 'cross-origin-opener-policy'; + }) + .flatMap(h => h.value); + + // Sanitize the header value. + coopHeaders = coopHeaders.map(v => v.toLowerCase().replace(/\s/g, '')); + + return coopHeaders; + } + + /** + * @param {string | undefined} coopDirective + * @param {LH.IcuMessage | string} findingDescription + * @param {LH.IcuMessage=} severity + * @return {LH.Audit.Details.TableItem} + */ + static findingToTableItem(coopDirective, findingDescription, severity) { + return { + directive: coopDirective, + description: findingDescription, + severity, + }; + } + + /** + * @param {string[]} coopHeaders + * @return {{score: number, results: LH.Audit.Details.TableItem[]}} + */ + static constructResults(coopHeaders) { + const rawCoop = [...coopHeaders]; + const allowedDirectives = [ + 'unsafe-none', 'same-origin-allow-popups', 'same-origin', + 'noopener-allow-popups', + ]; + const violations = []; + const syntax = []; + + if (!rawCoop.length) { + violations.push({ + severity: str_(i18n.UIStrings.itemSeverityHigh), + description: str_(UIStrings.noCoop), + directive: undefined, + }); + } + + for (const actualDirective of coopHeaders) { + // If there is a directive that's not an official COOP directive. + if (!allowedDirectives.includes(actualDirective)) { + syntax.push({ + severity: str_(i18n.UIStrings.itemSeverityLow), + description: str_(UIStrings.invalidSyntax), + directive: actualDirective, + }); + } + } + + const results = [ + ...violations.map( + f => this.findingToTableItem( + f.directive, f.description, + str_(i18n.UIStrings.itemSeverityHigh))), + ...syntax.map( + f => this.findingToTableItem( + f.directive, f.description, + str_(i18n.UIStrings.itemSeverityLow))), + ]; + + return {score: violations.length || syntax.length ? 0 : 1, results}; + } + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async audit(artifacts, context) { + const coopHeaders = await this.getRawCoop(artifacts, context); + const {score, results} = this.constructResults(coopHeaders); + + /** @type {LH.Audit.Details.Table['headings']} */ + const headings = [ + /* eslint-disable max-len */ + {key: 'description', valueType: 'text', subItemsHeading: {key: 'description'}, label: str_(i18n.UIStrings.columnDescription)}, + {key: 'directive', valueType: 'code', subItemsHeading: {key: 'directive'}, label: str_(UIStrings.columnDirective)}, + {key: 'severity', valueType: 'text', subItemsHeading: {key: 'severity'}, label: str_(UIStrings.columnSeverity)}, + /* eslint-enable max-len */ + ]; + const details = Audit.makeTableDetails(headings, results); + + return { + score, + notApplicable: !results.length, + details, + }; + } +} + +export default OriginIsolation; +export {UIStrings}; diff --git a/core/config/default-config.js b/core/config/default-config.js index f11177609340..abba05f618b6 100644 --- a/core/config/default-config.js +++ b/core/config/default-config.js @@ -193,6 +193,7 @@ const defaultConfig = { 'prioritize-lcp-image', 'csp-xss', 'has-hsts', + 'origin-isolation', 'script-treemap-data', 'accessibility/accesskeys', 'accessibility/aria-allowed-attr', @@ -543,6 +544,7 @@ const defaultConfig = { {id: 'notification-on-start', weight: 1, group: 'best-practices-trust-safety'}, {id: 'csp-xss', weight: 0, group: 'best-practices-trust-safety'}, {id: 'has-hsts', weight: 0, group: 'best-practices-trust-safety'}, + {id: 'origin-isolation', weight: 0, group: 'best-practices-trust-safety'}, // User Experience {id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'}, {id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'}, diff --git a/core/scripts/i18n/collect-strings.js b/core/scripts/i18n/collect-strings.js index 63cbe4dcce89..d5283204a6f5 100644 --- a/core/scripts/i18n/collect-strings.js +++ b/core/scripts/i18n/collect-strings.js @@ -731,6 +731,7 @@ function checkKnownFixedCollisions(strings) { 'Consider uploading your GIF to a service which will make it available to embed as an HTML5 video.', 'Directive', 'Directive', + 'Directive', 'Document contains a $MARKDOWN_SNIPPET_0$ that triggers $MARKDOWN_SNIPPET_1$', 'Document contains a $MARKDOWN_SNIPPET_0$ that triggers $MARKDOWN_SNIPPET_1$', 'Document has a valid $MARKDOWN_SNIPPET_0$', @@ -751,6 +752,7 @@ function checkKnownFixedCollisions(strings) { 'Potential Savings', 'Severity', 'Severity', + 'Severity', 'The page was evicted from the cache to allow another page to be cached.', 'The page was evicted from the cache to allow another page to be cached.', 'Use $MARKDOWN_SNIPPET_0$ to detect unused JavaScript code. $LINK_START_0$Learn more$LINK_END_0$', diff --git a/core/test/audits/origin-isolation-test.js b/core/test/audits/origin-isolation-test.js new file mode 100644 index 000000000000..640c20cd5517 --- /dev/null +++ b/core/test/audits/origin-isolation-test.js @@ -0,0 +1,217 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import OriginIsolation from '../../audits/origin-isolation.js'; +import {networkRecordsToDevtoolsLog} from '../network-records-to-devtools-log.js'; + +it('marked N/A if no violations found', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + {name: 'Cross-Origin-Opener-Policy', value: `same-origin`}, + ], + }, + ]), + }, + }; + const results = + await OriginIsolation.audit(artifacts, {computedCache: new Map()}); + expect(results.details.items).toHaveLength(0); + expect(results.notApplicable).toBeTruthy(); +}); + +it('No COOP header found', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + {name: 'Foo-Header', value: `same-origin`}, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = + await OriginIsolation.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('High'); + expect(results.details.items[0].description) + .toBeDisplayString('No COOP header found'); + expect(results.details.items).toMatchObject([ + { + directive: undefined, + }, + ]); +}); + +it('Messed up directive.', async () => { + const artifacts = { + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + {name: 'Cross-Origin-Opener-Policy', value: `fooDirective`}, + ], + }, + ]), + }, + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + }; + + const results = + await OriginIsolation.audit(artifacts, {computedCache: new Map()}); + expect(results.notApplicable).toBeFalsy(); + expect(results.details.items[0].severity).toBeDisplayString('Low'); + expect(results.details.items[0].description) + .toBeDisplayString('Invalid syntax'); + expect(results.details.items).toMatchObject([ + { + directive: 'foodirective', + }, + ]); +}); + +describe('getRawCoop', () => { + it('basic case', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Cross-Origin-Opener-Policy', + value: `same-origin`, + }, + ], + }, + ]), + }, + }; + const coopHeaders = + await OriginIsolation.getRawCoop(artifacts, {computedCache: new Map()}); + expect(coopHeaders).toEqual([ + `same-origin`, + ]); + }); + + it('ignore if empty', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Cross-Origin-Opener-Policy', + value: ``, + }, + ], + }, + ]), + }, + }; + const coopHeaders = + await OriginIsolation.getRawCoop(artifacts, {computedCache: new Map()}); + expect(coopHeaders).toEqual([ + ``, + ]); + }); + + it('ignore if only whitespace', async () => { + const artifacts = { + URL: { + requestedUrl: 'https://example.com', + mainDocumentUrl: 'https://example.com', + finalDisplayedUrl: 'https://example.com', + }, + devtoolsLogs: { + defaultPass: networkRecordsToDevtoolsLog([ + { + url: 'https://example.com', + responseHeaders: [ + { + name: 'Cross-Origin-Opener-Policy', + value: ' \t', + }, + ], + }, + ]), + }, + }; + const coopHeaders = + await OriginIsolation.getRawCoop(artifacts, {computedCache: new Map()}); + expect(coopHeaders).toEqual([ + ``, + ]); + }); +}); + +describe('constructResults', () => { + it('passes with no findings', () => { + const {score, results} = OriginIsolation.constructResults(['same-origin']); + expect(score).toEqual(1); + expect(results).toEqual([]); + }); + + it('constructs result based on misconfigured COOP header', () => { + const {score, results} = + OriginIsolation.constructResults(['foo-directive']); + expect(score).toEqual(0); + expect(results[0].severity).toBeDisplayString('Low'); + expect(results[0].description) + .toBeDisplayString('Invalid syntax'); + expect(results).toMatchObject([ + { + directive: 'foo-directive', + }, + ]); + }); + + it('returns single item for no COOP', () => { + const {score, results} = OriginIsolation.constructResults([]); + expect(score).toEqual(0); + expect(results[0].severity).toBeDisplayString('High'); + expect(results[0].description) + .toBeDisplayString('No COOP header found'); + expect(results).toMatchObject([ + { + directive: undefined, + }, + ]); + }); +}); diff --git a/core/test/fixtures/user-flows/reports/sample-flow-result.json b/core/test/fixtures/user-flows/reports/sample-flow-result.json index 7b6134fdaea7..cb42263486ad 100644 --- a/core/test/fixtures/user-flows/reports/sample-flow-result.json +++ b/core/test/fixtures/user-flows/reports/sample-flow-result.json @@ -2152,6 +2152,48 @@ ] } }, + "origin-isolation": { + "id": "origin-isolation", + "title": "Ensure proper origin isolation with COOP", + "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)", + "score": 1, + "scoreDisplayMode": "informative", + "details": { + "type": "table", + "headings": [ + { + "key": "description", + "valueType": "text", + "subItemsHeading": { + "key": "description" + }, + "label": "Description" + }, + { + "key": "directive", + "valueType": "code", + "subItemsHeading": { + "key": "directive" + }, + "label": "Directive" + }, + { + "key": "severity", + "valueType": "text", + "subItemsHeading": { + "key": "severity" + }, + "label": "Severity" + } + ], + "items": [ + { + "description": "No COOP header found", + "severity": "High" + } + ] + } + }, "script-treemap-data": { "id": "script-treemap-data", "title": "Script Treemap Data", @@ -4626,6 +4668,11 @@ "weight": 0, "group": "best-practices-trust-safety" }, + { + "id": "origin-isolation", + "weight": 0, + "group": "best-practices-trust-safety" + }, { "id": "paste-preventing-inputs", "weight": 3, @@ -6207,19 +6254,19 @@ }, { "startTime": 136, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:origin-isolation", "duration": 1, "entryType": "measure" }, { "startTime": 137, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 138, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -6303,690 +6350,696 @@ }, { "startTime": 152, - "name": "lh:audit:accesskeys", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 153, - "name": "lh:audit:aria-allowed-attr", + "name": "lh:audit:accesskeys", "duration": 1, "entryType": "measure" }, { "startTime": 154, - "name": "lh:audit:aria-allowed-role", + "name": "lh:audit:aria-allowed-attr", "duration": 1, "entryType": "measure" }, { "startTime": 155, - "name": "lh:audit:aria-command-name", + "name": "lh:audit:aria-allowed-role", "duration": 1, "entryType": "measure" }, { "startTime": 156, - "name": "lh:audit:aria-conditional-attr", + "name": "lh:audit:aria-command-name", "duration": 1, "entryType": "measure" }, { "startTime": 157, - "name": "lh:audit:aria-deprecated-role", + "name": "lh:audit:aria-conditional-attr", "duration": 1, "entryType": "measure" }, { "startTime": 158, - "name": "lh:audit:aria-dialog-name", + "name": "lh:audit:aria-deprecated-role", "duration": 1, "entryType": "measure" }, { "startTime": 159, - "name": "lh:audit:aria-hidden-body", + "name": "lh:audit:aria-dialog-name", "duration": 1, "entryType": "measure" }, { "startTime": 160, - "name": "lh:audit:aria-hidden-focus", + "name": "lh:audit:aria-hidden-body", "duration": 1, "entryType": "measure" }, { "startTime": 161, - "name": "lh:audit:aria-input-field-name", + "name": "lh:audit:aria-hidden-focus", "duration": 1, "entryType": "measure" }, { "startTime": 162, - "name": "lh:audit:aria-meter-name", + "name": "lh:audit:aria-input-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 163, - "name": "lh:audit:aria-progressbar-name", + "name": "lh:audit:aria-meter-name", "duration": 1, "entryType": "measure" }, { "startTime": 164, - "name": "lh:audit:aria-prohibited-attr", + "name": "lh:audit:aria-progressbar-name", "duration": 1, "entryType": "measure" }, { "startTime": 165, - "name": "lh:audit:aria-required-attr", + "name": "lh:audit:aria-prohibited-attr", "duration": 1, "entryType": "measure" }, { "startTime": 166, - "name": "lh:audit:aria-required-children", + "name": "lh:audit:aria-required-attr", "duration": 1, "entryType": "measure" }, { "startTime": 167, - "name": "lh:audit:aria-required-parent", + "name": "lh:audit:aria-required-children", "duration": 1, "entryType": "measure" }, { "startTime": 168, - "name": "lh:audit:aria-roles", + "name": "lh:audit:aria-required-parent", "duration": 1, "entryType": "measure" }, { "startTime": 169, - "name": "lh:audit:aria-text", + "name": "lh:audit:aria-roles", "duration": 1, "entryType": "measure" }, { "startTime": 170, - "name": "lh:audit:aria-toggle-field-name", + "name": "lh:audit:aria-text", "duration": 1, "entryType": "measure" }, { "startTime": 171, - "name": "lh:audit:aria-tooltip-name", + "name": "lh:audit:aria-toggle-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 172, - "name": "lh:audit:aria-treeitem-name", + "name": "lh:audit:aria-tooltip-name", "duration": 1, "entryType": "measure" }, { "startTime": 173, - "name": "lh:audit:aria-valid-attr-value", + "name": "lh:audit:aria-treeitem-name", "duration": 1, "entryType": "measure" }, { "startTime": 174, - "name": "lh:audit:aria-valid-attr", + "name": "lh:audit:aria-valid-attr-value", "duration": 1, "entryType": "measure" }, { "startTime": 175, - "name": "lh:audit:button-name", + "name": "lh:audit:aria-valid-attr", "duration": 1, "entryType": "measure" }, { "startTime": 176, - "name": "lh:audit:bypass", + "name": "lh:audit:button-name", "duration": 1, "entryType": "measure" }, { "startTime": 177, - "name": "lh:audit:color-contrast", + "name": "lh:audit:bypass", "duration": 1, "entryType": "measure" }, { "startTime": 178, - "name": "lh:audit:definition-list", + "name": "lh:audit:color-contrast", "duration": 1, "entryType": "measure" }, { "startTime": 179, - "name": "lh:audit:dlitem", + "name": "lh:audit:definition-list", "duration": 1, "entryType": "measure" }, { "startTime": 180, - "name": "lh:audit:document-title", + "name": "lh:audit:dlitem", "duration": 1, "entryType": "measure" }, { "startTime": 181, - "name": "lh:audit:duplicate-id-aria", + "name": "lh:audit:document-title", "duration": 1, "entryType": "measure" }, { "startTime": 182, - "name": "lh:audit:empty-heading", + "name": "lh:audit:duplicate-id-aria", "duration": 1, "entryType": "measure" }, { "startTime": 183, - "name": "lh:audit:form-field-multiple-labels", + "name": "lh:audit:empty-heading", "duration": 1, "entryType": "measure" }, { "startTime": 184, - "name": "lh:audit:frame-title", + "name": "lh:audit:form-field-multiple-labels", "duration": 1, "entryType": "measure" }, { "startTime": 185, - "name": "lh:audit:heading-order", + "name": "lh:audit:frame-title", "duration": 1, "entryType": "measure" }, { "startTime": 186, - "name": "lh:audit:html-has-lang", + "name": "lh:audit:heading-order", "duration": 1, "entryType": "measure" }, { "startTime": 187, - "name": "lh:audit:html-lang-valid", + "name": "lh:audit:html-has-lang", "duration": 1, "entryType": "measure" }, { "startTime": 188, - "name": "lh:audit:html-xml-lang-mismatch", + "name": "lh:audit:html-lang-valid", "duration": 1, "entryType": "measure" }, { "startTime": 189, - "name": "lh:audit:identical-links-same-purpose", + "name": "lh:audit:html-xml-lang-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 190, - "name": "lh:audit:image-alt", + "name": "lh:audit:identical-links-same-purpose", "duration": 1, "entryType": "measure" }, { "startTime": 191, - "name": "lh:audit:image-redundant-alt", + "name": "lh:audit:image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 192, - "name": "lh:audit:input-button-name", + "name": "lh:audit:image-redundant-alt", "duration": 1, "entryType": "measure" }, { "startTime": 193, - "name": "lh:audit:input-image-alt", + "name": "lh:audit:input-button-name", "duration": 1, "entryType": "measure" }, { "startTime": 194, - "name": "lh:audit:label-content-name-mismatch", + "name": "lh:audit:input-image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 195, - "name": "lh:audit:label", + "name": "lh:audit:label-content-name-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 196, - "name": "lh:audit:landmark-one-main", + "name": "lh:audit:label", "duration": 1, "entryType": "measure" }, { "startTime": 197, - "name": "lh:audit:link-name", + "name": "lh:audit:landmark-one-main", "duration": 1, "entryType": "measure" }, { "startTime": 198, - "name": "lh:audit:link-in-text-block", + "name": "lh:audit:link-name", "duration": 1, "entryType": "measure" }, { "startTime": 199, - "name": "lh:audit:list", + "name": "lh:audit:link-in-text-block", "duration": 1, "entryType": "measure" }, { "startTime": 200, - "name": "lh:audit:listitem", + "name": "lh:audit:list", "duration": 1, "entryType": "measure" }, { "startTime": 201, - "name": "lh:audit:meta-refresh", + "name": "lh:audit:listitem", "duration": 1, "entryType": "measure" }, { "startTime": 202, - "name": "lh:audit:meta-viewport", + "name": "lh:audit:meta-refresh", "duration": 1, "entryType": "measure" }, { "startTime": 203, - "name": "lh:audit:object-alt", + "name": "lh:audit:meta-viewport", "duration": 1, "entryType": "measure" }, { "startTime": 204, - "name": "lh:audit:select-name", + "name": "lh:audit:object-alt", "duration": 1, "entryType": "measure" }, { "startTime": 205, - "name": "lh:audit:skip-link", + "name": "lh:audit:select-name", "duration": 1, "entryType": "measure" }, { "startTime": 206, - "name": "lh:audit:tabindex", + "name": "lh:audit:skip-link", "duration": 1, "entryType": "measure" }, { "startTime": 207, - "name": "lh:audit:table-duplicate-name", + "name": "lh:audit:tabindex", "duration": 1, "entryType": "measure" }, { "startTime": 208, - "name": "lh:audit:table-fake-caption", + "name": "lh:audit:table-duplicate-name", "duration": 1, "entryType": "measure" }, { "startTime": 209, - "name": "lh:audit:target-size", + "name": "lh:audit:table-fake-caption", "duration": 1, "entryType": "measure" }, { "startTime": 210, - "name": "lh:audit:td-has-header", + "name": "lh:audit:target-size", "duration": 1, "entryType": "measure" }, { "startTime": 211, - "name": "lh:audit:td-headers-attr", + "name": "lh:audit:td-has-header", "duration": 1, "entryType": "measure" }, { "startTime": 212, - "name": "lh:audit:th-has-data-cells", + "name": "lh:audit:td-headers-attr", "duration": 1, "entryType": "measure" }, { "startTime": 213, - "name": "lh:audit:valid-lang", + "name": "lh:audit:th-has-data-cells", "duration": 1, "entryType": "measure" }, { "startTime": 214, - "name": "lh:audit:video-caption", + "name": "lh:audit:valid-lang", "duration": 1, "entryType": "measure" }, { "startTime": 215, - "name": "lh:audit:custom-controls-labels", + "name": "lh:audit:video-caption", "duration": 1, "entryType": "measure" }, { "startTime": 216, - "name": "lh:audit:custom-controls-roles", + "name": "lh:audit:custom-controls-labels", "duration": 1, "entryType": "measure" }, { "startTime": 217, - "name": "lh:audit:focus-traps", + "name": "lh:audit:custom-controls-roles", "duration": 1, "entryType": "measure" }, { "startTime": 218, - "name": "lh:audit:focusable-controls", + "name": "lh:audit:focus-traps", "duration": 1, "entryType": "measure" }, { "startTime": 219, - "name": "lh:audit:interactive-element-affordance", + "name": "lh:audit:focusable-controls", "duration": 1, "entryType": "measure" }, { "startTime": 220, - "name": "lh:audit:logical-tab-order", + "name": "lh:audit:interactive-element-affordance", "duration": 1, "entryType": "measure" }, { "startTime": 221, - "name": "lh:audit:managed-focus", + "name": "lh:audit:logical-tab-order", "duration": 1, "entryType": "measure" }, { "startTime": 222, - "name": "lh:audit:offscreen-content-hidden", + "name": "lh:audit:managed-focus", "duration": 1, "entryType": "measure" }, { "startTime": 223, - "name": "lh:audit:use-landmarks", + "name": "lh:audit:offscreen-content-hidden", "duration": 1, "entryType": "measure" }, { "startTime": 224, - "name": "lh:audit:visual-order-follows-dom", + "name": "lh:audit:use-landmarks", "duration": 1, "entryType": "measure" }, { "startTime": 225, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:audit:visual-order-follows-dom", "duration": 1, "entryType": "measure" }, { "startTime": 226, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 227, - "name": "lh:audit:offscreen-images", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 228, - "name": "lh:audit:render-blocking-resources", + "name": "lh:audit:offscreen-images", "duration": 1, "entryType": "measure" }, { "startTime": 229, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:render-blocking-resources", "duration": 1, "entryType": "measure" }, { "startTime": 230, - "name": "lh:computed:NavigationInsights", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 231, - "name": "lh:computed:FirstContentfulPaint", + "name": "lh:computed:NavigationInsights", "duration": 1, "entryType": "measure" }, { "startTime": 232, - "name": "lh:audit:unminified-css", + "name": "lh:computed:FirstContentfulPaint", "duration": 1, "entryType": "measure" }, { "startTime": 233, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 234, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 235, - "name": "lh:audit:unused-javascript", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 236, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 237, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 238, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 239, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 240, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 241, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 242, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 243, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 244, - "name": "lh:audit:doctype", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 245, - "name": "lh:audit:charset", + "name": "lh:audit:doctype", "duration": 1, "entryType": "measure" }, { "startTime": 246, - "name": "lh:audit:dom-size", + "name": "lh:audit:charset", "duration": 1, "entryType": "measure" }, { "startTime": 247, - "name": "lh:audit:geolocation-on-start", + "name": "lh:audit:dom-size", "duration": 1, "entryType": "measure" }, { "startTime": 248, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:geolocation-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 249, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 250, - "name": "lh:audit:js-libraries", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 251, - "name": "lh:audit:notification-on-start", + "name": "lh:audit:js-libraries", "duration": 1, "entryType": "measure" }, { "startTime": 252, - "name": "lh:audit:paste-preventing-inputs", + "name": "lh:audit:notification-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 253, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:paste-preventing-inputs", "duration": 1, "entryType": "measure" }, { "startTime": 254, - "name": "lh:audit:meta-description", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 255, - "name": "lh:audit:http-status-code", + "name": "lh:audit:meta-description", "duration": 1, "entryType": "measure" }, { "startTime": 256, - "name": "lh:audit:font-size", + "name": "lh:audit:http-status-code", "duration": 1, "entryType": "measure" }, { "startTime": 257, - "name": "lh:audit:link-text", + "name": "lh:audit:font-size", "duration": 1, "entryType": "measure" }, { "startTime": 258, - "name": "lh:audit:crawlable-anchors", + "name": "lh:audit:link-text", "duration": 1, "entryType": "measure" }, { "startTime": 259, - "name": "lh:audit:is-crawlable", + "name": "lh:audit:crawlable-anchors", "duration": 1, "entryType": "measure" }, { "startTime": 260, - "name": "lh:audit:robots-txt", + "name": "lh:audit:is-crawlable", "duration": 1, "entryType": "measure" }, { "startTime": 261, - "name": "lh:audit:hreflang", + "name": "lh:audit:robots-txt", "duration": 1, "entryType": "measure" }, { "startTime": 262, - "name": "lh:audit:canonical", + "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { "startTime": 263, - "name": "lh:audit:structured-data", + "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { "startTime": 264, - "name": "lh:audit:bf-cache", + "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { "startTime": 265, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 266, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 266 + "total": 267 }, "i18n": { "rendererFormattedStrings": { @@ -7530,7 +7583,8 @@ ], "core/lib/i18n/i18n.js | columnDescription": [ "audits[csp-xss].details.headings[0].label", - "audits[has-hsts].details.headings[0].label" + "audits[has-hsts].details.headings[0].label", + "audits[origin-isolation].details.headings[0].label" ], "core/audits/csp-xss.js | columnDirective": [ "audits[csp-xss].details.headings[1].label" @@ -7539,7 +7593,8 @@ "audits[csp-xss].details.headings[2].label" ], "core/lib/i18n/i18n.js | itemSeverityHigh": [ - "audits[csp-xss].details.items[0].severity" + "audits[csp-xss].details.items[0].severity", + "audits[origin-isolation].details.items[0].severity" ], "core/audits/csp-xss.js | noCsp": [ "audits[csp-xss].details.items[0].description" @@ -7566,6 +7621,21 @@ "core/audits/has-hsts.js | noPreload": [ "audits[has-hsts].details.items[1].description" ], + "core/audits/origin-isolation.js | title": [ + "audits[origin-isolation].title" + ], + "core/audits/origin-isolation.js | description": [ + "audits[origin-isolation].description" + ], + "core/audits/origin-isolation.js | columnDirective": [ + "audits[origin-isolation].details.headings[1].label" + ], + "core/audits/origin-isolation.js | columnSeverity": [ + "audits[origin-isolation].details.headings[2].label" + ], + "core/audits/origin-isolation.js | noCoop": [ + "audits[origin-isolation].details.items[0].description" + ], "core/audits/accessibility/accesskeys.js | title": [ "audits.accesskeys.title" ], @@ -19928,14 +19998,56 @@ ], "items": [ { - "directive": "includeSubDomains", - "description": "No `includeSubDomains` directive found", - "severity": "Medium" - }, - { - "directive": "preload", - "description": "No `preload` directive found", - "severity": "Medium" + "directive": "includeSubDomains", + "description": "No `includeSubDomains` directive found", + "severity": "Medium" + }, + { + "directive": "preload", + "description": "No `preload` directive found", + "severity": "Medium" + } + ] + } + }, + "origin-isolation": { + "id": "origin-isolation", + "title": "Ensure proper origin isolation with COOP", + "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)", + "score": 1, + "scoreDisplayMode": "informative", + "details": { + "type": "table", + "headings": [ + { + "key": "description", + "valueType": "text", + "subItemsHeading": { + "key": "description" + }, + "label": "Description" + }, + { + "key": "directive", + "valueType": "code", + "subItemsHeading": { + "key": "directive" + }, + "label": "Directive" + }, + { + "key": "severity", + "valueType": "text", + "subItemsHeading": { + "key": "severity" + }, + "label": "Severity" + } + ], + "items": [ + { + "description": "No COOP header found", + "severity": "High" } ] } @@ -22567,6 +22679,11 @@ "weight": 0, "group": "best-practices-trust-safety" }, + { + "id": "origin-isolation", + "weight": 0, + "group": "best-practices-trust-safety" + }, { "id": "paste-preventing-inputs", "weight": 3, @@ -24199,19 +24316,19 @@ }, { "startTime": 134, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:origin-isolation", "duration": 1, "entryType": "measure" }, { "startTime": 135, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 136, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -24301,690 +24418,696 @@ }, { "startTime": 151, - "name": "lh:audit:accesskeys", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 152, - "name": "lh:audit:aria-allowed-attr", + "name": "lh:audit:accesskeys", "duration": 1, "entryType": "measure" }, { "startTime": 153, - "name": "lh:audit:aria-allowed-role", + "name": "lh:audit:aria-allowed-attr", "duration": 1, "entryType": "measure" }, { "startTime": 154, - "name": "lh:audit:aria-command-name", + "name": "lh:audit:aria-allowed-role", "duration": 1, "entryType": "measure" }, { "startTime": 155, - "name": "lh:audit:aria-conditional-attr", + "name": "lh:audit:aria-command-name", "duration": 1, "entryType": "measure" }, { "startTime": 156, - "name": "lh:audit:aria-deprecated-role", + "name": "lh:audit:aria-conditional-attr", "duration": 1, "entryType": "measure" }, { "startTime": 157, - "name": "lh:audit:aria-dialog-name", + "name": "lh:audit:aria-deprecated-role", "duration": 1, "entryType": "measure" }, { "startTime": 158, - "name": "lh:audit:aria-hidden-body", + "name": "lh:audit:aria-dialog-name", "duration": 1, "entryType": "measure" }, { "startTime": 159, - "name": "lh:audit:aria-hidden-focus", + "name": "lh:audit:aria-hidden-body", "duration": 1, "entryType": "measure" }, { "startTime": 160, - "name": "lh:audit:aria-input-field-name", + "name": "lh:audit:aria-hidden-focus", "duration": 1, "entryType": "measure" }, { "startTime": 161, - "name": "lh:audit:aria-meter-name", + "name": "lh:audit:aria-input-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 162, - "name": "lh:audit:aria-progressbar-name", + "name": "lh:audit:aria-meter-name", "duration": 1, "entryType": "measure" }, { "startTime": 163, - "name": "lh:audit:aria-prohibited-attr", + "name": "lh:audit:aria-progressbar-name", "duration": 1, "entryType": "measure" }, { "startTime": 164, - "name": "lh:audit:aria-required-attr", + "name": "lh:audit:aria-prohibited-attr", "duration": 1, "entryType": "measure" }, { "startTime": 165, - "name": "lh:audit:aria-required-children", + "name": "lh:audit:aria-required-attr", "duration": 1, "entryType": "measure" }, { "startTime": 166, - "name": "lh:audit:aria-required-parent", + "name": "lh:audit:aria-required-children", "duration": 1, "entryType": "measure" }, { "startTime": 167, - "name": "lh:audit:aria-roles", + "name": "lh:audit:aria-required-parent", "duration": 1, "entryType": "measure" }, { "startTime": 168, - "name": "lh:audit:aria-text", + "name": "lh:audit:aria-roles", "duration": 1, "entryType": "measure" }, { "startTime": 169, - "name": "lh:audit:aria-toggle-field-name", + "name": "lh:audit:aria-text", "duration": 1, "entryType": "measure" }, { "startTime": 170, - "name": "lh:audit:aria-tooltip-name", + "name": "lh:audit:aria-toggle-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 171, - "name": "lh:audit:aria-treeitem-name", + "name": "lh:audit:aria-tooltip-name", "duration": 1, "entryType": "measure" }, { "startTime": 172, - "name": "lh:audit:aria-valid-attr-value", + "name": "lh:audit:aria-treeitem-name", "duration": 1, "entryType": "measure" }, { "startTime": 173, - "name": "lh:audit:aria-valid-attr", + "name": "lh:audit:aria-valid-attr-value", "duration": 1, "entryType": "measure" }, { "startTime": 174, - "name": "lh:audit:button-name", + "name": "lh:audit:aria-valid-attr", "duration": 1, "entryType": "measure" }, { "startTime": 175, - "name": "lh:audit:bypass", + "name": "lh:audit:button-name", "duration": 1, "entryType": "measure" }, { "startTime": 176, - "name": "lh:audit:color-contrast", + "name": "lh:audit:bypass", "duration": 1, "entryType": "measure" }, { "startTime": 177, - "name": "lh:audit:definition-list", + "name": "lh:audit:color-contrast", "duration": 1, "entryType": "measure" }, { "startTime": 178, - "name": "lh:audit:dlitem", + "name": "lh:audit:definition-list", "duration": 1, "entryType": "measure" }, { "startTime": 179, - "name": "lh:audit:document-title", + "name": "lh:audit:dlitem", "duration": 1, "entryType": "measure" }, { "startTime": 180, - "name": "lh:audit:duplicate-id-aria", + "name": "lh:audit:document-title", "duration": 1, "entryType": "measure" }, { "startTime": 181, - "name": "lh:audit:empty-heading", + "name": "lh:audit:duplicate-id-aria", "duration": 1, "entryType": "measure" }, { "startTime": 182, - "name": "lh:audit:form-field-multiple-labels", + "name": "lh:audit:empty-heading", "duration": 1, "entryType": "measure" }, { "startTime": 183, - "name": "lh:audit:frame-title", + "name": "lh:audit:form-field-multiple-labels", "duration": 1, "entryType": "measure" }, { "startTime": 184, - "name": "lh:audit:heading-order", + "name": "lh:audit:frame-title", "duration": 1, "entryType": "measure" }, { "startTime": 185, - "name": "lh:audit:html-has-lang", + "name": "lh:audit:heading-order", "duration": 1, "entryType": "measure" }, { "startTime": 186, - "name": "lh:audit:html-lang-valid", + "name": "lh:audit:html-has-lang", "duration": 1, "entryType": "measure" }, { "startTime": 187, - "name": "lh:audit:html-xml-lang-mismatch", + "name": "lh:audit:html-lang-valid", "duration": 1, "entryType": "measure" }, { "startTime": 188, - "name": "lh:audit:identical-links-same-purpose", + "name": "lh:audit:html-xml-lang-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 189, - "name": "lh:audit:image-alt", + "name": "lh:audit:identical-links-same-purpose", "duration": 1, "entryType": "measure" }, { "startTime": 190, - "name": "lh:audit:image-redundant-alt", + "name": "lh:audit:image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 191, - "name": "lh:audit:input-button-name", + "name": "lh:audit:image-redundant-alt", "duration": 1, "entryType": "measure" }, { "startTime": 192, - "name": "lh:audit:input-image-alt", + "name": "lh:audit:input-button-name", "duration": 1, "entryType": "measure" }, { "startTime": 193, - "name": "lh:audit:label-content-name-mismatch", + "name": "lh:audit:input-image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 194, - "name": "lh:audit:label", + "name": "lh:audit:label-content-name-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 195, - "name": "lh:audit:landmark-one-main", + "name": "lh:audit:label", "duration": 1, "entryType": "measure" }, { "startTime": 196, - "name": "lh:audit:link-name", + "name": "lh:audit:landmark-one-main", "duration": 1, "entryType": "measure" }, { "startTime": 197, - "name": "lh:audit:link-in-text-block", + "name": "lh:audit:link-name", "duration": 1, "entryType": "measure" }, { "startTime": 198, - "name": "lh:audit:list", + "name": "lh:audit:link-in-text-block", "duration": 1, "entryType": "measure" }, { "startTime": 199, - "name": "lh:audit:listitem", + "name": "lh:audit:list", "duration": 1, "entryType": "measure" }, { "startTime": 200, - "name": "lh:audit:meta-refresh", + "name": "lh:audit:listitem", "duration": 1, "entryType": "measure" }, { "startTime": 201, - "name": "lh:audit:meta-viewport", + "name": "lh:audit:meta-refresh", "duration": 1, "entryType": "measure" }, { "startTime": 202, - "name": "lh:audit:object-alt", + "name": "lh:audit:meta-viewport", "duration": 1, "entryType": "measure" }, { "startTime": 203, - "name": "lh:audit:select-name", + "name": "lh:audit:object-alt", "duration": 1, "entryType": "measure" }, { "startTime": 204, - "name": "lh:audit:skip-link", + "name": "lh:audit:select-name", "duration": 1, "entryType": "measure" }, { "startTime": 205, - "name": "lh:audit:tabindex", + "name": "lh:audit:skip-link", "duration": 1, "entryType": "measure" }, { "startTime": 206, - "name": "lh:audit:table-duplicate-name", + "name": "lh:audit:tabindex", "duration": 1, "entryType": "measure" }, { "startTime": 207, - "name": "lh:audit:table-fake-caption", + "name": "lh:audit:table-duplicate-name", "duration": 1, "entryType": "measure" }, { "startTime": 208, - "name": "lh:audit:target-size", + "name": "lh:audit:table-fake-caption", "duration": 1, "entryType": "measure" }, { "startTime": 209, - "name": "lh:audit:td-has-header", + "name": "lh:audit:target-size", "duration": 1, "entryType": "measure" }, { "startTime": 210, - "name": "lh:audit:td-headers-attr", + "name": "lh:audit:td-has-header", "duration": 1, "entryType": "measure" }, { "startTime": 211, - "name": "lh:audit:th-has-data-cells", + "name": "lh:audit:td-headers-attr", "duration": 1, "entryType": "measure" }, { "startTime": 212, - "name": "lh:audit:valid-lang", + "name": "lh:audit:th-has-data-cells", "duration": 1, "entryType": "measure" }, { "startTime": 213, - "name": "lh:audit:video-caption", + "name": "lh:audit:valid-lang", "duration": 1, "entryType": "measure" }, { "startTime": 214, - "name": "lh:audit:custom-controls-labels", + "name": "lh:audit:video-caption", "duration": 1, "entryType": "measure" }, { "startTime": 215, - "name": "lh:audit:custom-controls-roles", + "name": "lh:audit:custom-controls-labels", "duration": 1, "entryType": "measure" }, { "startTime": 216, - "name": "lh:audit:focus-traps", + "name": "lh:audit:custom-controls-roles", "duration": 1, "entryType": "measure" }, { "startTime": 217, - "name": "lh:audit:focusable-controls", + "name": "lh:audit:focus-traps", "duration": 1, "entryType": "measure" }, { "startTime": 218, - "name": "lh:audit:interactive-element-affordance", + "name": "lh:audit:focusable-controls", "duration": 1, "entryType": "measure" }, { "startTime": 219, - "name": "lh:audit:logical-tab-order", + "name": "lh:audit:interactive-element-affordance", "duration": 1, "entryType": "measure" }, { "startTime": 220, - "name": "lh:audit:managed-focus", + "name": "lh:audit:logical-tab-order", "duration": 1, "entryType": "measure" }, { "startTime": 221, - "name": "lh:audit:offscreen-content-hidden", + "name": "lh:audit:managed-focus", "duration": 1, "entryType": "measure" }, { "startTime": 222, - "name": "lh:audit:use-landmarks", + "name": "lh:audit:offscreen-content-hidden", "duration": 1, "entryType": "measure" }, { "startTime": 223, - "name": "lh:audit:visual-order-follows-dom", + "name": "lh:audit:use-landmarks", "duration": 1, "entryType": "measure" }, { "startTime": 224, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:audit:visual-order-follows-dom", "duration": 1, "entryType": "measure" }, { "startTime": 225, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 226, - "name": "lh:audit:offscreen-images", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 227, - "name": "lh:audit:render-blocking-resources", + "name": "lh:audit:offscreen-images", "duration": 1, "entryType": "measure" }, { "startTime": 228, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:render-blocking-resources", "duration": 1, "entryType": "measure" }, { "startTime": 229, - "name": "lh:computed:NavigationInsights", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 230, - "name": "lh:computed:FirstContentfulPaint", + "name": "lh:computed:NavigationInsights", "duration": 1, "entryType": "measure" }, { "startTime": 231, - "name": "lh:audit:unminified-css", + "name": "lh:computed:FirstContentfulPaint", "duration": 1, "entryType": "measure" }, { "startTime": 232, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 233, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 234, - "name": "lh:audit:unused-javascript", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 235, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 236, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 237, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 238, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 239, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 240, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 241, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 242, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 243, - "name": "lh:audit:doctype", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 244, - "name": "lh:audit:charset", + "name": "lh:audit:doctype", "duration": 1, "entryType": "measure" }, { "startTime": 245, - "name": "lh:audit:dom-size", + "name": "lh:audit:charset", "duration": 1, "entryType": "measure" }, { "startTime": 246, - "name": "lh:audit:geolocation-on-start", + "name": "lh:audit:dom-size", "duration": 1, "entryType": "measure" }, { "startTime": 247, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:geolocation-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 248, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 249, - "name": "lh:audit:js-libraries", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 250, - "name": "lh:audit:notification-on-start", + "name": "lh:audit:js-libraries", "duration": 1, "entryType": "measure" }, { "startTime": 251, - "name": "lh:audit:paste-preventing-inputs", + "name": "lh:audit:notification-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 252, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:paste-preventing-inputs", "duration": 1, "entryType": "measure" }, { "startTime": 253, - "name": "lh:audit:meta-description", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 254, - "name": "lh:audit:http-status-code", + "name": "lh:audit:meta-description", "duration": 1, "entryType": "measure" }, { "startTime": 255, - "name": "lh:audit:font-size", + "name": "lh:audit:http-status-code", "duration": 1, "entryType": "measure" }, { "startTime": 256, - "name": "lh:audit:link-text", + "name": "lh:audit:font-size", "duration": 1, "entryType": "measure" }, { "startTime": 257, - "name": "lh:audit:crawlable-anchors", + "name": "lh:audit:link-text", "duration": 1, "entryType": "measure" }, { "startTime": 258, - "name": "lh:audit:is-crawlable", + "name": "lh:audit:crawlable-anchors", "duration": 1, "entryType": "measure" }, { "startTime": 259, - "name": "lh:audit:robots-txt", + "name": "lh:audit:is-crawlable", "duration": 1, "entryType": "measure" }, { "startTime": 260, - "name": "lh:audit:hreflang", + "name": "lh:audit:robots-txt", "duration": 1, "entryType": "measure" }, { "startTime": 261, - "name": "lh:audit:canonical", + "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { "startTime": 262, - "name": "lh:audit:structured-data", + "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { "startTime": 263, - "name": "lh:audit:bf-cache", + "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { "startTime": 264, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 265, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 265 + "total": 266 }, "i18n": { "rendererFormattedStrings": { @@ -25506,7 +25629,8 @@ ], "core/lib/i18n/i18n.js | columnDescription": [ "audits[csp-xss].details.headings[0].label", - "audits[has-hsts].details.headings[0].label" + "audits[has-hsts].details.headings[0].label", + "audits[origin-isolation].details.headings[0].label" ], "core/audits/csp-xss.js | columnDirective": [ "audits[csp-xss].details.headings[1].label" @@ -25515,7 +25639,8 @@ "audits[csp-xss].details.headings[2].label" ], "core/lib/i18n/i18n.js | itemSeverityHigh": [ - "audits[csp-xss].details.items[0].severity" + "audits[csp-xss].details.items[0].severity", + "audits[origin-isolation].details.items[0].severity" ], "core/audits/csp-xss.js | noCsp": [ "audits[csp-xss].details.items[0].description" @@ -25542,6 +25667,21 @@ "core/audits/has-hsts.js | noPreload": [ "audits[has-hsts].details.items[1].description" ], + "core/audits/origin-isolation.js | title": [ + "audits[origin-isolation].title" + ], + "core/audits/origin-isolation.js | description": [ + "audits[origin-isolation].description" + ], + "core/audits/origin-isolation.js | columnDirective": [ + "audits[origin-isolation].details.headings[1].label" + ], + "core/audits/origin-isolation.js | columnSeverity": [ + "audits[origin-isolation].details.headings[2].label" + ], + "core/audits/origin-isolation.js | noCoop": [ + "audits[origin-isolation].details.items[0].description" + ], "core/audits/accessibility/accesskeys.js | title": [ "audits.accesskeys.title" ], diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index a9dab4f6e4b4..f9409c4c31b9 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -2807,6 +2807,48 @@ ] } }, + "origin-isolation": { + "id": "origin-isolation", + "title": "Ensure proper origin isolation with COOP", + "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)", + "score": 1, + "scoreDisplayMode": "informative", + "details": { + "type": "table", + "headings": [ + { + "key": "description", + "valueType": "text", + "subItemsHeading": { + "key": "description" + }, + "label": "Description" + }, + { + "key": "directive", + "valueType": "code", + "subItemsHeading": { + "key": "directive" + }, + "label": "Directive" + }, + { + "key": "severity", + "valueType": "text", + "subItemsHeading": { + "key": "severity" + }, + "label": "Severity" + } + ], + "items": [ + { + "description": "No COOP header found", + "severity": "High" + } + ] + } + }, "script-treemap-data": { "id": "script-treemap-data", "title": "Script Treemap Data", @@ -6756,6 +6798,11 @@ "weight": 0, "group": "best-practices-trust-safety" }, + { + "id": "origin-isolation", + "weight": 0, + "group": "best-practices-trust-safety" + }, { "id": "paste-preventing-inputs", "weight": 3, @@ -8203,6 +8250,12 @@ "duration": 100, "entryType": "measure" }, + { + "startTime": 0, + "name": "lh:audit:origin-isolation", + "duration": 100, + "entryType": "measure" + }, { "startTime": 0, "name": "lh:audit:script-treemap-data", @@ -9255,7 +9308,8 @@ "core/lib/i18n/i18n.js | columnDescription": [ "audits[errors-in-console].details.headings[1].label", "audits[csp-xss].details.headings[0].label", - "audits[has-hsts].details.headings[0].label" + "audits[has-hsts].details.headings[0].label", + "audits[origin-isolation].details.headings[0].label" ], "core/audits/server-response-time.js | title": [ "audits[server-response-time].title" @@ -9677,7 +9731,8 @@ ], "core/lib/i18n/i18n.js | itemSeverityHigh": [ "audits[csp-xss].details.items[0].severity", - "audits[has-hsts].details.items[0].severity" + "audits[has-hsts].details.items[0].severity", + "audits[origin-isolation].details.items[0].severity" ], "core/audits/csp-xss.js | noCsp": [ "audits[csp-xss].details.items[0].description" @@ -9697,6 +9752,21 @@ "core/audits/has-hsts.js | noHsts": [ "audits[has-hsts].details.items[0].description" ], + "core/audits/origin-isolation.js | title": [ + "audits[origin-isolation].title" + ], + "core/audits/origin-isolation.js | description": [ + "audits[origin-isolation].description" + ], + "core/audits/origin-isolation.js | columnDirective": [ + "audits[origin-isolation].details.headings[1].label" + ], + "core/audits/origin-isolation.js | columnSeverity": [ + "audits[origin-isolation].details.headings[2].label" + ], + "core/audits/origin-isolation.js | noCoop": [ + "audits[origin-isolation].details.items[0].description" + ], "core/audits/accessibility/accesskeys.js | title": [ "audits.accesskeys.title" ], diff --git a/core/test/scenarios/__snapshots__/api-test-pptr.js.snap b/core/test/scenarios/__snapshots__/api-test-pptr.js.snap index 8beb822b1239..b58c06ccba48 100644 --- a/core/test/scenarios/__snapshots__/api-test-pptr.js.snap +++ b/core/test/scenarios/__snapshots__/api-test-pptr.js.snap @@ -113,6 +113,7 @@ Array [ "object-alt", "offscreen-content-hidden", "offscreen-images", + "origin-isolation", "paste-preventing-inputs", "prioritize-lcp-image", "redirects", @@ -274,6 +275,7 @@ Array [ "object-alt", "offscreen-content-hidden", "offscreen-images", + "origin-isolation", "paste-preventing-inputs", "prioritize-lcp-image", "redirects", diff --git a/shared/localization/locales/en-US.json b/shared/localization/locales/en-US.json index 830214470630..66ad6195ce66 100644 --- a/shared/localization/locales/en-US.json +++ b/shared/localization/locales/en-US.json @@ -1217,6 +1217,24 @@ "core/audits/non-composited-animations.js | unsupportedTimingParameters": { "message": "Effect has unsupported timing parameters" }, + "core/audits/origin-isolation.js | columnDirective": { + "message": "Directive" + }, + "core/audits/origin-isolation.js | columnSeverity": { + "message": "Severity" + }, + "core/audits/origin-isolation.js | description": { + "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)" + }, + "core/audits/origin-isolation.js | invalidSyntax": { + "message": "Invalid syntax" + }, + "core/audits/origin-isolation.js | noCoop": { + "message": "No COOP header found" + }, + "core/audits/origin-isolation.js | title": { + "message": "Ensure proper origin isolation with COOP" + }, "core/audits/preload-fonts.js | description": { "message": "Preload `optional` fonts so first-time visitors may use them. [Learn more about preloading fonts](https://web.dev/articles/preload-optional-fonts)" }, diff --git a/shared/localization/locales/en-XL.json b/shared/localization/locales/en-XL.json index 3345091ac993..af4ad079f9ba 100644 --- a/shared/localization/locales/en-XL.json +++ b/shared/localization/locales/en-XL.json @@ -1217,6 +1217,24 @@ "core/audits/non-composited-animations.js | unsupportedTimingParameters": { "message": "Êf́f̂éĉt́ ĥáŝ ún̂śûṕp̂ór̂t́êd́ t̂ím̂ín̂ǵ p̂ár̂ám̂ét̂ér̂ś" }, + "core/audits/origin-isolation.js | columnDirective": { + "message": "D̂ír̂éĉt́îv́ê" + }, + "core/audits/origin-isolation.js | columnSeverity": { + "message": "Ŝév̂ér̂ít̂ý" + }, + "core/audits/origin-isolation.js | description": { + "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)" + }, + "core/audits/origin-isolation.js | invalidSyntax": { + "message": "Îńv̂ál̂íd̂ śŷńt̂áx̂" + }, + "core/audits/origin-isolation.js | noCoop": { + "message": "N̂ó ĈÓÔṔ ĥéâd́êŕ f̂óûńd̂" + }, + "core/audits/origin-isolation.js | title": { + "message": "Êńŝúr̂é p̂ŕôṕêŕ ôŕîǵîń îśôĺât́îón̂ ẃît́ĥ ĆÔÓP̂" + }, "core/audits/preload-fonts.js | description": { "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)" }, diff --git a/third-party/devtools-tests/e2e/lighthouse/navigation_test.ts b/third-party/devtools-tests/e2e/lighthouse/navigation_test.ts index b7bc5827abc0..aff7293db901 100644 --- a/third-party/devtools-tests/e2e/lighthouse/navigation_test.ts +++ b/third-party/devtools-tests/e2e/lighthouse/navigation_test.ts @@ -121,7 +121,7 @@ describe('Navigation', function() { }); const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr, ['max-potential-fid']); - assert.strictEqual(auditResults.length, 156); + assert.strictEqual(auditResults.length, 157); assert.deepStrictEqual(erroredAudits, []); assert.deepStrictEqual(failedAudits.map(audit => audit.id), [ 'document-title', @@ -199,7 +199,7 @@ describe('Navigation', function() { ]; const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr, flakyAudits); - assert.strictEqual(auditResults.length, 156); + assert.strictEqual(auditResults.length, 157); assert.deepStrictEqual(erroredAudits, []); assert.deepStrictEqual(failedAudits.map(audit => audit.id), [ 'document-title',