diff --git a/lighthouse-cli/test/fixtures/launcher-icon-100x100.png b/lighthouse-cli/test/fixtures/launcher-icon-100x100.png new file mode 100644 index 000000000000..72365bbf3092 Binary files /dev/null and b/lighthouse-cli/test/fixtures/launcher-icon-100x100.png differ diff --git a/lighthouse-cli/test/fixtures/perf/unsized-images.html b/lighthouse-cli/test/fixtures/perf/unsized-images.html new file mode 100644 index 000000000000..6c73fd38dbdc --- /dev/null +++ b/lighthouse-cli/test/fixtures/perf/unsized-images.html @@ -0,0 +1,24 @@ + + +
+ + + + + + + + + + + + + + + + + diff --git a/lighthouse-cli/test/smokehouse/test-definitions/perf-diagnostics/expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/perf-diagnostics/expectations.js index 50066408f67d..c92d4d30e8c2 100644 --- a/lighthouse-cli/test/smokehouse/test-definitions/perf-diagnostics/expectations.js +++ b/lighthouse-cli/test/smokehouse/test-definitions/perf-diagnostics/expectations.js @@ -81,4 +81,49 @@ module.exports = [ }, }, }, + { + lhr: { + requestedUrl: 'http://localhost:10200/perf/unsized-images.html', + finalUrl: 'http://localhost:10200/perf/unsized-images.html', + audits: { + 'unsized-images': { + score: 0, + details: { + items: [ + { + node: { + snippet: '', + }, + }, + { + node: { + snippet: '', + }, + }, + { + node: { + snippet: '', + }, + }, + { + node: { + snippet: '', + }, + }, + { + node: { + snippet: '', + }, + }, + { + node: { + snippet: '', + }, + }, + ], + }, + }, + }, + }, + }, ]; diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js index 1306665c65c7..8613ecc5d2bb 100644 --- a/lighthouse-core/audits/unsized-images.js +++ b/lighthouse-core/audits/unsized-images.js @@ -64,9 +64,9 @@ class UnsizedImages extends Audit { * @param {string | null} property * @return {boolean} */ - static doesCssPropProvideExplicitSize(property) { + static isCssPropExplicitlySet(property) { if (!property) return false; - return property !== 'auto'; + return !['auto', 'initial', 'unset', 'inherit'].includes(property); } /** @@ -86,13 +86,17 @@ class UnsizedImages extends Audit { const attrHeight = image.attributeHeight; const cssWidth = image._privateCssSizing.width; const cssHeight = image._privateCssSizing.height; + const cssAspectRatio = image._privateCssSizing.aspectRatio; const htmlWidthIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrWidth); - const cssWidthIsExplicit = UnsizedImages.doesCssPropProvideExplicitSize(cssWidth); + const cssWidthIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssWidth); const htmlHeightIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrHeight); - const cssHeightIsExplicit = UnsizedImages.doesCssPropProvideExplicitSize(cssHeight); + const cssHeightIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssHeight); + const explicitAspectRatio = UnsizedImages.isCssPropExplicitlySet(cssAspectRatio); const explicitWidth = htmlWidthIsExplicit || cssWidthIsExplicit; const explicitHeight = htmlHeightIsExplicit || cssHeightIsExplicit; - return explicitWidth && explicitHeight; + return (explicitWidth && explicitHeight) || + (explicitWidth && explicitAspectRatio) || + (explicitHeight && explicitAspectRatio); } /** diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index e8970c122bed..4d999374781f 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -272,10 +272,11 @@ class ImageElements extends Gatherer { }); const width = getEffectiveSizingRule(matchedRules, 'width'); const height = getEffectiveSizingRule(matchedRules, 'height'); + const aspectRatio = getEffectiveSizingRule(matchedRules, 'aspect-ratio'); // COMPAT: Maintain backcompat for <= 7.0.1 element.cssWidth = width === null ? undefined : width; element.cssHeight = height === null ? undefined : height; - element._privateCssSizing = {width, height}; + element._privateCssSizing = {width, height, aspectRatio}; } catch (err) { if (/No node.*found/.test(err.message)) return; throw err; diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js index 769b6df19bc8..a5879fb21b62 100644 --- a/lighthouse-core/test/audits/unsized-images-test.js +++ b/lighthouse-core/test/audits/unsized-images-test.js @@ -477,6 +477,60 @@ describe('Sized images audit', () => { }); }); + describe('has defined aspect-ratio', () => { + it('fails when an image only has explicit CSS aspect-ratio', async () => { + const result = await runAudit({ + attributeWidth: '', + attributeHeight: '', + _privateCssSizing: { + width: null, + height: null, + aspectRatio: '1 / 1', + }, + }); + expect(result.score).toEqual(0); + }); + + it('fails when an image only has non-explicit CSS aspect-ratio', async () => { + const result = await runAudit({ + attributeWidth: '100', + attributeHeight: '', + _privateCssSizing: { + width: null, + height: null, + aspectRatio: 'auto', + }, + }); + expect(result.score).toEqual(0); + }); + + it('passes when CSS aspect-ratio and attribute width are explicit', async () => { + const result = await runAudit({ + attributeWidth: '100', + attributeHeight: '', + _privateCssSizing: { + width: null, + height: null, + aspectRatio: '1 / 1', + }, + }); + expect(result.score).toEqual(1); + }); + + it('passes when CSS aspect-ratio and width are explicit', async () => { + const result = await runAudit({ + attributeWidth: '', + attributeHeight: '', + _privateCssSizing: { + width: '100', + height: null, + aspectRatio: '1 / 1', + }, + }); + expect(result.score).toEqual(1); + }); + }); + it('is not applicable when there are no images', async () => { const result = await UnsizedImagesAudit.audit({ ImageElements: [], @@ -578,28 +632,31 @@ describe('html attribute sized check', () => { describe('CSS property sized check', () => { it('fails if it was never defined', () => { - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize(undefined)).toEqual(false); + expect(UnsizedImagesAudit.isCssPropExplicitlySet(undefined)).toEqual(false); }); it('fails if it is empty', () => { - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('')).toEqual(false); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('')).toEqual(false); }); - it('fails if it is auto', () => { - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('auto')).toEqual(false); + it('fails if it is not explicit', () => { + expect(UnsizedImagesAudit.isCssPropExplicitlySet('auto')).toEqual(false); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('inherit')).toEqual(false); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('unset')).toEqual(false); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('initial')).toEqual(false); }); - it('passes if it is defined and not auto', () => { - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('200')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('300.5')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('150px')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('80%')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('5cm')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('20rem')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('7vw')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('-20')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('0')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('three')).toEqual(true); - expect(UnsizedImagesAudit.doesCssPropProvideExplicitSize('-20')).toEqual(true); + it('passes if it is defined and explicit', () => { + expect(UnsizedImagesAudit.isCssPropExplicitlySet('200')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('300.5')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('150px')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('80%')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('5cm')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('20rem')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('7vw')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('-20')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('0')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('three')).toEqual(true); + expect(UnsizedImagesAudit.isCssPropExplicitlySet('-20')).toEqual(true); }); }); diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index b56c933d9a60..073da753fe78 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -476,6 +476,8 @@ declare global { width: string | null; /** The height of the image as expressed by CSS rules. Set to `null` if there was no height set in CSS. */ height: string | null; + /** The aspect ratio of the image as expressed by CSS rules. Set to `null` if there was no aspect ratio set in CSS. */ + aspectRatio: string | null; } /** The BoundingClientRect of the element. */ clientRect: {