From c00a5794bbef2eed2c52ec725138b1a3fcf01630 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 26 Jun 2020 14:36:31 -0700 Subject: [PATCH 01/40] Added new docTypeError fxn and LHError --- lighthouse-core/gather/gather-runner.js | 33 +++++++++++++++++++++ lighthouse-core/lib/i18n/locales/en-US.json | 3 ++ lighthouse-core/lib/i18n/locales/en-XL.json | 3 ++ lighthouse-core/lib/lh-error.js | 7 +++++ 4 files changed, 46 insertions(+) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index ce70e4b87047..deb4c919455a 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -215,6 +215,31 @@ class GatherRunner { return new LHError(LHError.errors.CHROME_INTERSTITIAL_ERROR); } + /** + * Returns an error if we try to load a non-HTML page. + * @param {LH.Artifacts.NetworkRequest|undefined} mainRecord + * @return {LH.LighthouseError|undefined} + */ + static getDocTypeError(mainRecord) { + // MIME types are case-insenstive + const HTML_MIME_REGEX = /^text\/html$/i; + + // If we never requested a document, there's no doctype error, let other cases handle it. + if (!mainRecord) return undefined; + + // If the main document failed, this error case is undefined, let other cases handle it. + if (mainRecord.failed) return undefined; + + // mimeType is determined by the browser, we assume Chrome is determining mimeType correctly, + // independently of 'Content-Type' response headers, and always sending mimeType if well-formed. + if (mainRecord.mimeType) { + if (!HTML_MIME_REGEX.test(mainRecord.mimeType)) { + return new LHError(LHError.errors.INVALID_DOC_TYPE); + } + } + return undefined; + } + /** * Returns an error if the page load should be considered failed, e.g. from a * main document request failure, a security issue, etc. @@ -231,8 +256,13 @@ class GatherRunner { mainRecord = NetworkAnalyzer.findMainDocument(networkRecords, passContext.url); } catch (_) {} + console.log('mainRecord'); + console.log(mainRecord); const networkError = GatherRunner.getNetworkError(mainRecord); const interstitialError = GatherRunner.getInterstitialError(mainRecord, networkRecords); + const docTypeError = GatherRunner.getDocTypeError(mainRecord); + console.log('docTypeError'); + console.log(docTypeError); // Check to see if we need to ignore the page load failure. // e.g. When the driver is offline, the load will fail without page offline support. @@ -246,6 +276,9 @@ class GatherRunner { // Example: `DNS_FAILURE` is better than `NO_FCP`. if (networkError) return networkError; + // We want to error when the page is not of MIME type text/html + if (docTypeError) return docTypeError; + // Navigation errors are rather generic and express some failure of the page to render properly. // Use `navigationError` as the last resort. // Example: `NO_FCP`, the page never painted content for some unknown reason. diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index c7364f13cc5b..c22538fbdaf2 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1592,6 +1592,9 @@ "lighthouse-core/lib/lh-error.js | dnsFailure": { "message": "DNS servers could not resolve the provided domain." }, + "lighthouse-core/lib/lh-error.js | docTypeInvalid": { + "message": "The webpage you have provided appears to be non-HTML" + }, "lighthouse-core/lib/lh-error.js | erroredRequiredArtifact": { "message": "Required {artifactName} gatherer encountered an error: {errorMessage}" }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index 7d579c7a108a..0ae629d7a6a7 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1592,6 +1592,9 @@ "lighthouse-core/lib/lh-error.js | dnsFailure": { "message": "D̂ŃŜ śêŕv̂ér̂ś ĉóûĺd̂ ńôt́ r̂éŝól̂v́ê t́ĥé p̂ŕôv́îd́êd́ d̂óm̂áîń." }, + "lighthouse-core/lib/lh-error.js | docTypeInvalid": { + "message": "T̂h́ê ẃêb́p̂áĝé ŷóû h́âv́ê ṕr̂óv̂íd̂éd̂ áp̂ṕêár̂ś t̂ó b̂é n̂ón̂-H́T̂ḾL̂" + }, "lighthouse-core/lib/lh-error.js | erroredRequiredArtifact": { "message": "R̂éq̂úîŕêd́ {artifactName} ĝát̂h́êŕêŕ êńĉóûńt̂ér̂éd̂ án̂ ér̂ŕôŕ: {errorMessage}" }, diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js index a1015433b3a4..7e58ae7e3994 100644 --- a/lighthouse-core/lib/lh-error.js +++ b/lighthouse-core/lib/lh-error.js @@ -47,6 +47,8 @@ const UIStrings = { internalChromeError: 'An internal Chrome error occurred. Please restart Chrome and try re-running Lighthouse.', /** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */ requestContentTimeout: 'Fetching resource content has exceeded the allotted time', + /** Error message explaining that the webpage is non-HTML, so audits are ill-defined **/ + docTypeInvalid: 'The webpage you have provided appears to be non-HTML', /** Error message explaining that the provided URL Lighthouse points to is not valid, and cannot be loaded. */ urlInvalid: 'The URL you have provided appears to be invalid.', /** @@ -312,6 +314,11 @@ const ERRORS = { message: UIStrings.pageLoadFailedHung, lhrRuntimeError: true, }, + /* Used when the page is non-HTML. */ + INVALID_DOC_TYPE: { + code: 'INVALID_DOC_TYPE', + message: UIStrings.docTypeInvalid, + }, // Protocol internal failures TRACING_ALREADY_STARTED: { From 09682f41a34ac0a32a35638bbecef71c0d2d6a3b Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 26 Jun 2020 15:22:56 -0700 Subject: [PATCH 02/40] starting testing code --- lighthouse-core/test/gather/gather-runner-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index d8f42c70c1c4..97e615ad8b66 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -38,6 +38,7 @@ const GatherRunner = { getInstallabilityErrors: makeParamsOptional(GatherRunner_.getInstallabilityErrors), getInterstitialError: makeParamsOptional(GatherRunner_.getInterstitialError), getNetworkError: makeParamsOptional(GatherRunner_.getNetworkError), + getDocTypeError: makeParamsOptional(GatherRunner_.getDocTypeError), getPageLoadError: makeParamsOptional(GatherRunner_.getPageLoadError), getWebAppManifest: makeParamsOptional(GatherRunner_.getWebAppManifest), initializeBaseArtifacts: makeParamsOptional(GatherRunner_.initializeBaseArtifacts), From eec825dfbb4d726bdb1373be78523663f09c1378 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Mon, 29 Jun 2020 11:50:37 -0700 Subject: [PATCH 03/40] starting tests --- lighthouse-core/test/gather/gather-runner-test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 97e615ad8b66..b4ac75f361de 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -1096,6 +1096,18 @@ describe('GatherRunner', function() { }); }); + describe('#getDocTypeError', () => { + /** + * @param {NetworkRequest} mainRecord + */ + function getAndExpectError(mainRecord) { + const error = GatherRunner.getDocTypeError(mainRecord); + if (!error) throw new Error('expected a docType error'); + return error; + } + }); + + describe('#getPageLoadError', () => { /** * @param {RecursivePartial} passContext From a63f81372652bb2e0d3ff88315d5bdd2c5700910 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Mon, 29 Jun 2020 17:22:55 -0700 Subject: [PATCH 04/40] finished unit tests for getDocTypeError --- .../test/gather/gather-runner-test.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index b4ac75f361de..959714ee9b56 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -1105,6 +1105,40 @@ describe('GatherRunner', function() { if (!error) throw new Error('expected a docType error'); return error; } + + it('passes when the page was not requested', () => { + expect(GatherRunner.getDocTypeError(undefined)).toBeUndefined(); + }); + + it('passes when page fails to load normally', () => { + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + mainRecord.failed = true; + mainRecord.localizedFailDescription = 'foobar'; + expect(GatherRunner.getDocTypeError(mainRecord)).toBeUndefined(); + }); + + it('passes when the page is of MIME type text/html', () => { + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + const mimeType = 'text/html'; + mainRecord.url = url; + mainRecord.mimeType = mimeType; + expect(GatherRunner.getDocTypeError(mainRecord)).toBeUndefined(); + }); + + it('fails when the page is not of MIME type text/html', () => { + const url = 'http://the-page.com'; + const mimeType = 'application/xml'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + mainRecord.mimeType = mimeType; + const error = getAndExpectError(mainRecord); + expect(error.message).toEqual('INVALID_DOC_TYPE'); + expect(error.code).toEqual('INVALID_DOC_TYPE'); + expect(error.friendlyMessage).toBeDisplayString(/appears to be non-HTML/); + }); }); From 40bf826f812ccf4bc229a3a8e8c41c619e407095 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 30 Jun 2020 09:19:18 -0700 Subject: [PATCH 05/40] bugfixing --- lighthouse-core/gather/gather-runner.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index deb4c919455a..b28c05c93d1f 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -232,7 +232,7 @@ class GatherRunner { // mimeType is determined by the browser, we assume Chrome is determining mimeType correctly, // independently of 'Content-Type' response headers, and always sending mimeType if well-formed. - if (mainRecord.mimeType) { + if (mainRecord.mimeType || mainRecord.mimeType === '') { if (!HTML_MIME_REGEX.test(mainRecord.mimeType)) { return new LHError(LHError.errors.INVALID_DOC_TYPE); } @@ -256,13 +256,15 @@ class GatherRunner { mainRecord = NetworkAnalyzer.findMainDocument(networkRecords, passContext.url); } catch (_) {} - console.log('mainRecord'); - console.log(mainRecord); + //console.log('mainRecord'); + //if (mainRecord) { + // console.log(mainRecord.mimeType); + //console.log(mainRecord.mimeType === ''); + //} const networkError = GatherRunner.getNetworkError(mainRecord); const interstitialError = GatherRunner.getInterstitialError(mainRecord, networkRecords); const docTypeError = GatherRunner.getDocTypeError(mainRecord); - console.log('docTypeError'); - console.log(docTypeError); + //console.log(docTypeError); // Check to see if we need to ignore the page load failure. // e.g. When the driver is offline, the load will fail without page offline support. From 28cad19ef7841e186de8d2600a3b18231f4e1add Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 30 Jun 2020 14:50:49 -0700 Subject: [PATCH 06/40] integrated getDocTypeError into getPageLoadError --- .../test/gather/gather-runner-test.js | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 959714ee9b56..36330ec30b25 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -1161,26 +1161,30 @@ describe('GatherRunner', function() { navigationError = /** @type {LH.LighthouseError} */ (new Error('NAVIGATION_ERROR')); }); - it('passes when the page is loaded', () => { + it('passes when the page is loaded and doc type is text/html', () => { const passContext = { url: 'http://the-page.com', passConfig: {loadFailureMode: LoadFailureMode.fatal}, }; const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; + const mimeType = 'text/html'; mainRecord.url = passContext.url; + mainRecord.mimeType = mimeType; const error = GatherRunner.getPageLoadError(passContext, loadData, undefined); expect(error).toBeUndefined(); }); - it('passes when the page is loaded, ignoring any fragment', () => { + it('passes when the page is loaded and doc type is text/html, ignoring any fragment', () => { const passContext = { url: 'http://example.com/#/page/list', passConfig: {loadFailureMode: LoadFailureMode.fatal}, }; const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; + const mimeType = 'text/html'; mainRecord.url = 'http://example.com'; + mainRecord.mimeType = mimeType; const error = GatherRunner.getPageLoadError(passContext, loadData, undefined); expect(error).toBeUndefined(); }); @@ -1217,7 +1221,7 @@ describe('GatherRunner', function() { expect(error.message).toEqual('CHROME_INTERSTITIAL_ERROR'); }); - it('fails with network error next', () => { + it('fails with network error second', () => { const passContext = { url: 'http://the-page.com', passConfig: {loadFailureMode: LoadFailureMode.fatal}, @@ -1232,6 +1236,22 @@ describe('GatherRunner', function() { expect(error.message).toEqual('FAILED_DOCUMENT_REQUEST'); }); + it('fails with doc type error third', () => { + const passContext = { + url: 'http://the-page.com', + passConfig: {loadFailureMode: LoadFailureMode.fatal}, + }; + const mainRecord = new NetworkRequest(); + const loadData = {networkRecords: [mainRecord]}; + + const mimeType = 'application/xml'; + mainRecord.url = passContext.url; + mainRecord.mimeType = mimeType; + + const error = getAndExpectError(passContext, loadData, navigationError); + expect(error.message).toEqual('INVALID_DOC_TYPE'); + }); + it('fails with nav error last', () => { const passContext = { url: 'http://the-page.com', @@ -1240,7 +1260,9 @@ describe('GatherRunner', function() { const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; + const mimeType = 'text/html'; mainRecord.url = passContext.url; + mainRecord.mimeType = mimeType; const error = getAndExpectError(passContext, loadData, navigationError); expect(error.message).toEqual('NAVIGATION_ERROR'); @@ -1254,7 +1276,9 @@ describe('GatherRunner', function() { const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; + const mimeType = 'text/html'; mainRecord.url = passContext.url; + mainRecord.mimeType = mimeType; const error = getAndExpectError(passContext, loadData, navigationError); expect(error.message).toEqual('NAVIGATION_ERROR'); From 6ac8d69c97a3fd28ec28ff40858bb23702b03e56 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 30 Jun 2020 14:52:23 -0700 Subject: [PATCH 07/40] removed console logging from debugging --- lighthouse-core/gather/gather-runner.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index b28c05c93d1f..5449af58efe0 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -256,15 +256,9 @@ class GatherRunner { mainRecord = NetworkAnalyzer.findMainDocument(networkRecords, passContext.url); } catch (_) {} - //console.log('mainRecord'); - //if (mainRecord) { - // console.log(mainRecord.mimeType); - //console.log(mainRecord.mimeType === ''); - //} const networkError = GatherRunner.getNetworkError(mainRecord); const interstitialError = GatherRunner.getInterstitialError(mainRecord, networkRecords); const docTypeError = GatherRunner.getDocTypeError(mainRecord); - //console.log(docTypeError); // Check to see if we need to ignore the page load failure. // e.g. When the driver is offline, the load will fail without page offline support. From fbc63807349f10aa5c0694387da8ba14b3845ced Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 1 Jul 2020 09:52:11 -0700 Subject: [PATCH 08/40] changes from code review --- lighthouse-core/gather/gather-runner.js | 2 +- lighthouse-core/lib/i18n/locales/en-US.json | 2 +- lighthouse-core/lib/i18n/locales/en-XL.json | 2 +- lighthouse-core/lib/lh-error.js | 2 +- .../test/gather/gather-runner-test.js | 19 +++++++------------ 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index 5449af58efe0..0ede58d8546e 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -272,7 +272,7 @@ class GatherRunner { // Example: `DNS_FAILURE` is better than `NO_FCP`. if (networkError) return networkError; - // We want to error when the page is not of MIME type text/html + // Error if page is not HTML. if (docTypeError) return docTypeError; // Navigation errors are rather generic and express some failure of the page to render properly. diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index c22538fbdaf2..0f1856d2465c 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1593,7 +1593,7 @@ "message": "DNS servers could not resolve the provided domain." }, "lighthouse-core/lib/lh-error.js | docTypeInvalid": { - "message": "The webpage you have provided appears to be non-HTML" + "message": "The page provided is not HTML" }, "lighthouse-core/lib/lh-error.js | erroredRequiredArtifact": { "message": "Required {artifactName} gatherer encountered an error: {errorMessage}" diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index 0ae629d7a6a7..62475ee16f64 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1593,7 +1593,7 @@ "message": "D̂ŃŜ śêŕv̂ér̂ś ĉóûĺd̂ ńôt́ r̂éŝól̂v́ê t́ĥé p̂ŕôv́îd́êd́ d̂óm̂áîń." }, "lighthouse-core/lib/lh-error.js | docTypeInvalid": { - "message": "T̂h́ê ẃêb́p̂áĝé ŷóû h́âv́ê ṕr̂óv̂íd̂éd̂ áp̂ṕêár̂ś t̂ó b̂é n̂ón̂-H́T̂ḾL̂" + "message": "T̂h́ê ṕâǵê ṕr̂óv̂íd̂éd̂ íŝ ńôt́ ĤT́M̂Ĺ" }, "lighthouse-core/lib/lh-error.js | erroredRequiredArtifact": { "message": "R̂éq̂úîŕêd́ {artifactName} ĝát̂h́êŕêŕ êńĉóûńt̂ér̂éd̂ án̂ ér̂ŕôŕ: {errorMessage}" diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js index 7e58ae7e3994..8385a408ecea 100644 --- a/lighthouse-core/lib/lh-error.js +++ b/lighthouse-core/lib/lh-error.js @@ -48,7 +48,7 @@ const UIStrings = { /** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */ requestContentTimeout: 'Fetching resource content has exceeded the allotted time', /** Error message explaining that the webpage is non-HTML, so audits are ill-defined **/ - docTypeInvalid: 'The webpage you have provided appears to be non-HTML', + docTypeInvalid: 'The page provided is not HTML', /** Error message explaining that the provided URL Lighthouse points to is not valid, and cannot be loaded. */ urlInvalid: 'The URL you have provided appears to be invalid.', /** diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 36330ec30b25..5ac6496391ba 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -1161,30 +1161,28 @@ describe('GatherRunner', function() { navigationError = /** @type {LH.LighthouseError} */ (new Error('NAVIGATION_ERROR')); }); - it('passes when the page is loaded and doc type is text/html', () => { + it('passes when the page is loaded', () => { const passContext = { url: 'http://the-page.com', passConfig: {loadFailureMode: LoadFailureMode.fatal}, }; const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; - const mimeType = 'text/html'; mainRecord.url = passContext.url; - mainRecord.mimeType = mimeType; + mainRecord.mimeType = 'text/html'; const error = GatherRunner.getPageLoadError(passContext, loadData, undefined); expect(error).toBeUndefined(); }); - it('passes when the page is loaded and doc type is text/html, ignoring any fragment', () => { + it('passes when the page is loaded, ignoring any fragment', () => { const passContext = { url: 'http://example.com/#/page/list', passConfig: {loadFailureMode: LoadFailureMode.fatal}, }; const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; - const mimeType = 'text/html'; mainRecord.url = 'http://example.com'; - mainRecord.mimeType = mimeType; + mainRecord.mimeType = 'text/html'; const error = GatherRunner.getPageLoadError(passContext, loadData, undefined); expect(error).toBeUndefined(); }); @@ -1244,9 +1242,8 @@ describe('GatherRunner', function() { const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; - const mimeType = 'application/xml'; mainRecord.url = passContext.url; - mainRecord.mimeType = mimeType; + mainRecord.mimeType = 'application/xml'; const error = getAndExpectError(passContext, loadData, navigationError); expect(error.message).toEqual('INVALID_DOC_TYPE'); @@ -1260,9 +1257,8 @@ describe('GatherRunner', function() { const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; - const mimeType = 'text/html'; mainRecord.url = passContext.url; - mainRecord.mimeType = mimeType; + mainRecord.mimeType = 'text/html'; const error = getAndExpectError(passContext, loadData, navigationError); expect(error.message).toEqual('NAVIGATION_ERROR'); @@ -1276,9 +1272,8 @@ describe('GatherRunner', function() { const mainRecord = new NetworkRequest(); const loadData = {networkRecords: [mainRecord]}; - const mimeType = 'text/html'; mainRecord.url = passContext.url; - mainRecord.mimeType = mimeType; + mainRecord.mimeType = 'text/html'; const error = getAndExpectError(passContext, loadData, navigationError); expect(error.message).toEqual('NAVIGATION_ERROR'); From ba1deddf881f7998382e06c18f5ed0e1899d0322 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 1 Jul 2020 09:57:16 -0700 Subject: [PATCH 09/40] more code review changes --- lighthouse-core/test/gather/gather-runner-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 5ac6496391ba..ff8754c1c5f8 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -1137,7 +1137,7 @@ describe('GatherRunner', function() { const error = getAndExpectError(mainRecord); expect(error.message).toEqual('INVALID_DOC_TYPE'); expect(error.code).toEqual('INVALID_DOC_TYPE'); - expect(error.friendlyMessage).toBeDisplayString(/appears to be non-HTML/); + expect(error.friendlyMessage).toBeDisplayString(/The page provided is not HTML/); }); }); From 85ac35a0d8db705259d8a04cf3149c97f46c63c0 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 1 Jul 2020 17:35:59 -0700 Subject: [PATCH 10/40] committed new strings for testing --- lighthouse-core/lib/i18n/locales/en-US.json | 6 +++--- lighthouse-core/lib/i18n/locales/en-XL.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index 0f1856d2465c..1a62d12f60bf 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1592,9 +1592,6 @@ "lighthouse-core/lib/lh-error.js | dnsFailure": { "message": "DNS servers could not resolve the provided domain." }, - "lighthouse-core/lib/lh-error.js | docTypeInvalid": { - "message": "The page provided is not HTML" - }, "lighthouse-core/lib/lh-error.js | erroredRequiredArtifact": { "message": "Required {artifactName} gatherer encountered an error: {errorMessage}" }, @@ -1604,6 +1601,9 @@ "lighthouse-core/lib/lh-error.js | missingRequiredArtifact": { "message": "Required {artifactName} gatherer did not run." }, + "lighthouse-core/lib/lh-error.js | nonHtml": { + "message": "The page provided is not HTML (served as MIME type {mimeType})." + }, "lighthouse-core/lib/lh-error.js | pageLoadFailed": { "message": "Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests." }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index 62475ee16f64..a8ee59c6143d 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1592,9 +1592,6 @@ "lighthouse-core/lib/lh-error.js | dnsFailure": { "message": "D̂ŃŜ śêŕv̂ér̂ś ĉóûĺd̂ ńôt́ r̂éŝól̂v́ê t́ĥé p̂ŕôv́îd́êd́ d̂óm̂áîń." }, - "lighthouse-core/lib/lh-error.js | docTypeInvalid": { - "message": "T̂h́ê ṕâǵê ṕr̂óv̂íd̂éd̂ íŝ ńôt́ ĤT́M̂Ĺ" - }, "lighthouse-core/lib/lh-error.js | erroredRequiredArtifact": { "message": "R̂éq̂úîŕêd́ {artifactName} ĝát̂h́êŕêŕ êńĉóûńt̂ér̂éd̂ án̂ ér̂ŕôŕ: {errorMessage}" }, @@ -1604,6 +1601,9 @@ "lighthouse-core/lib/lh-error.js | missingRequiredArtifact": { "message": "R̂éq̂úîŕêd́ {artifactName} ĝát̂h́êŕêŕ d̂íd̂ ńôt́ r̂ún̂." }, + "lighthouse-core/lib/lh-error.js | nonHtml": { + "message": "T̂h́ê ṕâǵê ṕr̂óv̂íd̂éd̂ íŝ ńôt́ ĤT́M̂Ĺ (ŝér̂v́êd́ âś M̂ÍM̂É t̂ýp̂é {mimeType})." + }, "lighthouse-core/lib/lh-error.js | pageLoadFailed": { "message": "L̂íĝh́t̂h́ôúŝé ŵáŝ ún̂áb̂ĺê t́ô ŕêĺîáb̂ĺŷ ĺôád̂ t́ĥé p̂áĝé ŷóû ŕêq́ûéŝt́êd́. M̂ák̂é ŝúr̂é ŷóû ár̂é t̂éŝt́îńĝ t́ĥé ĉór̂ŕêćt̂ ÚR̂Ĺ âńd̂ t́ĥát̂ t́ĥé ŝér̂v́êŕ îś p̂ŕôṕêŕl̂ý r̂éŝṕôńd̂ín̂ǵ t̂ó âĺl̂ ŕêq́ûéŝt́ŝ." }, From 4f1b3211e13e60dd383c340af23ac16d31e5b1d1 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 1 Jul 2020 18:02:54 -0700 Subject: [PATCH 11/40] even more code review changes --- lighthouse-core/gather/gather-runner.js | 11 +++---- lighthouse-core/lib/lh-error.js | 14 ++++++--- .../test/gather/gather-runner-test.js | 31 +++++++------------ proto/lighthouse-result.proto | 2 ++ 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index 0ede58d8546e..bf87a9952ff6 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -220,21 +220,18 @@ class GatherRunner { * @param {LH.Artifacts.NetworkRequest|undefined} mainRecord * @return {LH.LighthouseError|undefined} */ - static getDocTypeError(mainRecord) { + static getNonHtmlError(mainRecord) { // MIME types are case-insenstive const HTML_MIME_REGEX = /^text\/html$/i; // If we never requested a document, there's no doctype error, let other cases handle it. if (!mainRecord) return undefined; - // If the main document failed, this error case is undefined, let other cases handle it. - if (mainRecord.failed) return undefined; - // mimeType is determined by the browser, we assume Chrome is determining mimeType correctly, // independently of 'Content-Type' response headers, and always sending mimeType if well-formed. if (mainRecord.mimeType || mainRecord.mimeType === '') { if (!HTML_MIME_REGEX.test(mainRecord.mimeType)) { - return new LHError(LHError.errors.INVALID_DOC_TYPE); + return new LHError(LHError.errors.NON_HTML, {mimeType: mainRecord.mimeType}); } } return undefined; @@ -258,7 +255,7 @@ class GatherRunner { const networkError = GatherRunner.getNetworkError(mainRecord); const interstitialError = GatherRunner.getInterstitialError(mainRecord, networkRecords); - const docTypeError = GatherRunner.getDocTypeError(mainRecord); + const nonHtmlError = GatherRunner.getNonHtmlError(mainRecord); // Check to see if we need to ignore the page load failure. // e.g. When the driver is offline, the load will fail without page offline support. @@ -273,7 +270,7 @@ class GatherRunner { if (networkError) return networkError; // Error if page is not HTML. - if (docTypeError) return docTypeError; + if (nonHtmlError) return nonHtmlError; // Navigation errors are rather generic and express some failure of the page to render properly. // Use `navigationError` as the last resort. diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js index 8385a408ecea..302286df5d16 100644 --- a/lighthouse-core/lib/lh-error.js +++ b/lighthouse-core/lib/lh-error.js @@ -47,8 +47,11 @@ const UIStrings = { internalChromeError: 'An internal Chrome error occurred. Please restart Chrome and try re-running Lighthouse.', /** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */ requestContentTimeout: 'Fetching resource content has exceeded the allotted time', - /** Error message explaining that the webpage is non-HTML, so audits are ill-defined **/ - docTypeInvalid: 'The page provided is not HTML', + /** + * @description Error message explaining that the webpage is non-HTML, so audits are ill-defined. + * @example {application/xml} mimeType + * */ + nonHtml: 'The page provided is not HTML (served as MIME type {mimeType}).', /** Error message explaining that the provided URL Lighthouse points to is not valid, and cannot be loaded. */ urlInvalid: 'The URL you have provided appears to be invalid.', /** @@ -315,9 +318,10 @@ const ERRORS = { lhrRuntimeError: true, }, /* Used when the page is non-HTML. */ - INVALID_DOC_TYPE: { - code: 'INVALID_DOC_TYPE', - message: UIStrings.docTypeInvalid, + NON_HTML: { + code: 'NON_HTML', + message: UIStrings.nonHtml, + lhrRuntimeError: true, }, // Protocol internal failures diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index ff8754c1c5f8..6a26ee90140e 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -38,7 +38,7 @@ const GatherRunner = { getInstallabilityErrors: makeParamsOptional(GatherRunner_.getInstallabilityErrors), getInterstitialError: makeParamsOptional(GatherRunner_.getInterstitialError), getNetworkError: makeParamsOptional(GatherRunner_.getNetworkError), - getDocTypeError: makeParamsOptional(GatherRunner_.getDocTypeError), + getNonHtmlError: makeParamsOptional(GatherRunner_.getNonHtmlError), getPageLoadError: makeParamsOptional(GatherRunner_.getPageLoadError), getWebAppManifest: makeParamsOptional(GatherRunner_.getWebAppManifest), initializeBaseArtifacts: makeParamsOptional(GatherRunner_.initializeBaseArtifacts), @@ -1096,27 +1096,18 @@ describe('GatherRunner', function() { }); }); - describe('#getDocTypeError', () => { + describe('#getNonHtmlError', () => { /** * @param {NetworkRequest} mainRecord */ function getAndExpectError(mainRecord) { - const error = GatherRunner.getDocTypeError(mainRecord); - if (!error) throw new Error('expected a docType error'); + const error = GatherRunner.getNonHtmlError(mainRecord); + if (!error) throw new Error('expected a non-HTML error'); return error; } it('passes when the page was not requested', () => { - expect(GatherRunner.getDocTypeError(undefined)).toBeUndefined(); - }); - - it('passes when page fails to load normally', () => { - const url = 'http://the-page.com'; - const mainRecord = new NetworkRequest(); - mainRecord.url = url; - mainRecord.failed = true; - mainRecord.localizedFailDescription = 'foobar'; - expect(GatherRunner.getDocTypeError(mainRecord)).toBeUndefined(); + expect(GatherRunner.getNonHtmlError(undefined)).toBeUndefined(); }); it('passes when the page is of MIME type text/html', () => { @@ -1125,7 +1116,7 @@ describe('GatherRunner', function() { const mimeType = 'text/html'; mainRecord.url = url; mainRecord.mimeType = mimeType; - expect(GatherRunner.getDocTypeError(mainRecord)).toBeUndefined(); + expect(GatherRunner.getNonHtmlError(mainRecord)).toBeUndefined(); }); it('fails when the page is not of MIME type text/html', () => { @@ -1135,9 +1126,9 @@ describe('GatherRunner', function() { mainRecord.url = url; mainRecord.mimeType = mimeType; const error = getAndExpectError(mainRecord); - expect(error.message).toEqual('INVALID_DOC_TYPE'); - expect(error.code).toEqual('INVALID_DOC_TYPE'); - expect(error.friendlyMessage).toBeDisplayString(/The page provided is not HTML/); + expect(error.message).toEqual('NON_HTML'); + expect(error.code).toEqual('NON_HTML'); + expect(error.friendlyMessage).toBeDisplayString(/is not HTML \(served as/); }); }); @@ -1234,7 +1225,7 @@ describe('GatherRunner', function() { expect(error.message).toEqual('FAILED_DOCUMENT_REQUEST'); }); - it('fails with doc type error third', () => { + it('fails with non-HTML error third', () => { const passContext = { url: 'http://the-page.com', passConfig: {loadFailureMode: LoadFailureMode.fatal}, @@ -1246,7 +1237,7 @@ describe('GatherRunner', function() { mainRecord.mimeType = 'application/xml'; const error = getAndExpectError(passContext, loadData, navigationError); - expect(error.message).toEqual('INVALID_DOC_TYPE'); + expect(error.message).toEqual('NON_HTML'); }); it('fails with nav error last', () => { diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto index ece8b4739aaa..16067949b08d 100644 --- a/proto/lighthouse-result.proto +++ b/proto/lighthouse-result.proto @@ -55,6 +55,8 @@ enum LighthouseError { DNS_FAILURE = 19; // A timeout in the initial connection to the debugger protocol. CRI_TIMEOUT = 20; + // The page requested was non-HTML. + NON_HTML = 21; } // The overarching Lighthouse Response object (LHR) From 9dd709d63007d5038da73c7ad0b603e8addd60d5 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 7 Jul 2020 15:33:25 -0700 Subject: [PATCH 12/40] changed naming from nonHtml to notHTML, removed an unnecessary if statement in getNonHtmlError --- lighthouse-core/gather/gather-runner.js | 6 ++---- lighthouse-core/lib/lh-error.js | 8 ++++---- lighthouse-core/test/gather/gather-runner-test.js | 6 +++--- proto/lighthouse-result.proto | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index bf87a9952ff6..ad60d2331562 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -229,10 +229,8 @@ class GatherRunner { // mimeType is determined by the browser, we assume Chrome is determining mimeType correctly, // independently of 'Content-Type' response headers, and always sending mimeType if well-formed. - if (mainRecord.mimeType || mainRecord.mimeType === '') { - if (!HTML_MIME_REGEX.test(mainRecord.mimeType)) { - return new LHError(LHError.errors.NON_HTML, {mimeType: mainRecord.mimeType}); - } + if (!HTML_MIME_REGEX.test(mainRecord.mimeType)) { + return new LHError(LHError.errors.NOT_HTML, {mimeType: mainRecord.mimeType}); } return undefined; } diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js index 302286df5d16..1f85256b41b6 100644 --- a/lighthouse-core/lib/lh-error.js +++ b/lighthouse-core/lib/lh-error.js @@ -51,7 +51,7 @@ const UIStrings = { * @description Error message explaining that the webpage is non-HTML, so audits are ill-defined. * @example {application/xml} mimeType * */ - nonHtml: 'The page provided is not HTML (served as MIME type {mimeType}).', + notHtml: 'The page provided is not HTML (served as MIME type {mimeType}).', /** Error message explaining that the provided URL Lighthouse points to is not valid, and cannot be loaded. */ urlInvalid: 'The URL you have provided appears to be invalid.', /** @@ -318,9 +318,9 @@ const ERRORS = { lhrRuntimeError: true, }, /* Used when the page is non-HTML. */ - NON_HTML: { - code: 'NON_HTML', - message: UIStrings.nonHtml, + NOT_HTML: { + code: 'NOT_HTML', + message: UIStrings.notHtml, lhrRuntimeError: true, }, diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 6a26ee90140e..6a6b9729e5a8 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -1126,8 +1126,8 @@ describe('GatherRunner', function() { mainRecord.url = url; mainRecord.mimeType = mimeType; const error = getAndExpectError(mainRecord); - expect(error.message).toEqual('NON_HTML'); - expect(error.code).toEqual('NON_HTML'); + expect(error.message).toEqual('NOT_HTML'); + expect(error.code).toEqual('NOT_HTML'); expect(error.friendlyMessage).toBeDisplayString(/is not HTML \(served as/); }); }); @@ -1237,7 +1237,7 @@ describe('GatherRunner', function() { mainRecord.mimeType = 'application/xml'; const error = getAndExpectError(passContext, loadData, navigationError); - expect(error.message).toEqual('NON_HTML'); + expect(error.message).toEqual('NOT_HTML'); }); it('fails with nav error last', () => { diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto index 16067949b08d..bfcc80dd9a32 100644 --- a/proto/lighthouse-result.proto +++ b/proto/lighthouse-result.proto @@ -56,7 +56,7 @@ enum LighthouseError { // A timeout in the initial connection to the debugger protocol. CRI_TIMEOUT = 20; // The page requested was non-HTML. - NON_HTML = 21; + NOT_HTML = 21; } // The overarching Lighthouse Response object (LHR) From cd32b47e310aa0cf03fefc7cd937bb6d6002f9fd Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 7 Jul 2020 15:34:36 -0700 Subject: [PATCH 13/40] added changed i18n strings --- lighthouse-core/lib/i18n/locales/en-US.json | 2 +- lighthouse-core/lib/i18n/locales/en-XL.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index 1a62d12f60bf..fd70b5f77401 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1601,7 +1601,7 @@ "lighthouse-core/lib/lh-error.js | missingRequiredArtifact": { "message": "Required {artifactName} gatherer did not run." }, - "lighthouse-core/lib/lh-error.js | nonHtml": { + "lighthouse-core/lib/lh-error.js | notHtml": { "message": "The page provided is not HTML (served as MIME type {mimeType})." }, "lighthouse-core/lib/lh-error.js | pageLoadFailed": { diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index a8ee59c6143d..f0dc69892d0f 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1601,7 +1601,7 @@ "lighthouse-core/lib/lh-error.js | missingRequiredArtifact": { "message": "R̂éq̂úîŕêd́ {artifactName} ĝát̂h́êŕêŕ d̂íd̂ ńôt́ r̂ún̂." }, - "lighthouse-core/lib/lh-error.js | nonHtml": { + "lighthouse-core/lib/lh-error.js | notHtml": { "message": "T̂h́ê ṕâǵê ṕr̂óv̂íd̂éd̂ íŝ ńôt́ ĤT́M̂Ĺ (ŝér̂v́êd́ âś M̂ÍM̂É t̂ýp̂é {mimeType})." }, "lighthouse-core/lib/lh-error.js | pageLoadFailed": { From 8e3b37149f1d3dfbfd52375d76d6b093372b2499 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 7 Jul 2020 17:13:17 -0700 Subject: [PATCH 14/40] removed regex and added const str for comparison --- lighthouse-core/gather/gather-runner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index 6356966fb530..be34109f5222 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -223,14 +223,14 @@ class GatherRunner { */ static getNonHtmlError(mainRecord) { // MIME types are case-insenstive - const HTML_MIME_REGEX = /^text\/html$/i; + const HTML_MIME_TYPE = 'text/html'; // If we never requested a document, there's no doctype error, let other cases handle it. if (!mainRecord) return undefined; // mimeType is determined by the browser, we assume Chrome is determining mimeType correctly, // independently of 'Content-Type' response headers, and always sending mimeType if well-formed. - if (!HTML_MIME_REGEX.test(mainRecord.mimeType)) { + if (HTML_MIME_TYPE !== mainRecord.mimeType) { return new LHError(LHError.errors.NOT_HTML, {mimeType: mainRecord.mimeType}); } return undefined; From 6f90641a9a1885a90cab43a678fc9c01ae7b24ab Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 7 Jul 2020 17:18:33 -0700 Subject: [PATCH 15/40] included a comment about Chrome MIME type normalization --- lighthouse-core/gather/gather-runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index be34109f5222..f1a4ba6d0d39 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -222,7 +222,7 @@ class GatherRunner { * @return {LH.LighthouseError|undefined} */ static getNonHtmlError(mainRecord) { - // MIME types are case-insenstive + // MIME types are case-insenstive but Chrome normalizes MIME types to be lowercase. const HTML_MIME_TYPE = 'text/html'; // If we never requested a document, there's no doctype error, let other cases handle it. From d8437ca2c7b996d4df16d5fabd9ca51f9518235e Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Thu, 16 Jul 2020 13:54:19 -0700 Subject: [PATCH 16/40] started fleshing out new audit --- lighthouse-core/audits/sized-images.js | 83 +++++++++++++++++++ .../gather/gatherers/image-elements.js | 4 + types/artifacts.d.ts | 4 + 3 files changed, 91 insertions(+) create mode 100644 lighthouse-core/audits/sized-images.js diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js new file mode 100644 index 000000000000..c0441421b2fb --- /dev/null +++ b/lighthouse-core/audits/sized-images.js @@ -0,0 +1,83 @@ +/** + * @license Copyright 2020 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const Audit = require('./audit.js'); +const i18n = require('./../lib/i18n/i18n.js'); + +const UIStrings = { + /** Short, user-visible title for the audit when successful. */ + title: '', + /** Short, user-visible title for the audit when failing. */ + failureTitle: '', + /** A more detailed description that describes why the audit is important and links to Lighthouse documentation on the audit; markdown links supported. */ + description: '', +}; + +const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); + +/** + * @fileoverview + * Audits if a + */ + +class SizedImages extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'sized-images', + title: str_(UIStrings.title), + failureTitle: str_(UIStrings.failureTitle), + description: str_(UIStrings.description), + requiredArtifacts: ['ImageElements'], + }; + } + + /** + * @param {string} attr + * @return {boolean} + */ + static isValid(attr) { + // an img size attribute is valid for preventing CLS + // if it is a non-negative, non-zero integer + const NON_NEGATIVE_INT_REGEX = /^\d+$/; + const ZERO_REGEX = /^0+$/; + return NON_NEGATIVE_INT_REGEX.test(attr) && !ZERO_REGEX.test(attr); + } + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async audit(artifacts, context) { + // CSS background-images are ignored for this audit + const images = artifacts.ImageElements.filter(el => !el.isCss); + const unsizedImages = []; + + for (const image of images) { + const width = image.attributeWidth; + const height = image.attributeHeight; + // images are considered sized if they have defined & valid values + if (!width || !height || !SizedImages.isValid(width) || !SizedImages.isValid(height)) { + unsizedImages.push(image); + } + } + + + + return { + score: unsizedImages.length > 0 ? 0 : 1, + notApplicable: images.length === 0, + details: , + }; + } +} + +module.exports = SizedImages; +module.exports.UIStrings = UIStrings; diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index d9ca423e48e3..c54847be19bf 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -50,6 +50,8 @@ function getHTMLImages(allElements) { clientRect: getClientRect(element), naturalWidth: element.naturalWidth, naturalHeight: element.naturalHeight, + attributeWidth: element.getAttribute('width') || '', + attributeHeight: element.getAttribute('height') || '', isCss: false, // @ts-ignore: loading attribute not yet added to HTMLImageElement definition. loading: element.loading, @@ -97,6 +99,8 @@ function getCSSImages(allElements) { // CSS Images do not expose natural size, we'll determine the size later naturalWidth: 0, naturalHeight: 0, + attributeWidth: element.getAttribute('width') || '', + attributeHeight: element.getAttribute('height') || '', isCss: true, isPicture: false, usesObjectFit: false, diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index a6cfefd26b0c..1b18f66e23fc 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -397,6 +397,10 @@ declare global { naturalWidth: number; /** The natural height of the underlying image, uses img.naturalHeight. See https://codepen.io/patrickhulce/pen/PXvQbM for examples. */ naturalHeight: number; + /** The attribute width of the image, ... */ + attributeWidth: string; + /** The attribute height of the image, ... */ + attributeHeight: string; /** The BoundingClientRect of the element. */ clientRect: { top: number; From 0fca1e6f4f08536020a453342d2b24e0a3c8f204 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Thu, 16 Jul 2020 15:27:30 -0700 Subject: [PATCH 17/40] working audit on basic examples --- lighthouse-core/audits/sized-images.js | 32 +++++++++++++++---- lighthouse-core/config/default-config.js | 2 ++ .../gather/gatherers/image-elements.js | 22 ++++++++++++- types/artifacts.d.ts | 5 +++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js index c0441421b2fb..cd5c4254d2e0 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/sized-images.js @@ -7,14 +7,17 @@ const Audit = require('./audit.js'); const i18n = require('./../lib/i18n/i18n.js'); +const URL = require('./../lib/url-shim.js'); const UIStrings = { /** Short, user-visible title for the audit when successful. */ - title: '', + title: 'Success', /** Short, user-visible title for the audit when failing. */ - failureTitle: '', + failureTitle: 'Failure', /** A more detailed description that describes why the audit is important and links to Lighthouse documentation on the audit; markdown links supported. */ - description: '', + description: 'Description', + /** Table column header for the HTML elements that do not allow pasting of content. */ + columnFailingElem: 'Failing Elements', }; const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); @@ -65,16 +68,33 @@ class SizedImages extends Audit { const height = image.attributeHeight; // images are considered sized if they have defined & valid values if (!width || !height || !SizedImages.isValid(width) || !SizedImages.isValid(height)) { - unsizedImages.push(image); + + const url = URL.elideDataURI(image.src); + + unsizedImages.push({ + url, + node: /** @type {LH.Audit.Details.NodeValue} */ ({ + type: 'node', + path: image.devtoolsNodePath, + selector: image.selector, + nodeLabel: image.nodeLabel, + snippet: image.snippet, + }), + }); } } - + /** @type {LH.Audit.Details.Table['headings']} */ + const headings = [ + {key: 'url', itemType: 'thumbnail', text: ''}, + {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)}, + {key: 'node', itemType: 'node', text: str_(UIStrings.columnFailingElem)}, + ]; return { score: unsizedImages.length > 0 ? 0 : 1, notApplicable: images.length === 0, - details: , + details: Audit.makeTableDetails(headings, unsizedImages), }; } } diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js index 9d6f841489b0..ac672db35d39 100644 --- a/lighthouse-core/config/default-config.js +++ b/lighthouse-core/config/default-config.js @@ -216,6 +216,7 @@ const defaultConfig = { 'content-width', 'image-aspect-ratio', 'image-size-responsive', + 'sized-images', 'deprecations', 'mainthread-work-breakdown', 'bootup-time', @@ -546,6 +547,7 @@ const defaultConfig = { {id: 'no-vulnerable-libraries', weight: 1, group: 'best-practices-trust-safety'}, // User Experience {id: 'password-inputs-can-be-pasted-into', weight: 1, group: 'best-practices-ux'}, + {id: 'sized-images', weight: 1, group: 'best-practices-ux'}, {id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'}, {id: 'image-size-responsive', weight: 1, group: 'best-practices-ux'}, // Browser Compatibility diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index c54847be19bf..fb77f993e636 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -13,7 +13,7 @@ const Gatherer = require('./gatherer.js'); const pageFunctions = require('../../lib/page-functions.js'); const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars -/* global window, getElementsInDocument, Image */ +/* global window, getElementsInDocument, Image, getNodePath, getNodeSelector, getNodeLabel, getOuterHTMLSnippet */ /** @param {Element} element */ @@ -65,6 +65,14 @@ function getHTMLImages(allElements) { ), // https://html.spec.whatwg.org/multipage/images.html#pixel-density-descriptor usesSrcSetDensityDescriptor: / \d+(\.\d+)?x/.test(element.srcset), + // @ts-ignore - getNodePath put into scope via stringification + devtoolsNodePath: getNodePath(element), + // @ts-ignore - put into scope via stringification + selector: getNodeSelector(element), + // @ts-ignore - put into scope via stringification + nodeLabel: getNodeLabel(element), + // @ts-ignore - put into scope via stringification + snippet: getOuterHTMLSnippet(element), }; }); } @@ -109,6 +117,14 @@ function getCSSImages(allElements) { ), usesSrcSetDensityDescriptor: false, resourceSize: 0, // this will get overwritten below + // @ts-ignore - getNodePath put into scope via stringification + devtoolsNodePath: getNodePath(element), + // @ts-ignore - put into scope via stringification + selector: getNodeSelector(element), + // @ts-ignore - put into scope via stringification + nodeLabel: getNodeLabel(element), + // @ts-ignore - put into scope via stringification + snippet: getOuterHTMLSnippet(element), }); } @@ -194,6 +210,10 @@ class ImageElements extends Gatherer { const expression = `(function() { ${pageFunctions.getElementsInDocumentString}; // define function on page + ${pageFunctions.getNodePathString}; + ${pageFunctions.getNodeSelectorString}; + ${pageFunctions.getNodeLabelString}; + ${pageFunctions.getOuterHTMLSnippetString}; ${getClientRect.toString()}; ${getHTMLImages.toString()}; ${getCSSImages.toString()}; diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 1b18f66e23fc..48ffc1766c65 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -425,6 +425,11 @@ declare global { usesSrcSetDensityDescriptor: boolean; /** The size of the underlying image file in bytes. 0 if the file could not be identified. */ resourceSize: number; + /** Path that uniquely identifies the node in the DOM */ + devtoolsNodePath: string; + snippet: string; + selector: string; + nodeLabel: string; /** The MIME type of the underlying image file. */ mimeType?: string; /** The loading attribute of the image. */ From 37628a89c945375c97991e7bc03e54442f7d1cad Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Thu, 16 Jul 2020 16:29:28 -0700 Subject: [PATCH 18/40] removed spaces and added titles and comments --- lighthouse-core/audits/sized-images.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js index cd5c4254d2e0..89bcf6842e44 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/sized-images.js @@ -10,13 +10,13 @@ const i18n = require('./../lib/i18n/i18n.js'); const URL = require('./../lib/url-shim.js'); const UIStrings = { - /** Short, user-visible title for the audit when successful. */ - title: 'Success', - /** Short, user-visible title for the audit when failing. */ - failureTitle: 'Failure', - /** A more detailed description that describes why the audit is important and links to Lighthouse documentation on the audit; markdown links supported. */ - description: 'Description', - /** Table column header for the HTML elements that do not allow pasting of content. */ + /** Title of a Lighthouse audit that provides detail on whether all images had width and height attributes. This descriptive title is shown to users when every image has width and height attributes */ + title: 'Image elements have `width` and `height` attributes', + /** Title of a Lighthouse audit that provides detail on whether all images had width and height attributes. This descriptive title is shown to users when one or more images does not have width and height attributes */ + failureTitle: 'Image elements do not have `width` and `height` attributes', + /** Description of a Lighthouse audit that tells the user why they should include width and height attributes for all images. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ + description: 'Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', + /** Table column header for the image elements that do not have image and height attributes. */ columnFailingElem: 'Failing Elements', }; @@ -68,9 +68,7 @@ class SizedImages extends Audit { const height = image.attributeHeight; // images are considered sized if they have defined & valid values if (!width || !height || !SizedImages.isValid(width) || !SizedImages.isValid(height)) { - const url = URL.elideDataURI(image.src); - unsizedImages.push({ url, node: /** @type {LH.Audit.Details.NodeValue} */ ({ From 488f73bb405dc6c4305c837408896e06941a1924 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Thu, 16 Jul 2020 16:54:45 -0700 Subject: [PATCH 19/40] added a file overview --- lighthouse-core/audits/sized-images.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js index 89bcf6842e44..884ec23c6e83 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/sized-images.js @@ -24,7 +24,7 @@ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); /** * @fileoverview - * Audits if a + * Audit that checks whether all images have width and height attributes. */ class SizedImages extends Audit { From a6417f143470e59180e800515ddb527eacbba399 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Thu, 16 Jul 2020 17:03:31 -0700 Subject: [PATCH 20/40] baked new strings & changed duplicate string assertion --- lighthouse-core/lib/i18n/locales/en-US.json | 12 ++++++++++++ lighthouse-core/lib/i18n/locales/en-XL.json | 12 ++++++++++++ lighthouse-core/scripts/i18n/collect-strings.js | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index 610dd40e9352..6deb30d44181 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1205,6 +1205,18 @@ "lighthouse-core/audits/service-worker.js | title": { "message": "Registers a service worker that controls page and `start_url`" }, + "lighthouse-core/audits/sized-images.js | columnFailingElem": { + "message": "Failing Elements" + }, + "lighthouse-core/audits/sized-images.js | description": { + "message": "Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)" + }, + "lighthouse-core/audits/sized-images.js | failureTitle": { + "message": "Image elements do not have `width` and `height` attributes" + }, + "lighthouse-core/audits/sized-images.js | title": { + "message": "Image elements have `width` and `height` attributes" + }, "lighthouse-core/audits/splash-screen.js | description": { "message": "A themed splash screen ensures a high-quality experience when users launch your app from their homescreens. [Learn more](https://web.dev/splash-screen/)." }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index 488563e86a32..ac6fadb54cb0 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1205,6 +1205,18 @@ "lighthouse-core/audits/service-worker.js | title": { "message": "R̂éĝíŝt́êŕŝ á ŝér̂v́îćê ẃôŕk̂ér̂ t́ĥát̂ ćôńt̂ŕôĺŝ ṕâǵê án̂d́ `start_url`" }, + "lighthouse-core/audits/sized-images.js | columnFailingElem": { + "message": "F̂áîĺîńĝ Él̂ém̂én̂t́ŝ" + }, + "lighthouse-core/audits/sized-images.js | description": { + "message": "Âĺŵáŷś îńĉĺûd́ê ẃîd́t̂h́ âńd̂ h́êíĝh́t̂ át̂t́r̂íb̂út̂éŝ ón̂ ýôúr̂ ím̂áĝé êĺêḿêńt̂ś t̂ó r̂éd̂úĉé l̂áŷóût́ ŝh́îf́t̂ín̂ǵ âńd̂ ím̂ṕr̂óv̂é ĈĹŜ. [Ĺêár̂ń m̂ór̂é](https://web.dev/optimize-cls/#images-without-dimensions)" + }, + "lighthouse-core/audits/sized-images.js | failureTitle": { + "message": "Îḿâǵê él̂ém̂én̂t́ŝ d́ô ńôt́ ĥáv̂é `width` âńd̂ `height` át̂t́r̂íb̂út̂éŝ" + }, + "lighthouse-core/audits/sized-images.js | title": { + "message": "Îḿâǵê él̂ém̂én̂t́ŝ h́âv́ê `width` án̂d́ `height` ât́t̂ŕîb́ût́êś" + }, "lighthouse-core/audits/splash-screen.js | description": { "message": " t́ĥém̂éd̂ śp̂ĺâśĥ śĉŕêén̂ én̂śûŕêś â h́îǵĥ-q́ûál̂ít̂ý êx́p̂ér̂íêńĉé ŵh́êń ûśêŕŝ ĺâún̂ćĥ ýôúr̂ áp̂ṕ f̂ŕôḿ t̂h́êír̂ h́ôḿêśĉŕêén̂ś. [L̂éâŕn̂ ḿôŕê](https://web.dev/splash-screen/)." }, diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index 0e07b85e8757..56f05c61975f 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -570,7 +570,7 @@ if (require.main === module) { if ((collisions) > 0) { console.log(`MEANING COLLISION: ${collisions} string(s) have the same content.`); - assert.equal(collisions, 16, `The number of duplicate strings have changed, update this assertion if that is expected, or reword strings. Collisions: ${collisionStrings}`); + assert.equal(collisions, 17, `The number of duplicate strings have changed, update this assertion if that is expected, or reword strings. Collisions: ${collisionStrings}`); } const strings = {...coreStrings, ...stackPackStrings}; From 3cc6c240550994aa0a98f18be3fe1ab2e0042695 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 17 Jul 2020 11:16:51 -0700 Subject: [PATCH 21/40] feedback fixed from draft pr, sized-images-test --- .../password-inputs-can-be-pasted-into.js | 4 +- lighthouse-core/audits/sized-images.js | 7 +- .../gather/gatherers/image-elements.js | 4 +- lighthouse-core/lib/i18n/i18n.js | 2 + lighthouse-core/lib/i18n/locales/ar-XB.json | 3 - lighthouse-core/lib/i18n/locales/ar.json | 3 - lighthouse-core/lib/i18n/locales/bg.json | 3 - lighthouse-core/lib/i18n/locales/ca.json | 3 - lighthouse-core/lib/i18n/locales/cs.json | 3 - lighthouse-core/lib/i18n/locales/da.json | 3 - lighthouse-core/lib/i18n/locales/de.json | 3 - lighthouse-core/lib/i18n/locales/el.json | 3 - lighthouse-core/lib/i18n/locales/en-GB.json | 3 - lighthouse-core/lib/i18n/locales/en-US.json | 9 +- lighthouse-core/lib/i18n/locales/en-XA.json | 3 - lighthouse-core/lib/i18n/locales/en-XL.json | 9 +- lighthouse-core/lib/i18n/locales/es-419.json | 3 - lighthouse-core/lib/i18n/locales/es.json | 3 - lighthouse-core/lib/i18n/locales/fi.json | 3 - lighthouse-core/lib/i18n/locales/fil.json | 3 - lighthouse-core/lib/i18n/locales/fr.json | 3 - lighthouse-core/lib/i18n/locales/he.json | 3 - lighthouse-core/lib/i18n/locales/hi.json | 3 - lighthouse-core/lib/i18n/locales/hr.json | 3 - lighthouse-core/lib/i18n/locales/hu.json | 3 - lighthouse-core/lib/i18n/locales/id.json | 3 - lighthouse-core/lib/i18n/locales/it.json | 3 - lighthouse-core/lib/i18n/locales/ja.json | 3 - lighthouse-core/lib/i18n/locales/ko.json | 3 - lighthouse-core/lib/i18n/locales/lt.json | 3 - lighthouse-core/lib/i18n/locales/lv.json | 3 - lighthouse-core/lib/i18n/locales/nl.json | 3 - lighthouse-core/lib/i18n/locales/no.json | 3 - lighthouse-core/lib/i18n/locales/pl.json | 3 - lighthouse-core/lib/i18n/locales/pt-PT.json | 3 - lighthouse-core/lib/i18n/locales/pt.json | 3 - lighthouse-core/lib/i18n/locales/ro.json | 3 - lighthouse-core/lib/i18n/locales/ru.json | 3 - lighthouse-core/lib/i18n/locales/sk.json | 3 - lighthouse-core/lib/i18n/locales/sl.json | 3 - lighthouse-core/lib/i18n/locales/sr-Latn.json | 3 - lighthouse-core/lib/i18n/locales/sr.json | 3 - lighthouse-core/lib/i18n/locales/sv.json | 3 - lighthouse-core/lib/i18n/locales/ta.json | 3 - lighthouse-core/lib/i18n/locales/te.json | 3 - lighthouse-core/lib/i18n/locales/th.json | 3 - lighthouse-core/lib/i18n/locales/tr.json | 3 - lighthouse-core/lib/i18n/locales/uk.json | 3 - lighthouse-core/lib/i18n/locales/vi.json | 3 - lighthouse-core/lib/i18n/locales/zh-HK.json | 3 - lighthouse-core/lib/i18n/locales/zh-TW.json | 3 - lighthouse-core/lib/i18n/locales/zh.json | 3 - .../scripts/i18n/collect-strings.js | 2 +- .../test/audits/sized-images-test.js | 166 ++++++++++++++++++ types/artifacts.d.ts | 4 +- 55 files changed, 182 insertions(+), 163 deletions(-) create mode 100644 lighthouse-core/test/audits/sized-images-test.js diff --git a/lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js b/lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js index b262e5ba6545..89ca318ce907 100644 --- a/lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js +++ b/lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js @@ -16,8 +16,6 @@ const UIStrings = { /** Description of a Lighthouse audit that tells the user why they should allow pasting of content into password fields. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ description: 'Preventing password pasting undermines good security policy. ' + '[Learn more](https://web.dev/password-inputs-can-be-pasted-into/).', - /** Table column header for the HTML elements that do not allow pasting of content. */ - columnFailingElem: 'Failing Elements', }; const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); @@ -53,7 +51,7 @@ class PasswordInputsCanBePastedIntoAudit extends Audit { /** @type {LH.Audit.Details.Table['headings']} */ const headings = [ - {key: 'node', itemType: 'node', text: str_(UIStrings.columnFailingElem)}, + {key: 'node', itemType: 'node', text: str_(i18n.UIStrings.columnFailingElem)}, ]; return { diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js index 884ec23c6e83..b8bba43c7527 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/sized-images.js @@ -16,8 +16,6 @@ const UIStrings = { failureTitle: 'Image elements do not have `width` and `height` attributes', /** Description of a Lighthouse audit that tells the user why they should include width and height attributes for all images. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ description: 'Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', - /** Table column header for the image elements that do not have image and height attributes. */ - columnFailingElem: 'Failing Elements', }; const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); @@ -55,10 +53,9 @@ class SizedImages extends Audit { /** * @param {LH.Artifacts} artifacts - * @param {LH.Audit.Context} context * @return {Promise} */ - static async audit(artifacts, context) { + static async audit(artifacts) { // CSS background-images are ignored for this audit const images = artifacts.ImageElements.filter(el => !el.isCss); const unsizedImages = []; @@ -86,7 +83,7 @@ class SizedImages extends Audit { const headings = [ {key: 'url', itemType: 'thumbnail', text: ''}, {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)}, - {key: 'node', itemType: 'node', text: str_(UIStrings.columnFailingElem)}, + {key: 'node', itemType: 'node', text: str_(i18n.UIStrings.columnFailingElem)}, ]; return { diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 2ede2ddb98f2..337b48ab30f3 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -109,8 +109,8 @@ function getCSSImages(allElements) { // CSS Images do not expose natural size, we'll determine the size later naturalWidth: 0, naturalHeight: 0, - attributeWidth: element.getAttribute('width') || '', - attributeHeight: element.getAttribute('height') || '', + attributeWidth: '', + attributeHeight: '', isCss: true, isPicture: false, usesObjectFit: false, diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js index eec8d71a3a01..60591ac0e3bc 100644 --- a/lighthouse-core/lib/i18n/i18n.js +++ b/lighthouse-core/lib/i18n/i18n.js @@ -88,6 +88,8 @@ const UIStrings = { columnStartTime: 'Start Time', /** Label for a column in a data table; entries will be the total number of milliseconds from the start time until the end time. */ columnDuration: 'Duration', + /** Label for a column in a data table; entries will be a representation of a DOM element that did not meet certain suggestions. */ + columnFailingElem: 'Failing Elements', /** Label for a row in a data table; entries will be the total number and byte size of all resources loaded by a web page. */ totalResourceType: 'Total', /** Label for a row in a data table; entries will be the total number and byte size of all 'Document' resources loaded by a web page. */ diff --git a/lighthouse-core/lib/i18n/locales/ar-XB.json b/lighthouse-core/lib/i18n/locales/ar-XB.json index 0de2ee591c02..96dcf33741f1 100644 --- a/lighthouse-core/lib/i18n/locales/ar-XB.json +++ b/lighthouse-core/lib/i18n/locales/ar-XB.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "‏‮Avoids‬‏ ‏‮requesting‬‏ ‏‮the‬‏ ‏‮notification‬‏ ‏‮permission‬‏ ‏‮on‬‏ ‏‮page‬‏ ‏‮load‬‏" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "‏‮Failing‬‏ ‏‮Elements‬‏" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "‏‮Preventing‬‏ ‏‮password‬‏ ‏‮pasting‬‏ ‏‮undermines‬‏ ‏‮good‬‏ ‏‮security‬‏ ‏‮policy‬‏. [‏‮Learn‬‏ ‏‮more‬‏](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/ar.json b/lighthouse-core/lib/i18n/locales/ar.json index 46c7563f698e..673641bb4f1c 100644 --- a/lighthouse-core/lib/i18n/locales/ar.json +++ b/lighthouse-core/lib/i18n/locales/ar.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "يتم تجنُّب طلب إذن الإشعار عند تحميل الصفحة" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "العناصر التي لا تسمح بلصق المحتوى" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "يؤدي منع لصق كلمة المرور إلى تقويض سياسة الأمان الجيدة. [مزيد من المعلومات](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/bg.json b/lighthouse-core/lib/i18n/locales/bg.json index 3dd05e1a7ada..ab5fcaf74f55 100644 --- a/lighthouse-core/lib/i18n/locales/bg.json +++ b/lighthouse-core/lib/i18n/locales/bg.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Избягва да иска разрешение за известяване при зареждането на страницата" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Елементи с грешки" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Забраната на поставянето на пароли неутрализира добра практика за сигурност. [Научете повече](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/ca.json b/lighthouse-core/lib/i18n/locales/ca.json index e3aaf65a1284..b5b274544190 100644 --- a/lighthouse-core/lib/i18n/locales/ca.json +++ b/lighthouse-core/lib/i18n/locales/ca.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evita sol·licitar el permís de notificació en carregar la pàgina" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elements amb errors" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Impedir enganxar la contrasenya va en detriment d'una bona política de seguretat. [Obtén més informació](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/cs.json b/lighthouse-core/lib/i18n/locales/cs.json index 2181c619ddf8..e13feec6a2d8 100644 --- a/lighthouse-core/lib/i18n/locales/cs.json +++ b/lighthouse-core/lib/i18n/locales/cs.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Nežádá při načtení stránky o oprávnění zobrazovat oznámení" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Prvky, které neprošly" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Blokování vkládání hesel je v rozporu s dobrými bezpečnostními zásadami. [Další informace](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/da.json b/lighthouse-core/lib/i18n/locales/da.json index ce51be779ab7..96dc9c79219e 100644 --- a/lighthouse-core/lib/i18n/locales/da.json +++ b/lighthouse-core/lib/i18n/locales/da.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Undgår at anmode om tilladelse til notifikationer ved indlæsning af siden" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementer, der ikke bestod gennemgangen" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "En god sikkerhedspolitik undermineres ved at forhindre indsættelse af adgangskoder. [Få flere oplysninger](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/de.json b/lighthouse-core/lib/i18n/locales/de.json index 81cf6a79be75..c921d4f9dfa4 100644 --- a/lighthouse-core/lib/i18n/locales/de.json +++ b/lighthouse-core/lib/i18n/locales/de.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Fordert während des Seitenaufbaus keine Benachrichtigungsberechtigung an" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Fehlerhafte Elemente" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Das Einfügen von Passwörtern sollte entsprechend guten Sicherheitsrichtlinien zulässig sein. [Weitere Informationen.](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/el.json b/lighthouse-core/lib/i18n/locales/el.json index b747812c142f..302745c0613b 100644 --- a/lighthouse-core/lib/i18n/locales/el.json +++ b/lighthouse-core/lib/i18n/locales/el.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Αποφυγή αιτήματος για άδεια ειδοποίησης κατά τη φόρτωση σελίδων" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Στοιχεία που απέτυχαν" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Η απαγόρευση της επικόλλησης κωδικών πρόσβασης υπονομεύει την ορθή πολιτική ασφάλειας. [Μάθετε περισσότερα](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/en-GB.json b/lighthouse-core/lib/i18n/locales/en-GB.json index b11898517022..868c697b1239 100644 --- a/lighthouse-core/lib/i18n/locales/en-GB.json +++ b/lighthouse-core/lib/i18n/locales/en-GB.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Avoids requesting the notification permission on page load" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Failing Elements" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Preventing password pasting undermines good security policy. [Learn more](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index 6deb30d44181..26a81eeeadc9 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -695,9 +695,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Avoids requesting the notification permission on page load" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Failing Elements" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Preventing password pasting undermines good security policy. [Learn more](https://web.dev/password-inputs-can-be-pasted-into/)." }, @@ -1205,9 +1202,6 @@ "lighthouse-core/audits/service-worker.js | title": { "message": "Registers a service worker that controls page and `start_url`" }, - "lighthouse-core/audits/sized-images.js | columnFailingElem": { - "message": "Failing Elements" - }, "lighthouse-core/audits/sized-images.js | description": { "message": "Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)" }, @@ -1502,6 +1496,9 @@ "lighthouse-core/lib/i18n/i18n.js | columnElement": { "message": "Element" }, + "lighthouse-core/lib/i18n/i18n.js | columnFailingElem": { + "message": "Failing Elements" + }, "lighthouse-core/lib/i18n/i18n.js | columnLocation": { "message": "Location" }, diff --git a/lighthouse-core/lib/i18n/locales/en-XA.json b/lighthouse-core/lib/i18n/locales/en-XA.json index be01b25f67d4..f2364c91cc9e 100644 --- a/lighthouse-core/lib/i18n/locales/en-XA.json +++ b/lighthouse-core/lib/i18n/locales/en-XA.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "[Åvöîðš ŕéqûéšţîñĝ ţĥé ñöţîƒîçåţîöñ þéŕmîššîöñ öñ þåĝé ļöåð one two three four five six seven eight nine ten eleven twelve]" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "[Fåîļîñĝ Éļéméñţš one two]" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "[Þŕévéñţîñĝ þåššŵöŕð þåšţîñĝ ûñðéŕmîñéš ĝööð šéçûŕîţý þöļîçý. ᐅ[ᐊĻéåŕñ möŕéᐅ](https://web.dev/password-inputs-can-be-pasted-into/)ᐊ. one two three four five six seven eight nine ten eleven twelve thirteen fourteen]" }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index ac6fadb54cb0..29437c5748ea 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -695,9 +695,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Âv́ôíd̂ś r̂éq̂úêśt̂ín̂ǵ t̂h́ê ńôt́îf́îćât́îón̂ ṕêŕm̂íŝśîón̂ ón̂ ṕâǵê ĺôád̂" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "F̂áîĺîńĝ Él̂ém̂én̂t́ŝ" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "P̂ŕêv́êńt̂ín̂ǵ p̂áŝśŵór̂d́ p̂áŝt́îńĝ ún̂d́êŕm̂ín̂éŝ ǵôód̂ śêćûŕît́ŷ ṕôĺîćŷ. [Ĺêár̂ń m̂ór̂é](https://web.dev/password-inputs-can-be-pasted-into/)." }, @@ -1205,9 +1202,6 @@ "lighthouse-core/audits/service-worker.js | title": { "message": "R̂éĝíŝt́êŕŝ á ŝér̂v́îćê ẃôŕk̂ér̂ t́ĥát̂ ćôńt̂ŕôĺŝ ṕâǵê án̂d́ `start_url`" }, - "lighthouse-core/audits/sized-images.js | columnFailingElem": { - "message": "F̂áîĺîńĝ Él̂ém̂én̂t́ŝ" - }, "lighthouse-core/audits/sized-images.js | description": { "message": "Âĺŵáŷś îńĉĺûd́ê ẃîd́t̂h́ âńd̂ h́êíĝh́t̂ át̂t́r̂íb̂út̂éŝ ón̂ ýôúr̂ ím̂áĝé êĺêḿêńt̂ś t̂ó r̂éd̂úĉé l̂áŷóût́ ŝh́îf́t̂ín̂ǵ âńd̂ ím̂ṕr̂óv̂é ĈĹŜ. [Ĺêár̂ń m̂ór̂é](https://web.dev/optimize-cls/#images-without-dimensions)" }, @@ -1502,6 +1496,9 @@ "lighthouse-core/lib/i18n/i18n.js | columnElement": { "message": "Êĺêḿêńt̂" }, + "lighthouse-core/lib/i18n/i18n.js | columnFailingElem": { + "message": "F̂áîĺîńĝ Él̂ém̂én̂t́ŝ" + }, "lighthouse-core/lib/i18n/i18n.js | columnLocation": { "message": "L̂óĉát̂íôń" }, diff --git a/lighthouse-core/lib/i18n/locales/es-419.json b/lighthouse-core/lib/i18n/locales/es-419.json index 8289988a4539..3b9692ded5bd 100644 --- a/lighthouse-core/lib/i18n/locales/es-419.json +++ b/lighthouse-core/lib/i18n/locales/es-419.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evita solicitar el permiso de notificaciones al cargar la página" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementos con errores" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Evitar el pegado de contraseñas debilita las buenas políticas de seguridad. [Obtén más información](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/es.json b/lighthouse-core/lib/i18n/locales/es.json index 2461f8dbe69f..c0e67c7f93a1 100644 --- a/lighthouse-core/lib/i18n/locales/es.json +++ b/lighthouse-core/lib/i18n/locales/es.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evita solicitar el permiso de notificación al cargar la página" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementos con errores" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Evitar que se pueda pegar texto en el campo de contraseña debilita una buena política de seguridad. [Más información](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/fi.json b/lighthouse-core/lib/i18n/locales/fi.json index 04179dd97419..1b35ea735676 100644 --- a/lighthouse-core/lib/i18n/locales/fi.json +++ b/lighthouse-core/lib/i18n/locales/fi.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Välttää ilmoitusten käyttöoikeuden pyytämistä sivun latauksessa" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Hylätyt elementit" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Salasanan liittämisen estäminen on hyvän tietoturvakäytännön vastaista. [Lue lisää](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/fil.json b/lighthouse-core/lib/i18n/locales/fil.json index ffee34f392d5..cf1b1304fd4a 100644 --- a/lighthouse-core/lib/i18n/locales/fil.json +++ b/lighthouse-core/lib/i18n/locales/fil.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Iniiwasan ang paghiling ng pahintulot sa notification sa pag-load ng page" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Mga Hindi Nakapasang Element" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Pinapahina ng paghadlang sa pag-paste ng password ang magandang patakarang panseguridad. [Matuto pa](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/fr.json b/lighthouse-core/lib/i18n/locales/fr.json index 88abf97dbccc..b794e8ea2677 100644 --- a/lighthouse-core/lib/i18n/locales/fr.json +++ b/lighthouse-core/lib/i18n/locales/fr.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Aucune autorisation d'envoi de notifications n'est demandée au chargement de la page" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Éléments non conformes" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Empêcher la copie de contenu dans les champs de mot de passe nuit aux règles de sécurité. [En savoir plus](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/he.json b/lighthouse-core/lib/i18n/locales/he.json index 7e8616d9510f..6ab371f64835 100644 --- a/lighthouse-core/lib/i18n/locales/he.json +++ b/lighthouse-core/lib/i18n/locales/he.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "הדף לא מבקש הרשאה להתראות במהלך טעינת הדף" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "רכיבים שנכשלו בבדיקה" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "מניעה של הדבקת סיסמאות פוגעת ביכולת לקיים מדיניות אבטחה טובה. [מידע נוסף](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/hi.json b/lighthouse-core/lib/i18n/locales/hi.json index bc8c575c9e5e..efde293cfc76 100644 --- a/lighthouse-core/lib/i18n/locales/hi.json +++ b/lighthouse-core/lib/i18n/locales/hi.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "पेज लोड पर सूचना भेजने की मंज़ूरी का अनुरोध नहीं किया जाता" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "फ़ेल होने वाले एलिमेंट" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "पासवर्ड वाले फ़ील्ड में कुछ कॉपी करके चिपकाने की सुविधा न लागू करने का मतलब है एक अच्छी सुरक्षा नीति को कमज़ोर समझना. [ज़्यादा जानें](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/hr.json b/lighthouse-core/lib/i18n/locales/hr.json index 5aadaeed956a..677c8baf7356 100644 --- a/lighthouse-core/lib/i18n/locales/hr.json +++ b/lighthouse-core/lib/i18n/locales/hr.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Izbjegava traženje dopuštenja za obavještavanje pri učitavanju stranice" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementi koji nisu prošli provjeru" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Sprječavanje lijepljenja zaporke narušava kvalitetu dobrih sigurnosnih pravila. [Saznajte više](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/hu.json b/lighthouse-core/lib/i18n/locales/hu.json index b0d5207b45ed..14c1c19cdae3 100644 --- a/lighthouse-core/lib/i18n/locales/hu.json +++ b/lighthouse-core/lib/i18n/locales/hu.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Kerüli az értesítésekre vonatkozó engedély kérését oldalbetöltéskor" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Hibás elemek" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "A jelszóbeillesztés megakadályozása rossz biztonsági gyakorlat. [További információ](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/id.json b/lighthouse-core/lib/i18n/locales/id.json index 450f9d2d7b6a..cc1c4d609c72 100644 --- a/lighthouse-core/lib/i18n/locales/id.json +++ b/lighthouse-core/lib/i18n/locales/id.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Menghindari meminta izin notifikasi pada pemuatan halaman" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elemen yang Gagal" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Mencegah menempelkan sandi akan merusak kebijakan keamanan yang baik. [Pelajari lebih lanjut](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/it.json b/lighthouse-core/lib/i18n/locales/it.json index 846ae9cf32e8..dc175ef36e59 100644 --- a/lighthouse-core/lib/i18n/locales/it.json +++ b/lighthouse-core/lib/i18n/locales/it.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evita di chiedere l'autorizzazione di accesso alle notifiche durante il caricamento della pagina" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementi che non consentono di incollare" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Impedire di incollare le password pregiudica l'efficacia delle norme di sicurezza. [Ulteriori informazioni](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/ja.json b/lighthouse-core/lib/i18n/locales/ja.json index d0c2e06aed01..a1fcb8223f8e 100644 --- a/lighthouse-core/lib/i18n/locales/ja.json +++ b/lighthouse-core/lib/i18n/locales/ja.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "ページの読み込み時に通知の許可はリクエストされません" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "問題のある要素" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "パスワードの貼り付けを禁止すると、良好なセキュリティ ポリシーが損なわれます。[詳細](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/ko.json b/lighthouse-core/lib/i18n/locales/ko.json index 24013b4bfe0f..2f224c8323f2 100644 --- a/lighthouse-core/lib/i18n/locales/ko.json +++ b/lighthouse-core/lib/i18n/locales/ko.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "페이지 로드 시 알림 권한 요청 방지하기" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "통과하지 못한 요소" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "비밀번호 붙여넣기 방지로 인해 타당한 보안 정책이 저해됩니다. [자세히 알아보기](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/lt.json b/lighthouse-core/lib/i18n/locales/lt.json index ac1106c5838a..bcb969f26b36 100644 --- a/lighthouse-core/lib/i18n/locales/lt.json +++ b/lighthouse-core/lib/i18n/locales/lt.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Įkeliant puslapį vengiama pateikti užklausą dėl pranešimų leidimo" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Netinkami elementai" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Neleidžiant įklijuoti slaptažodžių, pažeidžiama tinkamos saugos politika. [Sužinokite daugiau](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/lv.json b/lighthouse-core/lib/i18n/locales/lv.json index e4abe0bbedd2..2b480a796d0e 100644 --- a/lighthouse-core/lib/i18n/locales/lv.json +++ b/lighthouse-core/lib/i18n/locales/lv.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Netiek pieprasīta paziņojumu atļauja lapas ielādei" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Nederīgi elementi" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Paroles ielīmēšanas novēršana neatbilst labai drošības politikai. [Uzziniet vairāk](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/nl.json b/lighthouse-core/lib/i18n/locales/nl.json index ef799b674d81..6d5af5ad586a 100644 --- a/lighthouse-core/lib/i18n/locales/nl.json +++ b/lighthouse-core/lib/i18n/locales/nl.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Vermijdt verzoeken om de meldingsrechten bij laden van pagina" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Mislukte elementen" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Verhindering van het plakken van wachtwoorden ondermijnt een goed beveiligingsbeleid. [Meer informatie](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/no.json b/lighthouse-core/lib/i18n/locales/no.json index e4152664e6c3..19d2647c1aee 100644 --- a/lighthouse-core/lib/i18n/locales/no.json +++ b/lighthouse-core/lib/i18n/locales/no.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Unngår å spørre om varseltillatelsen ved sideinnlasting" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementer som ikke besto kontrollen" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Å hindre brukere i å lime inn passord underminerer gode retningslinjer for sikkerhet. [Finn ut mer](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/pl.json b/lighthouse-core/lib/i18n/locales/pl.json index 0ff0ce1c1bf3..388a3a7dbaea 100644 --- a/lighthouse-core/lib/i18n/locales/pl.json +++ b/lighthouse-core/lib/i18n/locales/pl.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Nie pyta o zgodę na wyświetlanie powiadomień podczas wczytywania strony" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Nieprawidłowe elementy" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Uniemożliwianie wklejania haseł jest sprzeczne z dobrymi zasadami bezpieczeństwa. [Więcej informacji](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/pt-PT.json b/lighthouse-core/lib/i18n/locales/pt-PT.json index 73bb73666c32..4b8531c1fa9a 100644 --- a/lighthouse-core/lib/i18n/locales/pt-PT.json +++ b/lighthouse-core/lib/i18n/locales/pt-PT.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evita a solicitação da autorização de notificações no carregamento da página" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementos reprovados" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Impedir a colagem de palavras-passe compromete o cumprimento de uma política de segurança adequada. [Saiba mais](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/pt.json b/lighthouse-core/lib/i18n/locales/pt.json index 44ef00d9eca8..85ad62f621f4 100644 --- a/lighthouse-core/lib/i18n/locales/pt.json +++ b/lighthouse-core/lib/i18n/locales/pt.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evita o pedido da permissão de notificação no carregamento de página" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementos com falha" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Impedir a colagem da senha prejudica a política de boa segurança. [Saiba mais](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/ro.json b/lighthouse-core/lib/i18n/locales/ro.json index bd834e3b6936..eee0a0a57000 100644 --- a/lighthouse-core/lib/i18n/locales/ro.json +++ b/lighthouse-core/lib/i18n/locales/ro.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Evită solicitarea permisiunii de notificare la încărcarea paginii" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elemente cu probleme" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Împiedicarea inserării parolelor subminează buna politică de securitate. [Află mai multe](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/ru.json b/lighthouse-core/lib/i18n/locales/ru.json index 1eff964dfa31..308c77f40acd 100644 --- a/lighthouse-core/lib/i18n/locales/ru.json +++ b/lighthouse-core/lib/i18n/locales/ru.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Разрешение на отправку уведомлений не запрашивается при загрузке страницы" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Элементы, запрещающие вставку из буфера обмена" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Запрет на вставку пароля из буфера обмена отрицательно сказывается на безопасности пользователей. [Подробнее…](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/sk.json b/lighthouse-core/lib/i18n/locales/sk.json index 549fe445a536..22521b615185 100644 --- a/lighthouse-core/lib/i18n/locales/sk.json +++ b/lighthouse-core/lib/i18n/locales/sk.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Nepožaduje povolenie na upozornenie pri načítaní stránky" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Prvky s chybami" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Ak zakážete prilepovanie hesiel, zhoršíte kvalitu zabezpečenia. [Ďalšie informácie](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/sl.json b/lighthouse-core/lib/i18n/locales/sl.json index 792e79e27ff8..c9b84355a602 100644 --- a/lighthouse-core/lib/i18n/locales/sl.json +++ b/lighthouse-core/lib/i18n/locales/sl.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Ne zahteva dovoljenja za obvestila pri nalaganju strani" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Neuspešni elementi" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Preprečevanje lepljenja gesel omeji dober pravilnik o varnosti. [Več o tem](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/sr-Latn.json b/lighthouse-core/lib/i18n/locales/sr-Latn.json index 1785683d494d..dcbbb34e936e 100644 --- a/lighthouse-core/lib/i18n/locales/sr-Latn.json +++ b/lighthouse-core/lib/i18n/locales/sr-Latn.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Izbegavajte traženje dozvole za obaveštenja pri učitavanju stranice" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Elementi koji nisu prošli proveru" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Sprečavanje lepljenja lozinke narušava dobre smernice za bezbednost. [Saznajte više](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/sr.json b/lighthouse-core/lib/i18n/locales/sr.json index c8f5f99ad3f0..6b80288ad3a7 100644 --- a/lighthouse-core/lib/i18n/locales/sr.json +++ b/lighthouse-core/lib/i18n/locales/sr.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Избегавајте тражење дозволе за обавештења при учитавању странице" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Елементи који нису прошли проверу" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Спречавање лепљења лозинке нарушава добре смернице за безбедност. [Сазнајте више](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/sv.json b/lighthouse-core/lib/i18n/locales/sv.json index 9c47664ef319..dba291938ce5 100644 --- a/lighthouse-core/lib/i18n/locales/sv.json +++ b/lighthouse-core/lib/i18n/locales/sv.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Undviker att begära aviseringsbehörighet vid sidinläsning" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Element med fel" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Att förhindra att lösenord klistras in underminerar en bra säkerhetspolicy. [Läs mer](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/ta.json b/lighthouse-core/lib/i18n/locales/ta.json index c8d9229ac2a5..53c7f02f1f64 100644 --- a/lighthouse-core/lib/i18n/locales/ta.json +++ b/lighthouse-core/lib/i18n/locales/ta.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "பக்கம் ஏற்றப்படும்போது அதுகுறித்த அறிவிப்பைத் தெரிந்துகொள்வதற்கான அனுமதியைக் கோராது" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "தோல்வியுறும் உறுப்புகள்" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "கடவுச்சொல்லை ஒட்டுவதைத் தடுப்பதால் நல்ல பாதுகாப்புக் கொள்கை பாதிக்கப்படுகிறது. [மேலும் அறிக](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/te.json b/lighthouse-core/lib/i18n/locales/te.json index 03cd7c227ccb..58f5b140613e 100644 --- a/lighthouse-core/lib/i18n/locales/te.json +++ b/lighthouse-core/lib/i18n/locales/te.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "పేజీ లోడ్ సమయంలో నోటిఫికేషన్ అనుమతిని అభ్యర్థించడం నివారిస్తుంది" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "విఫలం అవుతున్న మూలకాలు" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "పాస్‌వర్డ్‌లో అతికించే చర్యను నిరోధించడం వలన మంచి భద్రతా విధానానికి ఆటంకం ఏర్పడుతుంది. [మరింత తెలుసుకోండి](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/th.json b/lighthouse-core/lib/i18n/locales/th.json index e6891366121e..94f3c5be5b19 100644 --- a/lighthouse-core/lib/i18n/locales/th.json +++ b/lighthouse-core/lib/i18n/locales/th.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "หลีกเลี่ยงการขอสิทธิ์การแจ้งเตือนในการโหลดหน้าเว็บ" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "องค์ประกอบที่ไม่ผ่านการตรวจสอบ" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "การป้องกันการวางรหัสผ่านทำให้นโยบายความปลอดภัยที่ดีอ่อนแอลง [ดูข้อมูลเพิ่มเติม](https://web.dev/password-inputs-can-be-pasted-into/)" }, diff --git a/lighthouse-core/lib/i18n/locales/tr.json b/lighthouse-core/lib/i18n/locales/tr.json index 579013747940..515f15ee5e15 100644 --- a/lighthouse-core/lib/i18n/locales/tr.json +++ b/lighthouse-core/lib/i18n/locales/tr.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Sayfa yüklemede bildirim izni istemiyor" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Başarısız Öğeler" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Şifre yapıştırmanın engellenmesi, iyi güvenlik politikasına zarar verir. [Daha fazla bilgi](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/uk.json b/lighthouse-core/lib/i18n/locales/uk.json index 82c839ee609b..f7dfc73b872c 100644 --- a/lighthouse-core/lib/i18n/locales/uk.json +++ b/lighthouse-core/lib/i18n/locales/uk.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Уникає надсилання запитів на показ сповіщень під час завантаження сторінки" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Відхилені елементи" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Якщо заборонити вставляти пароль, це порушить правила щодо високого рівня безпеки. [Докладніше](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/vi.json b/lighthouse-core/lib/i18n/locales/vi.json index 6ab5a1a016c4..72961627a707 100644 --- a/lighthouse-core/lib/i18n/locales/vi.json +++ b/lighthouse-core/lib/i18n/locales/vi.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "Tránh yêu cầu quyền truy cập thông báo khi tải trang" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "Các phần tử không đạt" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "Việc ngăn dán mật khẩu sẽ làm giảm tác dụng của chính sách bảo mật hiệu quả. [Tìm hiểu thêm](https://web.dev/password-inputs-can-be-pasted-into/)." }, diff --git a/lighthouse-core/lib/i18n/locales/zh-HK.json b/lighthouse-core/lib/i18n/locales/zh-HK.json index 1cdf6b6caab8..27602dc3d1d3 100644 --- a/lighthouse-core/lib/i18n/locales/zh-HK.json +++ b/lighthouse-core/lib/i18n/locales/zh-HK.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "避免在載入網頁時要求使用者允許網站顯示通知" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "審核失敗的元素" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "禁止貼上密碼會對安全政策造成不良影響。[瞭解詳情](https://web.dev/password-inputs-can-be-pasted-into/)。" }, diff --git a/lighthouse-core/lib/i18n/locales/zh-TW.json b/lighthouse-core/lib/i18n/locales/zh-TW.json index 8c076befa770..f1a00abd6333 100644 --- a/lighthouse-core/lib/i18n/locales/zh-TW.json +++ b/lighthouse-core/lib/i18n/locales/zh-TW.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "避免在載入網頁時要求使用者允許網站顯示通知" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "未通過稽核的元素" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "禁止貼上密碼會對安全性政策造成不良影響[瞭解詳情](https://web.dev/password-inputs-can-be-pasted-into/)。" }, diff --git a/lighthouse-core/lib/i18n/locales/zh.json b/lighthouse-core/lib/i18n/locales/zh.json index f75753c88874..538ae3444d78 100644 --- a/lighthouse-core/lib/i18n/locales/zh.json +++ b/lighthouse-core/lib/i18n/locales/zh.json @@ -689,9 +689,6 @@ "lighthouse-core/audits/dobetterweb/notification-on-start.js | title": { "message": "避免在网页加载时请求通知权限" }, - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": { - "message": "失败的元素" - }, "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": { "message": "阻止密码粘贴,会破坏良好的安全政策。[了解详情](https://web.dev/password-inputs-can-be-pasted-into/)。" }, diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js index 56f05c61975f..0e07b85e8757 100644 --- a/lighthouse-core/scripts/i18n/collect-strings.js +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -570,7 +570,7 @@ if (require.main === module) { if ((collisions) > 0) { console.log(`MEANING COLLISION: ${collisions} string(s) have the same content.`); - assert.equal(collisions, 17, `The number of duplicate strings have changed, update this assertion if that is expected, or reword strings. Collisions: ${collisionStrings}`); + assert.equal(collisions, 16, `The number of duplicate strings have changed, update this assertion if that is expected, or reword strings. Collisions: ${collisionStrings}`); } const strings = {...coreStrings, ...stackPackStrings}; diff --git a/lighthouse-core/test/audits/sized-images-test.js b/lighthouse-core/test/audits/sized-images-test.js new file mode 100644 index 000000000000..46e63496ff62 --- /dev/null +++ b/lighthouse-core/test/audits/sized-images-test.js @@ -0,0 +1,166 @@ +/** + * @license Copyright 2020 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const SizedImagesAudit = require('../../audits/sized-images.js'); + +/* eslint-env jest */ + +function generateImage(props, src = 'https://google.com/logo.png', isCss = false, + path = '1,HTML,1,BODY,1,IMG', selector = 'body > img', nodeLabel = 'img', + snippet = '') { + const image = {src, isCss, path, selector, nodeLabel, snippet}; + Object.assign(image, props); + return image; +} + +describe('Sized images audit', () => { + function testImage(condition, data) { + const description = `handles when an image ${condition}`; + it(description, async () => { + const result = SizedImagesAudit.audit({ + ImageElements: [ + generateImage( + data.props + ), + ], + }); + expect(result.score).toEqual(data.score); + }); + } + + testImage('is a css image', { + score: 1, + props: { + isCss: true, + attributeWidth: '', + attributeHeight: '', + }, + }); + + describe('has empty', () => { + testImage('has empty width attribute', { + score: 0, + props: { + attributeWidth: '', + attributeHeight: '100', + }, + }); + + testImage('has empty height attribute', { + score: 0, + props: { + attributeWidth: '100', + attributeHeight: '', + }, + }); + + testImage('has empty width and height attributes', { + score: 0, + props: { + attributeWidth: '', + attributeHeight: '', + }, + }); + }); + + describe('has invalid', () => { + testImage('has invalid width attribute', { + score: 0, + props: { + attributeWidth: '-200', + attributeHeight: '100', + }, + }); + + testImage('has invalid height attribute', { + score: 0, + props: { + attributeWidth: '100', + attributeHeight: '300.5', + }, + }); + + testImage('has invalid width and height attributes', { + score: 0, + props: { + attributeWidth: '0', + attributeHeight: '100/2', + }, + }); + }); + + testImage('has valid width and height attributes', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '100', + }, + }); + + it('is not applicable when there are no images', async () => { + const result = SizedImagesAudit.audit({ + ImageElements: [], + }); + expect(result.notApplicable).toEqual(true); + expect(result.score).toEqual(1); + }); + + it('can return multiple unsized images', async () => { + const result = SizedImagesAudit.audit({ + ImageElements: [ + generateImage( + { + attributeWidth: '100', + attributeHeight: '150', + }, + 'image1.png' + ), + generateImage( + { + attributeWidth: '', + attributeHeight: '', + }, + 'image2.png' + ), + generateImage( + { + attributeWidth: '200', + attributeHeight: '75', + }, + 'image3.png' + ), + ], + }); + expect(result.score).toEqual(0); + expect(result.details.items).toHaveLength(2); + const srcs = result.details.items.map(item => item.url); + expect(srcs).toEqual(['image1.png', 'image3.png']); + }); +}); + +describe('Size attribute validity check', () => { + it('fails on non-numeric characters', async () => { + expect(SizedImagesAudit.isValid('zero')).toEqual(false); + expect(SizedImagesAudit.isValid('1002$')).toEqual(false); + expect(SizedImagesAudit.isValid('s-5')).toEqual(false); + expect(SizedImagesAudit.isValid('3,000')).toEqual(false); + expect(SizedImagesAudit.isValid('100.0')).toEqual(false); + expect(SizedImagesAudit.isValid('2/3')).toEqual(false); + expect(SizedImagesAudit.isValid('-2020')).toEqual(false); + expect(SizedImagesAudit.isValid('+2020')).toEqual(false); + }); + + it('fails on zero input', async () => { + expect(SizedImagesAudit.isValid('0')).toEqual(false); + }); + + it('passes on non-zero non-negative integer input', async () => { + expect(SizedImagesAudit.isValid('1')).toEqual(true); + expect(SizedImagesAudit.isValid('250')).toEqual(true); + expect(SizedImagesAudit.isValid('4000000')).toEqual(true); + }); +}); diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index ecae0e5d66a2..d7535e56b1f8 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -399,9 +399,9 @@ declare global { naturalWidth: number; /** The natural height of the underlying image, uses img.naturalHeight. See https://codepen.io/patrickhulce/pen/PXvQbM for examples. */ naturalHeight: number; - /** The attribute width of the image, ... */ + /** The raw width attribute of the image element. CSS images will be set to the empty string. */ attributeWidth: string; - /** The attribute height of the image, ... */ + /** The raw height attribute of the image element. CSS images will be set to the empty string. */ attributeHeight: string; /** The BoundingClientRect of the element. */ clientRect: { From b5c3679a8d0976c0ed39ce91d0f3dc269cd8ba11 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 17 Jul 2020 11:34:27 -0700 Subject: [PATCH 22/40] fixed testing bugs, updated sample json --- .../test/audits/sized-images-test.js | 18 ++-- lighthouse-core/test/results/sample_v2.json | 90 ++++++++++++++++--- 2 files changed, 86 insertions(+), 22 deletions(-) diff --git a/lighthouse-core/test/audits/sized-images-test.js b/lighthouse-core/test/audits/sized-images-test.js index 46e63496ff62..d73b5ab7fe14 100644 --- a/lighthouse-core/test/audits/sized-images-test.js +++ b/lighthouse-core/test/audits/sized-images-test.js @@ -21,7 +21,7 @@ describe('Sized images audit', () => { function testImage(condition, data) { const description = `handles when an image ${condition}`; it(description, async () => { - const result = SizedImagesAudit.audit({ + const result = await SizedImagesAudit.audit({ ImageElements: [ generateImage( data.props @@ -102,7 +102,7 @@ describe('Sized images audit', () => { }); it('is not applicable when there are no images', async () => { - const result = SizedImagesAudit.audit({ + const result = await SizedImagesAudit.audit({ ImageElements: [], }); expect(result.notApplicable).toEqual(true); @@ -110,26 +110,26 @@ describe('Sized images audit', () => { }); it('can return multiple unsized images', async () => { - const result = SizedImagesAudit.audit({ + const result = await SizedImagesAudit.audit({ ImageElements: [ generateImage( { - attributeWidth: '100', - attributeHeight: '150', + attributeWidth: '', + attributeHeight: '', }, 'image1.png' ), generateImage( { - attributeWidth: '', - attributeHeight: '', + attributeWidth: '100', + attributeHeight: '150', }, 'image2.png' ), generateImage( { - attributeWidth: '200', - attributeHeight: '75', + attributeWidth: '', + attributeHeight: '', }, 'image3.png' ), diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index 9293ae14fcba..bdf32d527e9d 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -679,6 +679,53 @@ ] } }, + "sized-images": { + "id": "sized-images", + "title": "Image elements do not have `width` and `height` attributes", + "description": "Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)", + "score": 0, + "scoreDisplayMode": "binary", + "details": { + "type": "table", + "headings": [ + { + "key": "url", + "itemType": "thumbnail", + "text": "" + }, + { + "key": "url", + "itemType": "url", + "text": "URL" + }, + { + "key": "node", + "itemType": "node", + "text": "Failing Elements" + } + ], + "items": [ + { + "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg", + "node": { + "type": "node" + } + }, + { + "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg", + "node": { + "type": "node" + } + }, + { + "url": "blob:http://localhost:10200/ae0eac03-ab9b-4a6a-b299-f5212153e277", + "node": { + "type": "node" + } + } + ] + } + }, "deprecations": { "id": "deprecations", "title": "Uses deprecated APIs", @@ -4523,6 +4570,11 @@ "weight": 1, "group": "best-practices-ux" }, + { + "id": "sized-images", + "weight": 1, + "group": "best-practices-ux" + }, { "id": "image-aspect-ratio", "weight": 1, @@ -4565,7 +4617,7 @@ } ], "id": "best-practices", - "score": 0.08 + "score": 0.07 }, "seo": { "title": "SEO", @@ -5216,6 +5268,12 @@ "duration": 100, "entryType": "measure" }, + { + "startTime": 0, + "name": "lh:audit:sized-images", + "duration": 100, + "entryType": "measure" + }, { "startTime": 0, "name": "lh:audit:deprecations", @@ -6216,6 +6274,7 @@ "audits[errors-in-console].details.headings[0].text", "audits[image-aspect-ratio].details.headings[1].text", "audits[image-size-responsive].details.headings[1].text", + "audits[sized-images].details.headings[1].text", "audits.deprecations.details.headings[1].text", "audits[bootup-time].details.headings[0].text", "audits[network-rtt].details.headings[0].text", @@ -6354,6 +6413,22 @@ "lighthouse-core/audits/image-size-responsive.js | columnExpected": [ "audits[image-size-responsive].details.headings[4].text" ], + "lighthouse-core/audits/sized-images.js | failureTitle": [ + "audits[sized-images].title" + ], + "lighthouse-core/audits/sized-images.js | description": [ + "audits[sized-images].description" + ], + "lighthouse-core/lib/i18n/i18n.js | columnFailingElem": [ + "audits[sized-images].details.headings[2].text", + "audits[color-contrast].details.headings[0].text", + "audits[html-has-lang].details.headings[0].text", + "audits[image-alt].details.headings[0].text", + "audits.label.details.headings[0].text", + "audits[link-name].details.headings[0].text", + "audits[object-alt].details.headings[0].text", + "audits[password-inputs-can-be-pasted-into].details.headings[0].text" + ], "lighthouse-core/audits/deprecations.js | failureTitle": [ "audits.deprecations.title" ], @@ -6737,14 +6812,6 @@ "lighthouse-core/audits/accessibility/color-contrast.js | description": [ "audits[color-contrast].description" ], - "lighthouse-core/audits/accessibility/axe-audit.js | failingElementsHeader": [ - "audits[color-contrast].details.headings[0].text", - "audits[html-has-lang].details.headings[0].text", - "audits[image-alt].details.headings[0].text", - "audits.label.details.headings[0].text", - "audits[link-name].details.headings[0].text", - "audits[object-alt].details.headings[0].text" - ], "lighthouse-core/audits/accessibility/definition-list.js | title": [ "audits[definition-list].title" ], @@ -7194,9 +7261,6 @@ "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | description": [ "audits[password-inputs-can-be-pasted-into].description" ], - "lighthouse-core/audits/dobetterweb/password-inputs-can-be-pasted-into.js | columnFailingElem": [ - "audits[password-inputs-can-be-pasted-into].details.headings[0].text" - ], "lighthouse-core/audits/dobetterweb/uses-http2.js | failureTitle": [ "audits[uses-http2].title" ], @@ -7528,4 +7592,4 @@ } } ] -} +} \ No newline at end of file From 18abbc61a7c387071513e9e25a60a54a7a07ba3b Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 17 Jul 2020 14:00:48 -0700 Subject: [PATCH 23/40] test edits and removed from default config --- lighthouse-core/config/default-config.js | 2 - .../test/audits/sized-images-test.js | 12 ++- lighthouse-core/test/results/sample_v2.json | 86 +++---------------- 3 files changed, 15 insertions(+), 85 deletions(-) diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js index 1a8efacbba73..b2af8b14b8c0 100644 --- a/lighthouse-core/config/default-config.js +++ b/lighthouse-core/config/default-config.js @@ -216,7 +216,6 @@ const defaultConfig = { 'content-width', 'image-aspect-ratio', 'image-size-responsive', - 'sized-images', 'deprecations', 'mainthread-work-breakdown', 'bootup-time', @@ -551,7 +550,6 @@ const defaultConfig = { {id: 'no-vulnerable-libraries', weight: 1, group: 'best-practices-trust-safety'}, // User Experience {id: 'password-inputs-can-be-pasted-into', weight: 1, group: 'best-practices-ux'}, - {id: 'sized-images', weight: 1, group: 'best-practices-ux'}, {id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'}, {id: 'image-size-responsive', weight: 1, group: 'best-practices-ux'}, // Browser Compatibility diff --git a/lighthouse-core/test/audits/sized-images-test.js b/lighthouse-core/test/audits/sized-images-test.js index d73b5ab7fe14..311e621d4bfc 100644 --- a/lighthouse-core/test/audits/sized-images-test.js +++ b/lighthouse-core/test/audits/sized-images-test.js @@ -9,10 +9,8 @@ const SizedImagesAudit = require('../../audits/sized-images.js'); /* eslint-env jest */ -function generateImage(props, src = 'https://google.com/logo.png', isCss = false, - path = '1,HTML,1,BODY,1,IMG', selector = 'body > img', nodeLabel = 'img', - snippet = '') { - const image = {src, isCss, path, selector, nodeLabel, snippet}; +function generateImage(props, src = 'https://google.com/logo.png', isCss = false) { + const image = {src, isCss}; Object.assign(image, props); return image; } @@ -143,7 +141,7 @@ describe('Sized images audit', () => { }); describe('Size attribute validity check', () => { - it('fails on non-numeric characters', async () => { + it('fails on non-numeric characters', () => { expect(SizedImagesAudit.isValid('zero')).toEqual(false); expect(SizedImagesAudit.isValid('1002$')).toEqual(false); expect(SizedImagesAudit.isValid('s-5')).toEqual(false); @@ -154,11 +152,11 @@ describe('Size attribute validity check', () => { expect(SizedImagesAudit.isValid('+2020')).toEqual(false); }); - it('fails on zero input', async () => { + it('fails on zero input', () => { expect(SizedImagesAudit.isValid('0')).toEqual(false); }); - it('passes on non-zero non-negative integer input', async () => { + it('passes on non-zero non-negative integer input', () => { expect(SizedImagesAudit.isValid('1')).toEqual(true); expect(SizedImagesAudit.isValid('250')).toEqual(true); expect(SizedImagesAudit.isValid('4000000')).toEqual(true); diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index bdf32d527e9d..e7bb9bcdcef4 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -679,53 +679,6 @@ ] } }, - "sized-images": { - "id": "sized-images", - "title": "Image elements do not have `width` and `height` attributes", - "description": "Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)", - "score": 0, - "scoreDisplayMode": "binary", - "details": { - "type": "table", - "headings": [ - { - "key": "url", - "itemType": "thumbnail", - "text": "" - }, - { - "key": "url", - "itemType": "url", - "text": "URL" - }, - { - "key": "node", - "itemType": "node", - "text": "Failing Elements" - } - ], - "items": [ - { - "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg", - "node": { - "type": "node" - } - }, - { - "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg", - "node": { - "type": "node" - } - }, - { - "url": "blob:http://localhost:10200/ae0eac03-ab9b-4a6a-b299-f5212153e277", - "node": { - "type": "node" - } - } - ] - } - }, "deprecations": { "id": "deprecations", "title": "Uses deprecated APIs", @@ -4570,11 +4523,6 @@ "weight": 1, "group": "best-practices-ux" }, - { - "id": "sized-images", - "weight": 1, - "group": "best-practices-ux" - }, { "id": "image-aspect-ratio", "weight": 1, @@ -4617,7 +4565,7 @@ } ], "id": "best-practices", - "score": 0.07 + "score": 0.08 }, "seo": { "title": "SEO", @@ -5268,12 +5216,6 @@ "duration": 100, "entryType": "measure" }, - { - "startTime": 0, - "name": "lh:audit:sized-images", - "duration": 100, - "entryType": "measure" - }, { "startTime": 0, "name": "lh:audit:deprecations", @@ -6274,7 +6216,6 @@ "audits[errors-in-console].details.headings[0].text", "audits[image-aspect-ratio].details.headings[1].text", "audits[image-size-responsive].details.headings[1].text", - "audits[sized-images].details.headings[1].text", "audits.deprecations.details.headings[1].text", "audits[bootup-time].details.headings[0].text", "audits[network-rtt].details.headings[0].text", @@ -6413,22 +6354,6 @@ "lighthouse-core/audits/image-size-responsive.js | columnExpected": [ "audits[image-size-responsive].details.headings[4].text" ], - "lighthouse-core/audits/sized-images.js | failureTitle": [ - "audits[sized-images].title" - ], - "lighthouse-core/audits/sized-images.js | description": [ - "audits[sized-images].description" - ], - "lighthouse-core/lib/i18n/i18n.js | columnFailingElem": [ - "audits[sized-images].details.headings[2].text", - "audits[color-contrast].details.headings[0].text", - "audits[html-has-lang].details.headings[0].text", - "audits[image-alt].details.headings[0].text", - "audits.label.details.headings[0].text", - "audits[link-name].details.headings[0].text", - "audits[object-alt].details.headings[0].text", - "audits[password-inputs-can-be-pasted-into].details.headings[0].text" - ], "lighthouse-core/audits/deprecations.js | failureTitle": [ "audits.deprecations.title" ], @@ -6812,6 +6737,15 @@ "lighthouse-core/audits/accessibility/color-contrast.js | description": [ "audits[color-contrast].description" ], + "lighthouse-core/lib/i18n/i18n.js | columnFailingElem": [ + "audits[color-contrast].details.headings[0].text", + "audits[html-has-lang].details.headings[0].text", + "audits[image-alt].details.headings[0].text", + "audits.label.details.headings[0].text", + "audits[link-name].details.headings[0].text", + "audits[object-alt].details.headings[0].text", + "audits[password-inputs-can-be-pasted-into].details.headings[0].text" + ], "lighthouse-core/audits/accessibility/definition-list.js | title": [ "audits[definition-list].title" ], From 9c16d43e3bebef9256c480069652a4dc65940944 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 17 Jul 2020 15:44:04 -0700 Subject: [PATCH 24/40] added to experimental config --- lighthouse-core/config/experimental-config.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lighthouse-core/config/experimental-config.js b/lighthouse-core/config/experimental-config.js index 23d8423eb424..a6d88d5a5ce7 100644 --- a/lighthouse-core/config/experimental-config.js +++ b/lighthouse-core/config/experimental-config.js @@ -13,9 +13,23 @@ /** @type {LH.Config.Json} */ const config = { extends: 'lighthouse:default', + passes: [{ + passName: 'defaultPass', + gatherers: [ + 'image-elements', + ], + }], audits: [ + 'sized-images', ], + // @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default + // config is awkward - easier to omit the property here. Will defer to default config. categories: { + 'best-practices': { + auditRefs: [ + {id: 'sized-images', weight: 1, group: 'best-practices-ux'}, + ], + }, }, }; From 1498e2d77a502c8d3e34b3108e451a63961bd0c6 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 17 Jul 2020 15:55:02 -0700 Subject: [PATCH 25/40] fixed ts error and edited exp config --- lighthouse-core/config/experimental-config.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lighthouse-core/config/experimental-config.js b/lighthouse-core/config/experimental-config.js index a6d88d5a5ce7..77858c2e7a6c 100644 --- a/lighthouse-core/config/experimental-config.js +++ b/lighthouse-core/config/experimental-config.js @@ -13,18 +13,12 @@ /** @type {LH.Config.Json} */ const config = { extends: 'lighthouse:default', - passes: [{ - passName: 'defaultPass', - gatherers: [ - 'image-elements', - ], - }], audits: [ 'sized-images', ], + categories: { // @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default // config is awkward - easier to omit the property here. Will defer to default config. - categories: { 'best-practices': { auditRefs: [ {id: 'sized-images', weight: 1, group: 'best-practices-ux'}, From bc64ee379cef882b01a6c4a778f0ca06b681aa5a Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 21 Jul 2020 18:02:53 -0700 Subject: [PATCH 26/40] added preliminary css sizing logic --- lighthouse-core/audits/sized-images.js | 31 +++++- .../gather/gatherers/image-elements.js | 102 +++++++++++++++++- types/artifacts.d.ts | 4 + 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js index b8bba43c7527..7ec31d96cb28 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/sized-images.js @@ -51,6 +51,29 @@ class SizedImages extends Audit { return NON_NEGATIVE_INT_REGEX.test(attr) && !ZERO_REGEX.test(attr); } + /** + * @param {string} attrWidth + * @param {string} attrHeight + * @param {string} cssWidth + * @param {string} cssHeight + * @return {boolean} + */ + static unsizedImage(attrWidth, attrHeight, cssWidth, cssHeight) { + if (attrWidth && attrHeight) { + return !SizedImages.isValid(attrWidth) || !SizedImages.isValid(attrHeight); + } + if (attrWidth && cssHeight) { + return !SizedImages.isValid(attrWidth) || cssHeight === 'auto'; + } + if (cssWidth && attrHeight) { + return cssWidth === 'auto' || !SizedImages.isValid(attrHeight); + } + if (cssWidth && cssHeight) { + return cssWidth === 'auto' || cssHeight === 'auto'; + } + return true; + } + /** * @param {LH.Artifacts} artifacts * @return {Promise} @@ -61,10 +84,12 @@ class SizedImages extends Audit { const unsizedImages = []; for (const image of images) { - const width = image.attributeWidth; - const height = image.attributeHeight; + const attrWidth = image.attributeWidth; + const attrHeight = image.attributeHeight; + const cssWidth = image.propertyWidth; + const cssHeight = image.propertyHeight; // images are considered sized if they have defined & valid values - if (!width || !height || !SizedImages.isValid(width) || !SizedImages.isValid(height)) { + if (SizedImages.unsizedImage(attrWidth, attrHeight, cssWidth, cssHeight)) { const url = URL.elideDataURI(image.src); unsizedImages.push({ url, diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 337b48ab30f3..ad2e735eef7d 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -12,6 +12,7 @@ const Gatherer = require('./gatherer.js'); const pageFunctions = require('../../lib/page-functions.js'); const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars +const FontSize = require('./seo/font-size.js'); /* global window, getElementsInDocument, Image, getNodePath, getNodeSelector, getNodeLabel, getOuterHTMLSnippet */ @@ -53,6 +54,8 @@ function getHTMLImages(allElements) { naturalHeight: element.naturalHeight, attributeWidth: element.getAttribute('width') || '', attributeHeight: element.getAttribute('height') || '', + propertyWidth: '', + propertyHeight: '', isCss: false, // @ts-ignore: loading attribute not yet added to HTMLImageElement definition. loading: element.loading, @@ -111,6 +114,8 @@ function getCSSImages(allElements) { naturalHeight: 0, attributeWidth: '', attributeHeight: '', + propertyWidth: '', + propertyHeight: '', isCss: true, isPicture: false, usesObjectFit: false, @@ -162,6 +167,72 @@ function determineNaturalSize(url) { }); } +/** + * @param {LH.Crdp.CSS.CSSStyle} [style] + * @param {string} property + * @return {boolean} + */ +function hasSizeDeclaration(style, property) { + return !!style && !!style.cssProperties.find(({name}) => name === property); +} + +/** + * Finds the most specific directly matched CSS font-size rule from the list. + * + * @param {Array} [matchedCSSRules] + * @param {string} property + * @returns {string} + */ +function findMostSpecificMatchedCSSRule(matchedCSSRules = [], property) { + let maxSpecificity = -Infinity; + /** @type {LH.Crdp.CSS.CSSRule|undefined} */ + let maxSpecificityRule; + + for (const {rule, matchingSelectors} of matchedCSSRules) { + if (hasSizeDeclaration(rule.style, property)) { + const specificities = matchingSelectors.map(idx => + FontSize.computeSelectorSpecificity(rule.selectorList.selectors[idx].text) + ); + const specificity = Math.max(...specificities); + // Use greater OR EQUAL so that the last rule wins in the event of a tie + if (specificity >= maxSpecificity) { + maxSpecificity = specificity; + maxSpecificityRule = rule; + } + } + } + + if (maxSpecificityRule) { + // @ts-ignore the existence of the property object is checked in hasSizeDeclaration + return maxSpecificityRule.style.cssProperties.find(({name}) => name === property).value; + } + return ''; +} + +/** + * @param {LH.Crdp.CSS.GetMatchedStylesForNodeResponse} matched CSS rules} + * @param {string} property + * @returns {string} + */ +function getEffectiveSizingRule({attributesStyle, inlineStyle, matchedCSSRules}, property) { + // CSS sizing can't be inherited + // We only need to check inline & matched styles + // Inline styles have highest priority + if (hasSizeDeclaration(inlineStyle, property)) { + // @ts-ignore the existence of the property object is checked in hasSizeDeclaration + return inlineStyle.cssProperties.find(({name}) => name === property).value; + } + + if (hasSizeDeclaration(attributesStyle, property)) { + // @ts-ignore the existence of the property object is checked in hasSizeDeclaration + return attributesStyle.cssProperties.find(({name}) => name === property).value; + } + // Rules directly referencing the node come next + const matchedRule = findMostSpecificMatchedCSSRule(matchedCSSRules, property); + if (matchedRule) return matchedRule; + return ''; +} + class ImageElements extends Gatherer { constructor() { super(); @@ -193,6 +264,25 @@ class ImageElements extends Gatherer { } } + /** + * @param {Driver} driver + * @param {string} devtoolsNodePath + * @param {LH.Artifacts.ImageElement} element + */ + async fetchSourceRules(driver, devtoolsNodePath, element) { + const {nodeId} = await driver.sendCommand('DOM.pushNodeByPathToFrontend', { + path: devtoolsNodePath, + }); + if (!nodeId) return; + const matchedRules = await driver.sendCommand('CSS.getMatchedStylesForNode', { + nodeId: nodeId, + }); + const sourceWidth = getEffectiveSizingRule(matchedRules, 'width'); + const sourceHeight = getEffectiveSizingRule(matchedRules, 'height'); + const sourceRules = {propertyWidth: sourceWidth, propertyHeight: sourceHeight}; + Object.assign(element, sourceRules); + } + /** * @param {LH.Gatherer.PassContext} passContext * @param {LH.Gatherer.LoadData} loadData @@ -232,6 +322,11 @@ class ImageElements extends Gatherer { const top50Images = Object.values(indexedNetworkRecords) .sort((a, b) => b.resourceSize - a.resourceSize) .slice(0, 50); + await Promise.all([ + driver.sendCommand('DOM.enable'), + driver.sendCommand('CSS.enable'), + driver.sendCommand('DOM.getDocument', {depth: -1, pierce: true}), + ]); for (let element of elements) { // Pull some of our information directly off the network record. @@ -244,7 +339,7 @@ class ImageElements extends Gatherer { // Use the min of the two numbers to be safe. const {resourceSize = 0, transferSize = 0} = networkRecord; element.resourceSize = Math.min(resourceSize, transferSize); - + await this.fetchSourceRules(driver, element.devtoolsNodePath, element); // Images within `picture` behave strangely and natural size information isn't accurate, // CSS images have no natural size information at all. Try to get the actual size if we can. // Additional fetch is expensive; don't bother if we don't have a networkRecord for the image, @@ -260,6 +355,11 @@ class ImageElements extends Gatherer { imageUsage.push(element); } + await Promise.all([ + driver.sendCommand('DOM.disable'), + driver.sendCommand('CSS.disable'), + ]); + return imageUsage; } } diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index d7535e56b1f8..ce13be2eb443 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -403,6 +403,10 @@ declare global { attributeWidth: string; /** The raw height attribute of the image element. CSS images will be set to the empty string. */ attributeHeight: string; + /** The CSS width property of the image element */ + propertyWidth: string; + /** The CSS height property of the image element */ + propertyHeight: string; /** The BoundingClientRect of the element. */ clientRect: { top: number; From b5bc5870092b3c74a7e98f9448b6653b0c57bafb Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 21 Jul 2020 18:14:19 -0700 Subject: [PATCH 27/40] added and shifted comments --- lighthouse-core/audits/sized-images.js | 2 +- lighthouse-core/gather/gatherers/image-elements.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/sized-images.js index 7ec31d96cb28..27d1a362bf5c 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/sized-images.js @@ -59,6 +59,7 @@ class SizedImages extends Audit { * @return {boolean} */ static unsizedImage(attrWidth, attrHeight, cssWidth, cssHeight) { + // images are considered sized if they have defined & valid values if (attrWidth && attrHeight) { return !SizedImages.isValid(attrWidth) || !SizedImages.isValid(attrHeight); } @@ -88,7 +89,6 @@ class SizedImages extends Audit { const attrHeight = image.attributeHeight; const cssWidth = image.propertyWidth; const cssHeight = image.propertyHeight; - // images are considered sized if they have defined & valid values if (SizedImages.unsizedImage(attrWidth, attrHeight, cssWidth, cssHeight)) { const url = URL.elideDataURI(image.src); unsizedImages.push({ diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index ad2e735eef7d..0f2825afda79 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -54,8 +54,8 @@ function getHTMLImages(allElements) { naturalHeight: element.naturalHeight, attributeWidth: element.getAttribute('width') || '', attributeHeight: element.getAttribute('height') || '', - propertyWidth: '', - propertyHeight: '', + propertyWidth: '', // this will get overwritten below + propertyHeight: '', // this will get overwritten below isCss: false, // @ts-ignore: loading attribute not yet added to HTMLImageElement definition. loading: element.loading, From ed14f5393031e018a19f0fc9a4f461126a3651e4 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 22 Jul 2020 16:39:37 -0700 Subject: [PATCH 28/40] edited UIstrings and filenames --- .../{sized-images.js => unsized-images.js} | 14 +++++++------- lighthouse-core/config/experimental-config.js | 4 ++-- lighthouse-core/lib/i18n/locales/en-US.json | 18 +++++++++--------- lighthouse-core/lib/i18n/locales/en-XL.json | 18 +++++++++--------- ...d-images-test.js => unsized-images-test.js} | 2 +- 5 files changed, 28 insertions(+), 28 deletions(-) rename lighthouse-core/audits/{sized-images.js => unsized-images.js} (82%) rename lighthouse-core/test/audits/{sized-images-test.js => unsized-images-test.js} (98%) diff --git a/lighthouse-core/audits/sized-images.js b/lighthouse-core/audits/unsized-images.js similarity index 82% rename from lighthouse-core/audits/sized-images.js rename to lighthouse-core/audits/unsized-images.js index 27d1a362bf5c..7371ccbc82c0 100644 --- a/lighthouse-core/audits/sized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -10,12 +10,12 @@ const i18n = require('./../lib/i18n/i18n.js'); const URL = require('./../lib/url-shim.js'); const UIStrings = { - /** Title of a Lighthouse audit that provides detail on whether all images had width and height attributes. This descriptive title is shown to users when every image has width and height attributes */ - title: 'Image elements have `width` and `height` attributes', - /** Title of a Lighthouse audit that provides detail on whether all images had width and height attributes. This descriptive title is shown to users when one or more images does not have width and height attributes */ - failureTitle: 'Image elements do not have `width` and `height` attributes', - /** Description of a Lighthouse audit that tells the user why they should include width and height attributes for all images. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ - description: 'Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', + /** Title of a Lighthouse audit that provides detail on whether all images have explicit width and height. This descriptive title is shown to users when every image has explicit width and height */ + title: 'Image elements have explicit `width` and `height`', + /** Title of a Lighthouse audit that provides detail on whether all images have explicit width and height. This descriptive title is shown to users when one or more images does not have explicit width and height */ + failureTitle: 'Image elements do not have explicit `width` and `height`', + /** Description of a Lighthouse audit that tells the user why they should include explicit width and height for all images. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ + description: 'Always include explicit width and height on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', }; const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); @@ -31,7 +31,7 @@ class SizedImages extends Audit { */ static get meta() { return { - id: 'sized-images', + id: 'unsized-images', title: str_(UIStrings.title), failureTitle: str_(UIStrings.failureTitle), description: str_(UIStrings.description), diff --git a/lighthouse-core/config/experimental-config.js b/lighthouse-core/config/experimental-config.js index ec9d7a3efd5e..3f9a4fb9b06b 100644 --- a/lighthouse-core/config/experimental-config.js +++ b/lighthouse-core/config/experimental-config.js @@ -14,7 +14,7 @@ const config = { extends: 'lighthouse:default', audits: [ - 'sized-images', + 'unsized-images', 'full-page-screenshot', ], passes: [{ @@ -28,7 +28,7 @@ const config = { // config is awkward - easier to omit the property here. Will defer to default config. 'best-practices': { auditRefs: [ - {id: 'sized-images', weight: 1, group: 'best-practices-ux'}, + {id: 'unsized-images', weight: 1, group: 'best-practices-ux'}, ], }, }, diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index 1c662aaceb70..853a7ebe4a9f 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1211,15 +1211,6 @@ "lighthouse-core/audits/service-worker.js | title": { "message": "Registers a service worker that controls page and `start_url`" }, - "lighthouse-core/audits/sized-images.js | description": { - "message": "Always include width and height attributes on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)" - }, - "lighthouse-core/audits/sized-images.js | failureTitle": { - "message": "Image elements do not have `width` and `height` attributes" - }, - "lighthouse-core/audits/sized-images.js | title": { - "message": "Image elements have `width` and `height` attributes" - }, "lighthouse-core/audits/splash-screen.js | description": { "message": "A themed splash screen ensures a high-quality experience when users launch your app from their homescreens. [Learn more](https://web.dev/splash-screen/)." }, @@ -1268,6 +1259,15 @@ "lighthouse-core/audits/timing-budget.js | title": { "message": "Timing budget" }, + "lighthouse-core/audits/unsized-images.js | description": { + "message": "Always include explicit width and height on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)" + }, + "lighthouse-core/audits/unsized-images.js | failureTitle": { + "message": "Image elements do not have explicit `width` and `height`" + }, + "lighthouse-core/audits/unsized-images.js | title": { + "message": "Image elements have explicit `width` and `height`" + }, "lighthouse-core/audits/user-timings.js | columnType": { "message": "Type" }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index edc7fc7d9021..3a1d6cc3387f 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1211,15 +1211,6 @@ "lighthouse-core/audits/service-worker.js | title": { "message": "R̂éĝíŝt́êŕŝ á ŝér̂v́îćê ẃôŕk̂ér̂ t́ĥát̂ ćôńt̂ŕôĺŝ ṕâǵê án̂d́ `start_url`" }, - "lighthouse-core/audits/sized-images.js | description": { - "message": "Âĺŵáŷś îńĉĺûd́ê ẃîd́t̂h́ âńd̂ h́êíĝh́t̂ át̂t́r̂íb̂út̂éŝ ón̂ ýôúr̂ ím̂áĝé êĺêḿêńt̂ś t̂ó r̂éd̂úĉé l̂áŷóût́ ŝh́îf́t̂ín̂ǵ âńd̂ ím̂ṕr̂óv̂é ĈĹŜ. [Ĺêár̂ń m̂ór̂é](https://web.dev/optimize-cls/#images-without-dimensions)" - }, - "lighthouse-core/audits/sized-images.js | failureTitle": { - "message": "Îḿâǵê él̂ém̂én̂t́ŝ d́ô ńôt́ ĥáv̂é `width` âńd̂ `height` át̂t́r̂íb̂út̂éŝ" - }, - "lighthouse-core/audits/sized-images.js | title": { - "message": "Îḿâǵê él̂ém̂én̂t́ŝ h́âv́ê `width` án̂d́ `height` ât́t̂ŕîb́ût́êś" - }, "lighthouse-core/audits/splash-screen.js | description": { "message": " t́ĥém̂éd̂ śp̂ĺâśĥ śĉŕêén̂ én̂śûŕêś â h́îǵĥ-q́ûál̂ít̂ý êx́p̂ér̂íêńĉé ŵh́êń ûśêŕŝ ĺâún̂ćĥ ýôúr̂ áp̂ṕ f̂ŕôḿ t̂h́êír̂ h́ôḿêśĉŕêén̂ś. [L̂éâŕn̂ ḿôŕê](https://web.dev/splash-screen/)." }, @@ -1268,6 +1259,15 @@ "lighthouse-core/audits/timing-budget.js | title": { "message": "T̂ím̂ín̂ǵ b̂úd̂ǵêt́" }, + "lighthouse-core/audits/unsized-images.js | description": { + "message": "Âĺŵáŷś îńĉĺûd́ê éx̂ṕl̂íĉít̂ ẃîd́t̂h́ âńd̂ h́êíĝh́t̂ ón̂ ýôúr̂ ím̂áĝé êĺêḿêńt̂ś t̂ó r̂éd̂úĉé l̂áŷóût́ ŝh́îf́t̂ín̂ǵ âńd̂ ím̂ṕr̂óv̂é ĈĹŜ. [Ĺêár̂ń m̂ór̂é](https://web.dev/optimize-cls/#images-without-dimensions)" + }, + "lighthouse-core/audits/unsized-images.js | failureTitle": { + "message": "Îḿâǵê él̂ém̂én̂t́ŝ d́ô ńôt́ ĥáv̂é êx́p̂ĺîćît́ `width` âńd̂ `height`" + }, + "lighthouse-core/audits/unsized-images.js | title": { + "message": "Îḿâǵê él̂ém̂én̂t́ŝ h́âv́ê éx̂ṕl̂íĉít̂ `width` án̂d́ `height`" + }, "lighthouse-core/audits/user-timings.js | columnType": { "message": "T̂ýp̂é" }, diff --git a/lighthouse-core/test/audits/sized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js similarity index 98% rename from lighthouse-core/test/audits/sized-images-test.js rename to lighthouse-core/test/audits/unsized-images-test.js index 311e621d4bfc..333e019871cb 100644 --- a/lighthouse-core/test/audits/sized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -5,7 +5,7 @@ */ 'use strict'; -const SizedImagesAudit = require('../../audits/sized-images.js'); +const SizedImagesAudit = require('../../audits/unsized-images.js'); /* eslint-env jest */ From e5a2c7c7e844091dab38c540d9d927a51523e856 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 22 Jul 2020 17:39:17 -0700 Subject: [PATCH 29/40] made css props optional in artifacts & refactored image-elements --- lighthouse-core/audits/unsized-images.js | 49 ++++++++++--------- .../gather/gatherers/image-elements.js | 32 ++++++------ types/artifacts.d.ts | 8 +-- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index 7371ccbc82c0..77f61d7465fe 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -43,7 +43,7 @@ class SizedImages extends Audit { * @param {string} attr * @return {boolean} */ - static isValid(attr) { + static isValidAttr(attr) { // an img size attribute is valid for preventing CLS // if it is a non-negative, non-zero integer const NON_NEGATIVE_INT_REGEX = /^\d+$/; @@ -52,27 +52,32 @@ class SizedImages extends Audit { } /** - * @param {string} attrWidth - * @param {string} attrHeight - * @param {string} cssWidth - * @param {string} cssHeight + * @param {string} property * @return {boolean} */ - static unsizedImage(attrWidth, attrHeight, cssWidth, cssHeight) { + static isValidCss(property) { + // an img css size property is valid for preventing CLS + // if it ... + return property !== 'auto'; + } + + /** + * @param {LH.Artifacts.ImageElement} image + * @return {boolean} + */ + static isUnsizedImage(image) { // images are considered sized if they have defined & valid values - if (attrWidth && attrHeight) { - return !SizedImages.isValid(attrWidth) || !SizedImages.isValid(attrHeight); - } - if (attrWidth && cssHeight) { - return !SizedImages.isValid(attrWidth) || cssHeight === 'auto'; - } - if (cssWidth && attrHeight) { - return cssWidth === 'auto' || !SizedImages.isValid(attrHeight); - } - if (cssWidth && cssHeight) { - return cssWidth === 'auto' || cssHeight === 'auto'; - } - return true; + const attrWidth = image.attributeWidth; + const attrHeight = image.attributeHeight; + const cssWidth = image.cssWidth; + const cssHeight = image.cssHeight; + const widthIsValidAttribute = attrWidth && SizedImages.isValidAttr(attrWidth); + const widthIsValidCss = cssWidth && SizedImages.isValidCss(cssWidth); + const heightIsValidAttribute = attrHeight && SizedImages.isValidAttr(attrHeight); + const heightIsValidCss = cssHeight && SizedImages.isValidCss(cssHeight); + const validWidth = widthIsValidAttribute || widthIsValidCss; + const validHeight = heightIsValidAttribute || heightIsValidCss; + return !validWidth || !validHeight; } /** @@ -85,11 +90,7 @@ class SizedImages extends Audit { const unsizedImages = []; for (const image of images) { - const attrWidth = image.attributeWidth; - const attrHeight = image.attributeHeight; - const cssWidth = image.propertyWidth; - const cssHeight = image.propertyHeight; - if (SizedImages.unsizedImage(attrWidth, attrHeight, cssWidth, cssHeight)) { + if (SizedImages.isUnsizedImage(image)) { const url = URL.elideDataURI(image.src); unsizedImages.push({ url, diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 0f2825afda79..7c5e14609073 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -54,8 +54,8 @@ function getHTMLImages(allElements) { naturalHeight: element.naturalHeight, attributeWidth: element.getAttribute('width') || '', attributeHeight: element.getAttribute('height') || '', - propertyWidth: '', // this will get overwritten below - propertyHeight: '', // this will get overwritten below + cssWidth: '', // this will get overwritten below + cssHeight: '', // this will get overwritten below isCss: false, // @ts-ignore: loading attribute not yet added to HTMLImageElement definition. loading: element.loading, @@ -114,8 +114,8 @@ function getCSSImages(allElements) { naturalHeight: 0, attributeWidth: '', attributeHeight: '', - propertyWidth: '', - propertyHeight: '', + cssWidth: '', + cssHeight: '', isCss: true, isPicture: false, usesObjectFit: false, @@ -170,10 +170,13 @@ function determineNaturalSize(url) { /** * @param {LH.Crdp.CSS.CSSStyle} [style] * @param {string} property - * @return {boolean} + * @return {string | undefined} */ -function hasSizeDeclaration(style, property) { - return !!style && !!style.cssProperties.find(({name}) => name === property); +function findSizeDeclaration(style, property) { + if (!style) return; + const definedProp = style.cssProperties.find(({name}) => name === property); + if (!definedProp) return; + return definedProp.value; } /** @@ -189,7 +192,8 @@ function findMostSpecificMatchedCSSRule(matchedCSSRules = [], property) { let maxSpecificityRule; for (const {rule, matchingSelectors} of matchedCSSRules) { - if (hasSizeDeclaration(rule.style, property)) { + // hasSizeDeclaration from font-size.js using `.some()` + if (!!rule.style && rule.style.cssProperties.some(({name}) => name === property)) { const specificities = matchingSelectors.map(idx => FontSize.computeSelectorSpecificity(rule.selectorList.selectors[idx].text) ); @@ -218,15 +222,11 @@ function getEffectiveSizingRule({attributesStyle, inlineStyle, matchedCSSRules}, // CSS sizing can't be inherited // We only need to check inline & matched styles // Inline styles have highest priority - if (hasSizeDeclaration(inlineStyle, property)) { - // @ts-ignore the existence of the property object is checked in hasSizeDeclaration - return inlineStyle.cssProperties.find(({name}) => name === property).value; - } + const inlineRule = findSizeDeclaration(inlineStyle, property); + if (inlineRule) return inlineRule; - if (hasSizeDeclaration(attributesStyle, property)) { - // @ts-ignore the existence of the property object is checked in hasSizeDeclaration - return attributesStyle.cssProperties.find(({name}) => name === property).value; - } + const attributeRule = findSizeDeclaration(attributesStyle, property); + if (attributeRule) return attributeRule; // Rules directly referencing the node come next const matchedRule = findMostSpecificMatchedCSSRule(matchedCSSRules, property); if (matchedRule) return matchedRule; diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 546c9f130a13..a33d870f6bc6 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -414,10 +414,10 @@ declare global { attributeWidth: string; /** The raw height attribute of the image element. CSS images will be set to the empty string. */ attributeHeight: string; - /** The CSS width property of the image element */ - propertyWidth: string; - /** The CSS height property of the image element */ - propertyHeight: string; + /** The CSS width property of the image element. */ + cssWidth?: string; + /** The CSS height property of the image element. */ + cssHeight?: string; /** The BoundingClientRect of the element. */ clientRect: { top: number; From 30648c03293f938ac46540afec9b1bc838c48bc3 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 22 Jul 2020 17:44:37 -0700 Subject: [PATCH 30/40] fixed unsized-images-test --- .../test/audits/unsized-images-test.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js index 333e019871cb..10cc0eb3971a 100644 --- a/lighthouse-core/test/audits/unsized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -5,7 +5,7 @@ */ 'use strict'; -const SizedImagesAudit = require('../../audits/unsized-images.js'); +const UnSizedImagesAudit = require('../../audits/unsized-images.js'); /* eslint-env jest */ @@ -19,7 +19,7 @@ describe('Sized images audit', () => { function testImage(condition, data) { const description = `handles when an image ${condition}`; it(description, async () => { - const result = await SizedImagesAudit.audit({ + const result = await UnSizedImagesAudit.audit({ ImageElements: [ generateImage( data.props @@ -100,7 +100,7 @@ describe('Sized images audit', () => { }); it('is not applicable when there are no images', async () => { - const result = await SizedImagesAudit.audit({ + const result = await UnSizedImagesAudit.audit({ ImageElements: [], }); expect(result.notApplicable).toEqual(true); @@ -108,7 +108,7 @@ describe('Sized images audit', () => { }); it('can return multiple unsized images', async () => { - const result = await SizedImagesAudit.audit({ + const result = await UnSizedImagesAudit.audit({ ImageElements: [ generateImage( { @@ -142,23 +142,23 @@ describe('Sized images audit', () => { describe('Size attribute validity check', () => { it('fails on non-numeric characters', () => { - expect(SizedImagesAudit.isValid('zero')).toEqual(false); - expect(SizedImagesAudit.isValid('1002$')).toEqual(false); - expect(SizedImagesAudit.isValid('s-5')).toEqual(false); - expect(SizedImagesAudit.isValid('3,000')).toEqual(false); - expect(SizedImagesAudit.isValid('100.0')).toEqual(false); - expect(SizedImagesAudit.isValid('2/3')).toEqual(false); - expect(SizedImagesAudit.isValid('-2020')).toEqual(false); - expect(SizedImagesAudit.isValid('+2020')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('zero')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('1002$')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('s-5')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('3,000')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('100.0')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('2/3')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('-2020')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('+2020')).toEqual(false); }); it('fails on zero input', () => { - expect(SizedImagesAudit.isValid('0')).toEqual(false); + expect(UnSizedImagesAudit.isValidAttr('0')).toEqual(false); }); it('passes on non-zero non-negative integer input', () => { - expect(SizedImagesAudit.isValid('1')).toEqual(true); - expect(SizedImagesAudit.isValid('250')).toEqual(true); - expect(SizedImagesAudit.isValid('4000000')).toEqual(true); + expect(UnSizedImagesAudit.isValidAttr('1')).toEqual(true); + expect(UnSizedImagesAudit.isValidAttr('250')).toEqual(true); + expect(UnSizedImagesAudit.isValidAttr('4000000')).toEqual(true); }); }); From b004ee3df29bba52101fbd668e46cc0575b9d393 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 22 Jul 2020 18:07:23 -0700 Subject: [PATCH 31/40] removed contents of image-elements file --- .../gather/gatherers/image-elements.js | 367 ------------------ 1 file changed, 367 deletions(-) diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 7c5e14609073..e69de29bb2d1 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -1,367 +0,0 @@ -/** - * @license Copyright 2017 The Lighthouse Authors. All Rights Reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - */ -/** - * @fileoverview Gathers all images used on the page with their src, size, - * and attribute information. Executes script in the context of the page. - */ -'use strict'; - -const Gatherer = require('./gatherer.js'); -const pageFunctions = require('../../lib/page-functions.js'); -const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars -const FontSize = require('./seo/font-size.js'); - -/* global window, getElementsInDocument, Image, getNodePath, getNodeSelector, getNodeLabel, getOuterHTMLSnippet */ - - -/** @param {Element} element */ -/* istanbul ignore next */ -function getClientRect(element) { - const clientRect = element.getBoundingClientRect(); - return { - // Just grab the DOMRect properties we want, excluding x/y/width/height - top: clientRect.top, - bottom: clientRect.bottom, - left: clientRect.left, - right: clientRect.right, - }; -} - -/** - * @param {Array} allElements - * @return {Array} - */ -/* istanbul ignore next */ -function getHTMLImages(allElements) { - const allImageElements = /** @type {Array} */ (allElements.filter(element => { - return element.localName === 'img'; - })); - - return allImageElements.map(element => { - const computedStyle = window.getComputedStyle(element); - return { - // currentSrc used over src to get the url as determined by the browser - // after taking into account srcset/media/sizes/etc. - src: element.currentSrc, - srcset: element.srcset, - displayedWidth: element.width, - displayedHeight: element.height, - clientRect: getClientRect(element), - naturalWidth: element.naturalWidth, - naturalHeight: element.naturalHeight, - attributeWidth: element.getAttribute('width') || '', - attributeHeight: element.getAttribute('height') || '', - cssWidth: '', // this will get overwritten below - cssHeight: '', // this will get overwritten below - isCss: false, - // @ts-ignore: loading attribute not yet added to HTMLImageElement definition. - loading: element.loading, - resourceSize: 0, // this will get overwritten below - isPicture: !!element.parentElement && element.parentElement.tagName === 'PICTURE', - usesObjectFit: ['cover', 'contain', 'scale-down', 'none'].includes( - computedStyle.getPropertyValue('object-fit') - ), - usesPixelArtScaling: ['pixelated', 'crisp-edges'].includes( - computedStyle.getPropertyValue('image-rendering') - ), - // https://html.spec.whatwg.org/multipage/images.html#pixel-density-descriptor - usesSrcSetDensityDescriptor: / \d+(\.\d+)?x/.test(element.srcset), - // @ts-ignore - getNodePath put into scope via stringification - devtoolsNodePath: getNodePath(element), - // @ts-ignore - put into scope via stringification - selector: getNodeSelector(element), - // @ts-ignore - put into scope via stringification - nodeLabel: getNodeLabel(element), - // @ts-ignore - put into scope via stringification - snippet: getOuterHTMLSnippet(element), - }; - }); -} - -/** - * @param {Array} allElements - * @return {Array} - */ -/* istanbul ignore next */ -function getCSSImages(allElements) { - // Chrome normalizes background image style from getComputedStyle to be an absolute URL in quotes. - // Only match basic background-image: url("http://host/image.jpeg") declarations - const CSS_URL_REGEX = /^url\("([^"]+)"\)$/; - - /** @type {Array} */ - const images = []; - - for (const element of allElements) { - const style = window.getComputedStyle(element); - // If the element didn't have a CSS background image, we're not interested. - if (!style.backgroundImage || !CSS_URL_REGEX.test(style.backgroundImage)) continue; - - const imageMatch = style.backgroundImage.match(CSS_URL_REGEX); - // @ts-ignore test() above ensures that there is a match. - const url = imageMatch[1]; - - images.push({ - src: url, - srcset: '', - displayedWidth: element.clientWidth, - displayedHeight: element.clientHeight, - clientRect: getClientRect(element), - // CSS Images do not expose natural size, we'll determine the size later - naturalWidth: 0, - naturalHeight: 0, - attributeWidth: '', - attributeHeight: '', - cssWidth: '', - cssHeight: '', - isCss: true, - isPicture: false, - usesObjectFit: false, - usesPixelArtScaling: ['pixelated', 'crisp-edges'].includes( - style.getPropertyValue('image-rendering') - ), - usesSrcSetDensityDescriptor: false, - resourceSize: 0, // this will get overwritten below - // @ts-ignore - getNodePath put into scope via stringification - devtoolsNodePath: getNodePath(element), - // @ts-ignore - put into scope via stringification - selector: getNodeSelector(element), - // @ts-ignore - put into scope via stringification - nodeLabel: getNodeLabel(element), - // @ts-ignore - put into scope via stringification - snippet: getOuterHTMLSnippet(element), - }); - } - - return images; -} - -/** @return {Array} */ -/* istanbul ignore next */ -function collectImageElementInfo() { - /** @type {Array} */ - // @ts-ignore - added by getElementsInDocumentFnString - const allElements = getElementsInDocument(); - return getHTMLImages(allElements).concat(getCSSImages(allElements)); -} - -/** - * @param {string} url - * @return {Promise<{naturalWidth: number, naturalHeight: number}>} - */ -/* istanbul ignore next */ -function determineNaturalSize(url) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.addEventListener('error', _ => reject(new Error('determineNaturalSize failed img load'))); - img.addEventListener('load', () => { - resolve({ - naturalWidth: img.naturalWidth, - naturalHeight: img.naturalHeight, - }); - }); - - img.src = url; - }); -} - -/** - * @param {LH.Crdp.CSS.CSSStyle} [style] - * @param {string} property - * @return {string | undefined} - */ -function findSizeDeclaration(style, property) { - if (!style) return; - const definedProp = style.cssProperties.find(({name}) => name === property); - if (!definedProp) return; - return definedProp.value; -} - -/** - * Finds the most specific directly matched CSS font-size rule from the list. - * - * @param {Array} [matchedCSSRules] - * @param {string} property - * @returns {string} - */ -function findMostSpecificMatchedCSSRule(matchedCSSRules = [], property) { - let maxSpecificity = -Infinity; - /** @type {LH.Crdp.CSS.CSSRule|undefined} */ - let maxSpecificityRule; - - for (const {rule, matchingSelectors} of matchedCSSRules) { - // hasSizeDeclaration from font-size.js using `.some()` - if (!!rule.style && rule.style.cssProperties.some(({name}) => name === property)) { - const specificities = matchingSelectors.map(idx => - FontSize.computeSelectorSpecificity(rule.selectorList.selectors[idx].text) - ); - const specificity = Math.max(...specificities); - // Use greater OR EQUAL so that the last rule wins in the event of a tie - if (specificity >= maxSpecificity) { - maxSpecificity = specificity; - maxSpecificityRule = rule; - } - } - } - - if (maxSpecificityRule) { - // @ts-ignore the existence of the property object is checked in hasSizeDeclaration - return maxSpecificityRule.style.cssProperties.find(({name}) => name === property).value; - } - return ''; -} - -/** - * @param {LH.Crdp.CSS.GetMatchedStylesForNodeResponse} matched CSS rules} - * @param {string} property - * @returns {string} - */ -function getEffectiveSizingRule({attributesStyle, inlineStyle, matchedCSSRules}, property) { - // CSS sizing can't be inherited - // We only need to check inline & matched styles - // Inline styles have highest priority - const inlineRule = findSizeDeclaration(inlineStyle, property); - if (inlineRule) return inlineRule; - - const attributeRule = findSizeDeclaration(attributesStyle, property); - if (attributeRule) return attributeRule; - // Rules directly referencing the node come next - const matchedRule = findMostSpecificMatchedCSSRule(matchedCSSRules, property); - if (matchedRule) return matchedRule; - return ''; -} - -class ImageElements extends Gatherer { - constructor() { - super(); - /** @type {Map} */ - this._naturalSizeCache = new Map(); - } - - /** - * @param {Driver} driver - * @param {LH.Artifacts.ImageElement} element - * @return {Promise} - */ - async fetchElementWithSizeInformation(driver, element) { - const url = JSON.stringify(element.src); - if (this._naturalSizeCache.has(url)) { - return Object.assign(element, this._naturalSizeCache.get(url)); - } - - try { - // We don't want this to take forever, 250ms should be enough for images that are cached - driver.setNextProtocolTimeout(250); - /** @type {{naturalWidth: number, naturalHeight: number}} */ - const size = await driver.evaluateAsync(`(${determineNaturalSize.toString()})(${url})`); - this._naturalSizeCache.set(url, size); - return Object.assign(element, size); - } catch (_) { - // determineNaturalSize fails on invalid images, which we treat as non-visible - return element; - } - } - - /** - * @param {Driver} driver - * @param {string} devtoolsNodePath - * @param {LH.Artifacts.ImageElement} element - */ - async fetchSourceRules(driver, devtoolsNodePath, element) { - const {nodeId} = await driver.sendCommand('DOM.pushNodeByPathToFrontend', { - path: devtoolsNodePath, - }); - if (!nodeId) return; - const matchedRules = await driver.sendCommand('CSS.getMatchedStylesForNode', { - nodeId: nodeId, - }); - const sourceWidth = getEffectiveSizingRule(matchedRules, 'width'); - const sourceHeight = getEffectiveSizingRule(matchedRules, 'height'); - const sourceRules = {propertyWidth: sourceWidth, propertyHeight: sourceHeight}; - Object.assign(element, sourceRules); - } - - /** - * @param {LH.Gatherer.PassContext} passContext - * @param {LH.Gatherer.LoadData} loadData - * @return {Promise} - */ - async afterPass(passContext, loadData) { - const driver = passContext.driver; - const indexedNetworkRecords = loadData.networkRecords.reduce((map, record) => { - // The network record is only valid for size information if it finished with a successful status - // code that indicates a complete resource response. - if (/^image/.test(record.mimeType) && record.finished && record.statusCode === 200) { - map[record.url] = record; - } - - return map; - }, /** @type {Object} */ ({})); - - const expression = `(function() { - ${pageFunctions.getElementsInDocumentString}; // define function on page - ${pageFunctions.getNodePathString}; - ${pageFunctions.getNodeSelectorString}; - ${pageFunctions.getNodeLabelString}; - ${pageFunctions.getOuterHTMLSnippetString}; - ${getClientRect.toString()}; - ${getHTMLImages.toString()}; - ${getCSSImages.toString()}; - ${collectImageElementInfo.toString()}; - - return collectImageElementInfo(); - })()`; - - /** @type {Array} */ - const elements = await driver.evaluateAsync(expression); - - /** @type {Array} */ - const imageUsage = []; - const top50Images = Object.values(indexedNetworkRecords) - .sort((a, b) => b.resourceSize - a.resourceSize) - .slice(0, 50); - await Promise.all([ - driver.sendCommand('DOM.enable'), - driver.sendCommand('CSS.enable'), - driver.sendCommand('DOM.getDocument', {depth: -1, pierce: true}), - ]); - - for (let element of elements) { - // Pull some of our information directly off the network record. - const networkRecord = indexedNetworkRecords[element.src] || {}; - element.mimeType = networkRecord.mimeType; - // Resource size is almost always the right one to be using because of the below: - // transferSize = resourceSize + headers.length - // HOWEVER, there are some cases where an image is compressed again over the network and transfer size - // is smaller (see https://github.com/GoogleChrome/lighthouse/pull/4968). - // Use the min of the two numbers to be safe. - const {resourceSize = 0, transferSize = 0} = networkRecord; - element.resourceSize = Math.min(resourceSize, transferSize); - await this.fetchSourceRules(driver, element.devtoolsNodePath, element); - // Images within `picture` behave strangely and natural size information isn't accurate, - // CSS images have no natural size information at all. Try to get the actual size if we can. - // Additional fetch is expensive; don't bother if we don't have a networkRecord for the image, - // or it's not in the top 50 largest images. - if ( - (element.isPicture || element.isCss || element.srcset) && - networkRecord && - top50Images.includes(networkRecord) - ) { - element = await this.fetchElementWithSizeInformation(driver, element); - } - - imageUsage.push(element); - } - - await Promise.all([ - driver.sendCommand('DOM.disable'), - driver.sendCommand('CSS.disable'), - ]); - - return imageUsage; - } -} - -module.exports = ImageElements; From e724369107c5f7a0f02702dc9e0eda74d1942241 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 22 Jul 2020 18:14:36 -0700 Subject: [PATCH 32/40] reverted image-elements to html size attribute changes --- .../gather/gatherers/image-elements.js | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index e69de29bb2d1..322bebe5c84c 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -0,0 +1,266 @@ +/** + * @license Copyright 2017 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +/** + * @fileoverview Gathers all images used on the page with their src, size, + * and attribute information. Executes script in the context of the page. + */ +'use strict'; + +const Gatherer = require('./gatherer.js'); +const pageFunctions = require('../../lib/page-functions.js'); +const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars + +/* global window, getElementsInDocument, Image, getNodePath, getNodeSelector, getNodeLabel, getOuterHTMLSnippet */ + + +/** @param {Element} element */ +/* istanbul ignore next */ +function getClientRect(element) { + const clientRect = element.getBoundingClientRect(); + return { + // Just grab the DOMRect properties we want, excluding x/y/width/height + top: clientRect.top, + bottom: clientRect.bottom, + left: clientRect.left, + right: clientRect.right, + }; +} + +/** +* @param {Array} allElements +* @return {Array} +*/ +/* istanbul ignore next */ +function getHTMLImages(allElements) { + const allImageElements = /** @type {Array} */ (allElements.filter(element => { + return element.localName === 'img'; + })); + + return allImageElements.map(element => { + const computedStyle = window.getComputedStyle(element); + return { + // currentSrc used over src to get the url as determined by the browser + // after taking into account srcset/media/sizes/etc. + src: element.currentSrc, + srcset: element.srcset, + displayedWidth: element.width, + displayedHeight: element.height, + clientRect: getClientRect(element), + naturalWidth: element.naturalWidth, + naturalHeight: element.naturalHeight, + attributeWidth: element.getAttribute('width') || '', + attributeHeight: element.getAttribute('height') || '', + isCss: false, + // @ts-ignore: loading attribute not yet added to HTMLImageElement definition. + loading: element.loading, + resourceSize: 0, // this will get overwritten below + isPicture: !!element.parentElement && element.parentElement.tagName === 'PICTURE', + usesObjectFit: ['cover', 'contain', 'scale-down', 'none'].includes( + computedStyle.getPropertyValue('object-fit') + ), + usesPixelArtScaling: ['pixelated', 'crisp-edges'].includes( + computedStyle.getPropertyValue('image-rendering') + ), + // https://html.spec.whatwg.org/multipage/images.html#pixel-density-descriptor + usesSrcSetDensityDescriptor: / \d+(\.\d+)?x/.test(element.srcset), + // @ts-ignore - getNodePath put into scope via stringification + devtoolsNodePath: getNodePath(element), + // @ts-ignore - put into scope via stringification + selector: getNodeSelector(element), + // @ts-ignore - put into scope via stringification + nodeLabel: getNodeLabel(element), + // @ts-ignore - put into scope via stringification + snippet: getOuterHTMLSnippet(element), + }; + }); +} + +/** +* @param {Array} allElements +* @return {Array} +*/ +/* istanbul ignore next */ +function getCSSImages(allElements) { + // Chrome normalizes background image style from getComputedStyle to be an absolute URL in quotes. + // Only match basic background-image: url("http://host/image.jpeg") declarations + const CSS_URL_REGEX = /^url\("([^"]+)"\)$/; + + /** @type {Array} */ + const images = []; + + for (const element of allElements) { + const style = window.getComputedStyle(element); + // If the element didn't have a CSS background image, we're not interested. + if (!style.backgroundImage || !CSS_URL_REGEX.test(style.backgroundImage)) continue; + + const imageMatch = style.backgroundImage.match(CSS_URL_REGEX); + // @ts-ignore test() above ensures that there is a match. + const url = imageMatch[1]; + + images.push({ + src: url, + srcset: '', + displayedWidth: element.clientWidth, + displayedHeight: element.clientHeight, + clientRect: getClientRect(element), + // CSS Images do not expose natural size, we'll determine the size later + naturalWidth: 0, + naturalHeight: 0, + attributeWidth: '', + attributeHeight: '', + isCss: true, + isPicture: false, + usesObjectFit: false, + usesPixelArtScaling: ['pixelated', 'crisp-edges'].includes( + style.getPropertyValue('image-rendering') + ), + usesSrcSetDensityDescriptor: false, + resourceSize: 0, // this will get overwritten below + // @ts-ignore - getNodePath put into scope via stringification + devtoolsNodePath: getNodePath(element), + // @ts-ignore - put into scope via stringification + selector: getNodeSelector(element), + // @ts-ignore - put into scope via stringification + nodeLabel: getNodeLabel(element), + // @ts-ignore - put into scope via stringification + snippet: getOuterHTMLSnippet(element), + }); + } + + return images; +} + +/** @return {Array} */ +/* istanbul ignore next */ +function collectImageElementInfo() { + /** @type {Array} */ + // @ts-ignore - added by getElementsInDocumentFnString + const allElements = getElementsInDocument(); + return getHTMLImages(allElements).concat(getCSSImages(allElements)); +} + +/** +* @param {string} url +* @return {Promise<{naturalWidth: number, naturalHeight: number}>} +*/ +/* istanbul ignore next */ +function determineNaturalSize(url) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.addEventListener('error', _ => reject(new Error('determineNaturalSize failed img load'))); + img.addEventListener('load', () => { + resolve({ + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + }); + }); + + img.src = url; + }); +} + +class ImageElements extends Gatherer { + constructor() { + super(); + /** @type {Map} */ + this._naturalSizeCache = new Map(); + } + + /** + * @param {Driver} driver + * @param {LH.Artifacts.ImageElement} element + * @return {Promise} + */ + async fetchElementWithSizeInformation(driver, element) { + const url = JSON.stringify(element.src); + if (this._naturalSizeCache.has(url)) { + return Object.assign(element, this._naturalSizeCache.get(url)); + } + + try { + // We don't want this to take forever, 250ms should be enough for images that are cached + driver.setNextProtocolTimeout(250); + /** @type {{naturalWidth: number, naturalHeight: number}} */ + const size = await driver.evaluateAsync(`(${determineNaturalSize.toString()})(${url})`); + this._naturalSizeCache.set(url, size); + return Object.assign(element, size); + } catch (_) { + // determineNaturalSize fails on invalid images, which we treat as non-visible + return element; + } + } + + /** + * @param {LH.Gatherer.PassContext} passContext + * @param {LH.Gatherer.LoadData} loadData + * @return {Promise} + */ + async afterPass(passContext, loadData) { + const driver = passContext.driver; + const indexedNetworkRecords = loadData.networkRecords.reduce((map, record) => { + // The network record is only valid for size information if it finished with a successful status + // code that indicates a complete resource response. + if (/^image/.test(record.mimeType) && record.finished && record.statusCode === 200) { + map[record.url] = record; + } + + return map; + }, /** @type {Object} */ ({})); + + const expression = `(function() { + ${pageFunctions.getElementsInDocumentString}; // define function on page + ${pageFunctions.getNodePathString}; + ${pageFunctions.getNodeSelectorString}; + ${pageFunctions.getNodeLabelString}; + ${pageFunctions.getOuterHTMLSnippetString}; + ${getClientRect.toString()}; + ${getHTMLImages.toString()}; + ${getCSSImages.toString()}; + ${collectImageElementInfo.toString()}; + return collectImageElementInfo(); + })()`; + + /** @type {Array} */ + const elements = await driver.evaluateAsync(expression); + + /** @type {Array} */ + const imageUsage = []; + const top50Images = Object.values(indexedNetworkRecords) + .sort((a, b) => b.resourceSize - a.resourceSize) + .slice(0, 50); + + for (let element of elements) { + // Pull some of our information directly off the network record. + const networkRecord = indexedNetworkRecords[element.src] || {}; + element.mimeType = networkRecord.mimeType; + // Resource size is almost always the right one to be using because of the below: + // transferSize = resourceSize + headers.length + // HOWEVER, there are some cases where an image is compressed again over the network and transfer size + // is smaller (see https://github.com/GoogleChrome/lighthouse/pull/4968). + // Use the min of the two numbers to be safe. + const {resourceSize = 0, transferSize = 0} = networkRecord; + element.resourceSize = Math.min(resourceSize, transferSize); + + // Images within `picture` behave strangely and natural size information isn't accurate, + // CSS images have no natural size information at all. Try to get the actual size if we can. + // Additional fetch is expensive; don't bother if we don't have a networkRecord for the image, + // or it's not in the top 50 largest images. + if ( + (element.isPicture || element.isCss || element.srcset) && + networkRecord && + top50Images.includes(networkRecord) + ) { + element = await this.fetchElementWithSizeInformation(driver, element); + } + + imageUsage.push(element); + } + + return imageUsage; + } +} + +module.exports = ImageElements; From 7c80ad30854701c0960dd444be9aeb88d8ad354b Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Wed, 22 Jul 2020 18:19:27 -0700 Subject: [PATCH 33/40] fixed image-elements indenting --- .../gather/gatherers/image-elements.js | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 322bebe5c84c..337b48ab30f3 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -30,9 +30,9 @@ function getClientRect(element) { } /** -* @param {Array} allElements -* @return {Array} -*/ + * @param {Array} allElements + * @return {Array} + */ /* istanbul ignore next */ function getHTMLImages(allElements) { const allImageElements = /** @type {Array} */ (allElements.filter(element => { @@ -79,9 +79,9 @@ function getHTMLImages(allElements) { } /** -* @param {Array} allElements -* @return {Array} -*/ + * @param {Array} allElements + * @return {Array} + */ /* istanbul ignore next */ function getCSSImages(allElements) { // Chrome normalizes background image style from getComputedStyle to be an absolute URL in quotes. @@ -143,9 +143,9 @@ function collectImageElementInfo() { } /** -* @param {string} url -* @return {Promise<{naturalWidth: number, naturalHeight: number}>} -*/ + * @param {string} url + * @return {Promise<{naturalWidth: number, naturalHeight: number}>} + */ /* istanbul ignore next */ function determineNaturalSize(url) { return new Promise((resolve, reject) => { @@ -170,10 +170,10 @@ class ImageElements extends Gatherer { } /** - * @param {Driver} driver - * @param {LH.Artifacts.ImageElement} element - * @return {Promise} - */ + * @param {Driver} driver + * @param {LH.Artifacts.ImageElement} element + * @return {Promise} + */ async fetchElementWithSizeInformation(driver, element) { const url = JSON.stringify(element.src); if (this._naturalSizeCache.has(url)) { @@ -194,10 +194,10 @@ class ImageElements extends Gatherer { } /** - * @param {LH.Gatherer.PassContext} passContext - * @param {LH.Gatherer.LoadData} loadData - * @return {Promise} - */ + * @param {LH.Gatherer.PassContext} passContext + * @param {LH.Gatherer.LoadData} loadData + * @return {Promise} + */ async afterPass(passContext, loadData) { const driver = passContext.driver; const indexedNetworkRecords = loadData.networkRecords.reduce((map, record) => { @@ -220,6 +220,7 @@ class ImageElements extends Gatherer { ${getHTMLImages.toString()}; ${getCSSImages.toString()}; ${collectImageElementInfo.toString()}; + return collectImageElementInfo(); })()`; From 1e0504c5032fd95dd096a5a7fe1324a5c85beedf Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Thu, 23 Jul 2020 15:00:27 -0700 Subject: [PATCH 34/40] changed size typedef, isValidCss, added tests --- lighthouse-core/audits/unsized-images.js | 5 ++++- .../test/audits/unsized-images-test.js | 18 ++++++++++++++++++ types/artifacts.d.ts | 4 ++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index 77f61d7465fe..93eea7270fc8 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -57,7 +57,10 @@ class SizedImages extends Audit { */ static isValidCss(property) { // an img css size property is valid for preventing CLS - // if it ... + // if it is defined and not equal to 'auto' + // `undefined` and `''` are implicitly rejected as invalid + // because of their falsy short-circuit of && in isUnsizedImage + if (!property) return false; return property !== 'auto'; } diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js index 10cc0eb3971a..ff6f76660eab 100644 --- a/lighthouse-core/test/audits/unsized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -162,3 +162,21 @@ describe('Size attribute validity check', () => { expect(UnSizedImagesAudit.isValidAttr('4000000')).toEqual(true); }); }); + +describe('CSS size property validity check', () => { + it('fails if it was never defined', () => { + expect(UnSizedImagesAudit.isValidCss(undefined)).toEqual(false); + }); + + it('fails if it is empty', () => { + expect(UnSizedImagesAudit.isValidCss('')).toEqual(false); + }); + + it('fails if it is auto', () => { + expect(UnSizedImagesAudit.isValidCss('auto')).toEqual(false); + }); + + it('passes if it is defined and not auto', () => { + expect(UnSizedImagesAudit.isValidCss('200')).toEqual(true); + }); +}); diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index a33d870f6bc6..18fc796579e8 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -415,9 +415,9 @@ declare global { /** The raw height attribute of the image element. CSS images will be set to the empty string. */ attributeHeight: string; /** The CSS width property of the image element. */ - cssWidth?: string; + cssWidth?: string | undefined; /** The CSS height property of the image element. */ - cssHeight?: string; + cssHeight?: string | undefined; /** The BoundingClientRect of the element. */ clientRect: { top: number; From cb50193f821cdb073c910597dc9943a414bfb3aa Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Fri, 24 Jul 2020 15:33:05 -0700 Subject: [PATCH 35/40] fixed isValidCss and added css testing --- lighthouse-core/audits/unsized-images.js | 2 +- .../test/audits/unsized-images-test.js | 270 ++++++++++++++++-- 2 files changed, 255 insertions(+), 17 deletions(-) diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index 93eea7270fc8..619018a59dc1 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -57,7 +57,7 @@ class SizedImages extends Audit { */ static isValidCss(property) { // an img css size property is valid for preventing CLS - // if it is defined and not equal to 'auto' + // if it is defined, not empty, and not equal to 'auto' // `undefined` and `''` are implicitly rejected as invalid // because of their falsy short-circuit of && in isUnsizedImage if (!property) return false; diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js index ff6f76660eab..3a1ab44de13a 100644 --- a/lighthouse-core/test/audits/unsized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -36,41 +36,185 @@ describe('Sized images audit', () => { isCss: true, attributeWidth: '', attributeHeight: '', + cssWidth: '', + cssHeight: '', }, }); - describe('has empty', () => { - testImage('has empty width attribute', { + describe('has empty width', () => { + testImage('only has attribute height', { score: 0, props: { attributeWidth: '', attributeHeight: '100', + cssWidth: '', + cssHeight: '', }, }); - testImage('has empty height attribute', { + testImage('only has css height', { + score: 0, + props: { + attributeWidth: '', + attributeHeight: '', + cssWidth: '', + cssHeight: '100', + }, + }); + + testImage('only has attribute height & css height', { + score: 0, + props: { + attributeWidth: '', + attributeHeight: '100', + cssWidth: '', + cssHeight: '100', + }, + }); + }); + + describe('has empty height', () => { + testImage('only has attribute width', { score: 0, props: { attributeWidth: '100', attributeHeight: '', + cssWidth: '', + cssHeight: '', }, }); - testImage('has empty width and height attributes', { + testImage('only has css width', { score: 0, props: { attributeWidth: '', attributeHeight: '', + cssWidth: '100', + cssHeight: '', + }, + }); + + testImage('only has attribute width & css width', { + score: 0, + props: { + attributeWidth: '100', + attributeHeight: '', + cssWidth: '100', + cssHeight: '', + }, + }); + }); + + testImage('has empty width and height', { + score: 0, + props: { + attributeWidth: '', + attributeHeight: '', + cssWidth: '', + cssHeight: '', + }, + }); + + describe('has valid width and height', () => { + testImage('has attribute width and css height', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '', + cssWidth: '', + cssHeight: '100', + }, + }); + + testImage('has attribute width and attribute height', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '100', + cssWidth: '', + cssHeight: '', + }, + }); + + testImage('has css width and attribute height', { + score: 1, + props: { + attributeWidth: '', + attributeHeight: '100', + cssWidth: '100', + cssHeight: '', + }, + }); + + testImage('has css width and css height', { + score: 1, + props: { + attributeWidth: '', + attributeHeight: '', + cssWidth: '100', + cssHeight: '100', + }, + }); + + testImage('has css & attribute width and css height', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '', + cssWidth: '100', + cssHeight: '100', + }, + }); + + testImage('has css & attribute width and attribute height', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '100', + cssWidth: '100', + cssHeight: '', + }, + }); + + testImage('has css & attribute height and css width', { + score: 1, + props: { + attributeWidth: '', + attributeHeight: '100', + cssWidth: '100', + cssHeight: '100', + }, + }); + + testImage('has css & attribute height and attribute width', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '100', + cssWidth: '', + cssHeight: '100', + }, + }); + + testImage('has css & attribute height and css & attribute width', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '100', + cssWidth: '100', + cssHeight: '100', }, }); }); - describe('has invalid', () => { + describe('has invalid width', () => { testImage('has invalid width attribute', { score: 0, props: { attributeWidth: '-200', attributeHeight: '100', + cssWidth: '', + cssHeight: '', }, }); @@ -78,25 +222,101 @@ describe('Sized images audit', () => { score: 0, props: { attributeWidth: '100', - attributeHeight: '300.5', + attributeHeight: '-200', + cssWidth: '', + cssHeight: '', }, }); - testImage('has invalid width and height attributes', { + testImage('has invalid css width', { score: 0, props: { - attributeWidth: '0', - attributeHeight: '100/2', + attributeWidth: '', + attributeHeight: '', + cssWidth: 'auto', + cssHeight: '100', }, }); - }); - testImage('has valid width and height attributes', { - score: 1, - props: { - attributeWidth: '100', - attributeHeight: '100', - }, + testImage('has invalid css height', { + score: 0, + props: { + attributeWidth: '', + attributeHeight: '', + cssWidth: '100', + cssHeight: 'auto', + }, + }); + + testImage('has invalid width attribute, and valid css width', { + score: 1, + props: { + attributeWidth: '-200', + attributeHeight: '100', + cssWidth: '100', + cssHeight: '', + }, + }); + + testImage('has invalid height attribute, and valid css height', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '-200', + cssWidth: '', + cssHeight: '100', + }, + }); + + testImage('has invalid css width, and valid attribute width', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '', + cssWidth: 'auto', + cssHeight: '100', + }, + }); + + testImage('has invalid css height, and valid attribute height', { + score: 1, + props: { + attributeWidth: '', + attributeHeight: '100', + cssWidth: '100', + cssHeight: 'auto', + }, + }); + + testImage('has invalid css width & height, and valid attribute width & height', { + score: 1, + props: { + attributeWidth: '100', + attributeHeight: '100', + cssWidth: 'auto', + cssHeight: 'auto', + }, + }); + + testImage('has invalid attribute width & height, and valid css width & height', { + score: 1, + props: { + attributeWidth: '-200', + attributeHeight: '-200', + cssWidth: '100', + cssHeight: '100', + }, + }); + + testImage('has invalid attribute width & height, and invalid css width & height', { + score: 0, + props: { + attributeWidth: '-200', + attributeHeight: '-200', + cssWidth: 'auto', + cssHeight: 'auto', + }, + }); }); it('is not applicable when there are no images', async () => { @@ -114,6 +334,8 @@ describe('Sized images audit', () => { { attributeWidth: '', attributeHeight: '', + cssWidth: '', + cssHeight: '', }, 'image1.png' ), @@ -128,6 +350,8 @@ describe('Sized images audit', () => { { attributeWidth: '', attributeHeight: '', + cssWidth: '', + cssHeight: '', }, 'image3.png' ), @@ -141,6 +365,10 @@ describe('Sized images audit', () => { }); describe('Size attribute validity check', () => { + it('fails if it is empty', () => { + expect(UnSizedImagesAudit.isValidAttr('')).toEqual(false); + }); + it('fails on non-numeric characters', () => { expect(UnSizedImagesAudit.isValidAttr('zero')).toEqual(false); expect(UnSizedImagesAudit.isValidAttr('1002$')).toEqual(false); @@ -178,5 +406,15 @@ describe('CSS size property validity check', () => { it('passes if it is defined and not auto', () => { expect(UnSizedImagesAudit.isValidCss('200')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('300.5')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('150px')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('80%')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('5cm')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('20rem')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('7vw')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('-20')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('0')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('three')).toEqual(true); + expect(UnSizedImagesAudit.isValidCss('-20')).toEqual(true); }); }); From 7c5935899234321862fa0ed2e77af74b982d69ca Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Mon, 27 Jul 2020 16:14:05 -0700 Subject: [PATCH 36/40] fixed commenting format --- lighthouse-core/audits/unsized-images.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index 619018a59dc1..67cc6ee3d49f 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -22,7 +22,7 @@ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); /** * @fileoverview - * Audit that checks whether all images have width and height attributes. + * Audit that checks whether all images have explicit width and height. */ class SizedImages extends Audit { @@ -44,8 +44,8 @@ class SizedImages extends Audit { * @return {boolean} */ static isValidAttr(attr) { - // an img size attribute is valid for preventing CLS - // if it is a non-negative, non-zero integer + // An img size attribute is valid for preventing CLS + // if it is a non-negative, non-zero integer. const NON_NEGATIVE_INT_REGEX = /^\d+$/; const ZERO_REGEX = /^0+$/; return NON_NEGATIVE_INT_REGEX.test(attr) && !ZERO_REGEX.test(attr); @@ -56,10 +56,10 @@ class SizedImages extends Audit { * @return {boolean} */ static isValidCss(property) { - // an img css size property is valid for preventing CLS - // if it is defined, not empty, and not equal to 'auto' + // An img css size property is valid for preventing CLS + // if it is defined, not empty, and not equal to 'auto'. // `undefined` and `''` are implicitly rejected as invalid - // because of their falsy short-circuit of && in isUnsizedImage + // because of their falsy short-circuit of && in isUnsizedImage. if (!property) return false; return property !== 'auto'; } @@ -69,7 +69,7 @@ class SizedImages extends Audit { * @return {boolean} */ static isUnsizedImage(image) { - // images are considered sized if they have defined & valid values + // Images are considered sized if they have defined & valid values. const attrWidth = image.attributeWidth; const attrHeight = image.attributeHeight; const cssWidth = image.cssWidth; @@ -88,7 +88,7 @@ class SizedImages extends Audit { * @return {Promise} */ static async audit(artifacts) { - // CSS background-images are ignored for this audit + // CSS background-images are ignored for this audit. const images = artifacts.ImageElements.filter(el => !el.isCss); const unsizedImages = []; From 9c97f250ed7ed27793ed55db6a8d5e6295986b43 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Mon, 27 Jul 2020 16:27:33 -0700 Subject: [PATCH 37/40] updated audit description UIString --- lighthouse-core/audits/unsized-images.js | 2 +- lighthouse-core/lib/i18n/locales/en-US.json | 2 +- lighthouse-core/lib/i18n/locales/en-XL.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index 67cc6ee3d49f..c9962cfadd93 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -15,7 +15,7 @@ const UIStrings = { /** Title of a Lighthouse audit that provides detail on whether all images have explicit width and height. This descriptive title is shown to users when one or more images does not have explicit width and height */ failureTitle: 'Image elements do not have explicit `width` and `height`', /** Description of a Lighthouse audit that tells the user why they should include explicit width and height for all images. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ - description: 'Always include explicit width and height on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', + description: 'Always include explicit width and height on image elements to reduce layout shifts and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', }; const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index 853a7ebe4a9f..7ab944ef2b6b 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -1260,7 +1260,7 @@ "message": "Timing budget" }, "lighthouse-core/audits/unsized-images.js | description": { - "message": "Always include explicit width and height on your image elements to reduce layout shifting and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)" + "message": "Always include explicit width and height on image elements to reduce layout shifts and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)" }, "lighthouse-core/audits/unsized-images.js | failureTitle": { "message": "Image elements do not have explicit `width` and `height`" diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index 3a1d6cc3387f..db6894dfd96a 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -1260,7 +1260,7 @@ "message": "T̂ím̂ín̂ǵ b̂úd̂ǵêt́" }, "lighthouse-core/audits/unsized-images.js | description": { - "message": "Âĺŵáŷś îńĉĺûd́ê éx̂ṕl̂íĉít̂ ẃîd́t̂h́ âńd̂ h́êíĝh́t̂ ón̂ ýôúr̂ ím̂áĝé êĺêḿêńt̂ś t̂ó r̂éd̂úĉé l̂áŷóût́ ŝh́îf́t̂ín̂ǵ âńd̂ ím̂ṕr̂óv̂é ĈĹŜ. [Ĺêár̂ń m̂ór̂é](https://web.dev/optimize-cls/#images-without-dimensions)" + "message": "Âĺŵáŷś îńĉĺûd́ê éx̂ṕl̂íĉít̂ ẃîd́t̂h́ âńd̂ h́êíĝh́t̂ ón̂ ím̂áĝé êĺêḿêńt̂ś t̂ó r̂éd̂úĉé l̂áŷóût́ ŝh́îf́t̂ś âńd̂ ím̂ṕr̂óv̂é ĈĹŜ. [Ĺêár̂ń m̂ór̂é](https://web.dev/optimize-cls/#images-without-dimensions)" }, "lighthouse-core/audits/unsized-images.js | failureTitle": { "message": "Îḿâǵê él̂ém̂én̂t́ŝ d́ô ńôt́ ĥáv̂é êx́p̂ĺîćît́ `width` âńd̂ `height`" From a684510e2a75a8500ff6131af26c60df79fdb6b4 Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Mon, 27 Jul 2020 16:51:36 -0700 Subject: [PATCH 38/40] fixed nits --- lighthouse-core/audits/unsized-images.js | 57 +++++++++---------- lighthouse-core/config/experimental-config.js | 4 +- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index c9962cfadd93..1036b0b62b1f 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -3,6 +3,11 @@ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +/** + * @fileoverview + * Audit that checks whether all images have explicit width and height. + */ + 'use strict'; const Audit = require('./audit.js'); @@ -20,11 +25,6 @@ const UIStrings = { const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); -/** - * @fileoverview - * Audit that checks whether all images have explicit width and height. - */ - class SizedImages extends Audit { /** * @return {LH.Audit.Meta} @@ -40,44 +40,42 @@ class SizedImages extends Audit { } /** + * An img size attribute is valid for preventing CLS + * if it is a non-negative, non-zero integer. * @param {string} attr * @return {boolean} */ static isValidAttr(attr) { - // An img size attribute is valid for preventing CLS - // if it is a non-negative, non-zero integer. const NON_NEGATIVE_INT_REGEX = /^\d+$/; const ZERO_REGEX = /^0+$/; return NON_NEGATIVE_INT_REGEX.test(attr) && !ZERO_REGEX.test(attr); } /** - * @param {string} property + * An img css size property is valid for preventing CLS + * if it is defined, not empty, and not equal to 'auto'. + * @param {string | undefined} property * @return {boolean} */ static isValidCss(property) { - // An img css size property is valid for preventing CLS - // if it is defined, not empty, and not equal to 'auto'. - // `undefined` and `''` are implicitly rejected as invalid - // because of their falsy short-circuit of && in isUnsizedImage. if (!property) return false; return property !== 'auto'; } /** + * Images are considered sized if they have defined & valid values. * @param {LH.Artifacts.ImageElement} image * @return {boolean} */ static isUnsizedImage(image) { - // Images are considered sized if they have defined & valid values. const attrWidth = image.attributeWidth; const attrHeight = image.attributeHeight; const cssWidth = image.cssWidth; const cssHeight = image.cssHeight; - const widthIsValidAttribute = attrWidth && SizedImages.isValidAttr(attrWidth); - const widthIsValidCss = cssWidth && SizedImages.isValidCss(cssWidth); - const heightIsValidAttribute = attrHeight && SizedImages.isValidAttr(attrHeight); - const heightIsValidCss = cssHeight && SizedImages.isValidCss(cssHeight); + const widthIsValidAttribute = SizedImages.isValidAttr(attrWidth); + const widthIsValidCss = SizedImages.isValidCss(cssWidth); + const heightIsValidAttribute = SizedImages.isValidAttr(attrHeight); + const heightIsValidCss = SizedImages.isValidCss(cssHeight); const validWidth = widthIsValidAttribute || widthIsValidCss; const validHeight = heightIsValidAttribute || heightIsValidCss; return !validWidth || !validHeight; @@ -93,19 +91,18 @@ class SizedImages extends Audit { const unsizedImages = []; for (const image of images) { - if (SizedImages.isUnsizedImage(image)) { - const url = URL.elideDataURI(image.src); - unsizedImages.push({ - url, - node: /** @type {LH.Audit.Details.NodeValue} */ ({ - type: 'node', - path: image.devtoolsNodePath, - selector: image.selector, - nodeLabel: image.nodeLabel, - snippet: image.snippet, - }), - }); - } + if (!SizedImages.isUnsizedImage(image)) continue; + const url = URL.elideDataURI(image.src); + unsizedImages.push({ + url, + node: /** @type {LH.Audit.Details.NodeValue} */ ({ + type: 'node', + path: image.devtoolsNodePath, + selector: image.selector, + nodeLabel: image.nodeLabel, + snippet: image.snippet, + }), + }); } /** @type {LH.Audit.Details.Table['headings']} */ diff --git a/lighthouse-core/config/experimental-config.js b/lighthouse-core/config/experimental-config.js index 3f9a4fb9b06b..a9ba9327eb5c 100644 --- a/lighthouse-core/config/experimental-config.js +++ b/lighthouse-core/config/experimental-config.js @@ -24,8 +24,8 @@ const config = { ], }], categories: { - // @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default - // config is awkward - easier to omit the property here. Will defer to default config. + // @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default + // config is awkward - easier to omit the property here. Will defer to default config. 'best-practices': { auditRefs: [ {id: 'unsized-images', weight: 1, group: 'best-practices-ux'}, From 1504d1dc56d6a6c9657bb802dd6f8d66d2fcd5bd Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 28 Jul 2020 17:27:00 -0700 Subject: [PATCH 39/40] refactored unsized-images-test --- .../test/audits/unsized-images-test.js | 246 +++++++++--------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js index 3a1ab44de13a..caae685f5335 100644 --- a/lighthouse-core/test/audits/unsized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -16,306 +16,306 @@ function generateImage(props, src = 'https://google.com/logo.png', isCss = false } describe('Sized images audit', () => { - function testImage(condition, data) { - const description = `handles when an image ${condition}`; - it(description, async () => { - const result = await UnSizedImagesAudit.audit({ - ImageElements: [ - generateImage( - data.props - ), - ], - }); - expect(result.score).toEqual(data.score); + function runAudit(props) { + const result = UnSizedImagesAudit.audit({ + ImageElements: [ + generateImage( + props + ), + ], }); + return result; } - testImage('is a css image', { - score: 1, - props: { + it('passes when an image is a css image', async () => { + const result = await runAudit({ isCss: true, attributeWidth: '', attributeHeight: '', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(1); }); describe('has empty width', () => { - testImage('only has attribute height', { - score: 0, - props: { + it('fails when an image only has attribute height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '100', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); - testImage('only has css height', { - score: 0, - props: { + it('fails when an image only has css height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '', cssWidth: '', cssHeight: '100', - }, + }); + expect(result.score).toEqual(0); }); - testImage('only has attribute height & css height', { - score: 0, - props: { + it('fails when an image only has attribute height & css height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '100', cssWidth: '', cssHeight: '100', - }, + }); + expect(result.score).toEqual(0); }); }); describe('has empty height', () => { - testImage('only has attribute width', { - score: 0, - props: { + it('fails when an image only has attribute width', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); - testImage('only has css width', { - score: 0, - props: { + it('fails when an image only has css width', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '', cssWidth: '100', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); - testImage('only has attribute width & css width', { - score: 0, - props: { + it('fails when an image only has attribute width & css width', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '', cssWidth: '100', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); }); - testImage('has empty width and height', { - score: 0, - props: { + it('fails when an image has empty width and height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); describe('has valid width and height', () => { - testImage('has attribute width and css height', { - score: 1, - props: { + it('passes when an image has attribute width and css height', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '', cssWidth: '', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has attribute width and attribute height', { - score: 1, - props: { + it('passes when an image has attribute width and attribute height', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '100', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css width and attribute height', { - score: 1, - props: { + it('passes when an image has css width and attribute height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '100', cssWidth: '100', cssHeight: '', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css width and css height', { - score: 1, - props: { + it('passes when an image has css width and css height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '', cssWidth: '100', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css & attribute width and css height', { - score: 1, - props: { + it('passes when an image has css & attribute width and css height', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '', cssWidth: '100', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css & attribute width and attribute height', { - score: 1, - props: { + it('passes when an image has css & attribute width and attribute height', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '100', cssWidth: '100', cssHeight: '', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css & attribute height and css width', { - score: 1, - props: { + it('passes when an image has css & attribute height and css width', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '100', cssWidth: '100', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css & attribute height and attribute width', { - score: 1, - props: { + it('passes when an image has css & attribute height and attribute width', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '100', cssWidth: '', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has css & attribute height and css & attribute width', { - score: 1, - props: { + it('passes when an image has css & attribute height and css & attribute width', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '100', cssWidth: '100', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); }); describe('has invalid width', () => { - testImage('has invalid width attribute', { - score: 0, - props: { + it('fails when an image has invalid width attribute', async () => { + const result = await runAudit({ attributeWidth: '-200', attributeHeight: '100', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); - testImage('has invalid height attribute', { - score: 0, - props: { + it('fails when an image has invalid height attribute', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '-200', cssWidth: '', cssHeight: '', - }, + }); + expect(result.score).toEqual(0); }); - testImage('has invalid css width', { - score: 0, - props: { + it('fails when an image has invalid css width', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '', cssWidth: 'auto', cssHeight: '100', - }, + }); + expect(result.score).toEqual(0); }); - testImage('has invalid css height', { - score: 0, - props: { + it('fails when an image has invalid css height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '', cssWidth: '100', cssHeight: 'auto', - }, + }); + expect(result.score).toEqual(0); }); - testImage('has invalid width attribute, and valid css width', { - score: 1, - props: { + it('passes when an image has invalid width attribute, and valid css width', async () => { + const result = await runAudit({ attributeWidth: '-200', attributeHeight: '100', cssWidth: '100', cssHeight: '', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has invalid height attribute, and valid css height', { - score: 1, - props: { + it('passes when an image has invalid height attribute, and valid css height', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '-200', cssWidth: '', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has invalid css width, and valid attribute width', { - score: 1, - props: { + it('passes when an image has invalid css width, and valid attribute width', async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '', cssWidth: 'auto', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has invalid css height, and valid attribute height', { - score: 1, - props: { + it('passes when an image has invalid css height, and valid attribute height', async () => { + const result = await runAudit({ attributeWidth: '', attributeHeight: '100', cssWidth: '100', cssHeight: 'auto', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has invalid css width & height, and valid attribute width & height', { - score: 1, - props: { + it('passes when an image has invalid css width & height, and valid attribute width & height', + async () => { + const result = await runAudit({ attributeWidth: '100', attributeHeight: '100', cssWidth: 'auto', cssHeight: 'auto', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has invalid attribute width & height, and valid css width & height', { - score: 1, - props: { + it('passes when an image has invalid attribute width & height, and valid css width & height', + async () => { + const result = await runAudit({ attributeWidth: '-200', attributeHeight: '-200', cssWidth: '100', cssHeight: '100', - }, + }); + expect(result.score).toEqual(1); }); - testImage('has invalid attribute width & height, and invalid css width & height', { - score: 0, - props: { + it('fails when an image has invalid attribute width & height, and invalid css width & height', + async () => { + const result = await runAudit({ attributeWidth: '-200', attributeHeight: '-200', cssWidth: 'auto', cssHeight: 'auto', - }, + }); + expect(result.score).toEqual(0); }); }); From 5d545e4a2a8500e68386d5e489461f55a067970b Mon Sep 17 00:00:00 2001 From: Lemuel Cardenas-arriaga Date: Tue, 28 Jul 2020 17:32:17 -0700 Subject: [PATCH 40/40] small change in runAudit --- lighthouse-core/test/audits/unsized-images-test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js index caae685f5335..4ee5eab63534 100644 --- a/lighthouse-core/test/audits/unsized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -19,9 +19,7 @@ describe('Sized images audit', () => { function runAudit(props) { const result = UnSizedImagesAudit.audit({ ImageElements: [ - generateImage( - props - ), + generateImage(props), ], }); return result;