diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap index ea4a32e6ffdc..d81f6ddb09f2 100644 --- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap +++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap @@ -84,6 +84,9 @@ Object { Object { "path": "themed-omnibox", }, + Object { + "path": "maskable-icon", + }, Object { "path": "content-width", }, @@ -1020,6 +1023,11 @@ Object { "id": "apple-touch-icon", "weight": 1, }, + Object { + "group": "pwa-optimized", + "id": "maskable-icon", + "weight": 1, + }, Object { "id": "pwa-cross-browser", "weight": 0, diff --git a/lighthouse-core/audits/maskable-icon.js b/lighthouse-core/audits/maskable-icon.js new file mode 100644 index 000000000000..2f2bde212636 --- /dev/null +++ b/lighthouse-core/audits/maskable-icon.js @@ -0,0 +1,70 @@ +/** + * @license Copyright 2020 Google Inc. 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 ManifestValues = require('../computed/manifest-values.js'); +const i18n = require('../lib/i18n/i18n.js'); + +const UIStrings = { + /** Title of a Lighthouse audit that provides detail on if the manifest contains a maskable icon. This descriptive title is shown to users when the manifest contains at least one maskable icon. */ + title: 'Manifest has a maskable icon', + /** Title of a Lighthouse audit that provides detial on if the manifest contains a maskable icon. this descriptive title is shown to users when the manifest contains no icons that are maskable. */ + failureTitle: 'Manifest doesn\'t have a maskable icon', + /** Description of a Lighthouse audit that tells the user why they their manifest should have at least one maskable icon. 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: 'A maskable icon ensures that the image fills the entire ' + + 'shape without being letterboxed when installing ' + + 'the app on a device. [Learn more](https://web.dev/maskable-icon/).', +}; + +const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); + +/** + * @fileoverview + * Audits if a manifest contains at least one icon that is maskable + * + * Requirements: + * * manifest is not empty + * * manifest has valid icons + * * at least one of the icons has a purpose of 'maskable' + */ + +class MaskableIcon extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'maskable-icon', + title: str_(UIStrings.title), + failureTitle: str_(UIStrings.failureTitle), + description: str_(UIStrings.description), + requiredArtifacts: ['WebAppManifest', 'InstallabilityErrors'], + }; + } + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async audit(artifacts, context) { + const manifestValues = await ManifestValues.request(artifacts, context); + if (manifestValues.isParseFailure) { + return { + score: 0, + explanation: manifestValues.parseFailureReason, + }; + } + const maskableIconCheck = manifestValues.allChecks.find(i => i.id === 'hasMaskableIcon'); + return { + score: (maskableIconCheck && maskableIconCheck.passing) ? 1 : 0, + }; + } +} + +module.exports = MaskableIcon; +module.exports.UIStrings = UIStrings; diff --git a/lighthouse-core/computed/manifest-values.js b/lighthouse-core/computed/manifest-values.js index a4ee39029bc3..5c316db6c8d0 100644 --- a/lighthouse-core/computed/manifest-values.js +++ b/lighthouse-core/computed/manifest-values.js @@ -86,6 +86,12 @@ class ManifestValues { failureText: 'Manifest does not have `name`', validate: manifestValue => !!manifestValue.name.value, }, + { + id: 'hasMaskableIcon', + failureText: 'Manifest does not have at least one icon that is maskable', + validate: ManifestValue => icons.doExist(ManifestValue) && + icons.containsMaskableIcon(ManifestValue), + }, ]; } diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js index 15eba06b4b20..057c277b8d8a 100644 --- a/lighthouse-core/config/default-config.js +++ b/lighthouse-core/config/default-config.js @@ -199,6 +199,7 @@ const defaultConfig = { 'apple-touch-icon', 'splash-screen', 'themed-omnibox', + 'maskable-icon', 'content-width', 'image-aspect-ratio', 'deprecations', @@ -559,6 +560,7 @@ const defaultConfig = { {id: 'viewport', weight: 2, group: 'pwa-optimized'}, {id: 'without-javascript', weight: 1, group: 'pwa-optimized'}, {id: 'apple-touch-icon', weight: 1, group: 'pwa-optimized'}, + {id: 'maskable-icon', weight: 1, group: 'pwa-optimized'}, // Manual audits {id: 'pwa-cross-browser', weight: 0}, {id: 'pwa-page-transitions', weight: 0}, diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json index b8c437e5f209..bf7ef5575eb6 100644 --- a/lighthouse-core/lib/i18n/locales/en-US.json +++ b/lighthouse-core/lib/i18n/locales/en-US.json @@ -836,6 +836,15 @@ "lighthouse-core/audits/manual/pwa-page-transitions.js | title": { "message": "Page transitions don't feel like they block on the network" }, + "lighthouse-core/audits/maskable-icon.js | description": { + "message": "A maskable icon ensures that the image fills the entire shape without being letterboxed when installing the app on a device. [Learn more](https://web.dev/maskable-icon/)." + }, + "lighthouse-core/audits/maskable-icon.js | failureTitle": { + "message": "Manifest doesn't have a maskable icon" + }, + "lighthouse-core/audits/maskable-icon.js | title": { + "message": "Manifest has a maskable icon" + }, "lighthouse-core/audits/metrics/estimated-input-latency.js | description": { "message": "Estimated Input Latency is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50 ms, users may perceive your app as laggy. [Learn more](https://web.dev/estimated-input-latency)." }, diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json index 0d27fdd2757f..244526b14e8f 100644 --- a/lighthouse-core/lib/i18n/locales/en-XL.json +++ b/lighthouse-core/lib/i18n/locales/en-XL.json @@ -836,6 +836,15 @@ "lighthouse-core/audits/manual/pwa-page-transitions.js | title": { "message": "P̂áĝé t̂ŕâńŝít̂íôńŝ d́ôń't̂ f́êél̂ ĺîḱê t́ĥéŷ b́l̂óĉḱ ôń t̂h́ê ńêt́ŵór̂ḱ" }, + "lighthouse-core/audits/maskable-icon.js | description": { + "message": "Â ḿâśk̂áb̂ĺê íĉón̂ én̂śûŕêś t̂h́ât́ t̂h́ê ím̂áĝé f̂íl̂ĺŝ t́ĥé êńt̂ír̂é ŝh́âṕê ẃît́ĥóût́ b̂éîńĝ ĺêt́t̂ér̂b́ôx́êd́ ŵh́êń îńŝt́âĺl̂ín̂ǵ t̂h́ê áp̂ṕ ôń â d́êv́îćê. [Ĺêár̂ń m̂ór̂é](https://web.dev/maskable-icon/)." + }, + "lighthouse-core/audits/maskable-icon.js | failureTitle": { + "message": "M̂án̂íf̂éŝt́ d̂óêśn̂'t́ ĥáv̂é â ḿâśk̂áb̂ĺê íĉón̂" + }, + "lighthouse-core/audits/maskable-icon.js | title": { + "message": "M̂án̂íf̂éŝt́ ĥáŝ á m̂áŝḱâb́l̂é îćôń" + }, "lighthouse-core/audits/metrics/estimated-input-latency.js | description": { "message": "Êśt̂ím̂át̂éd̂ Ín̂ṕût́ L̂át̂én̂ćŷ íŝ án̂ éŝt́îḿât́ê óf̂ h́ôẃ l̂ón̂ǵ ŷóûŕ âṕp̂ t́âḱêś t̂ó r̂éŝṕôńd̂ t́ô úŝér̂ ín̂ṕût́, îń m̂íl̂ĺîśêćôńd̂ś, d̂úr̂ín̂ǵ t̂h́ê b́ûśîéŝt́ 5ŝ ẃîńd̂óŵ óf̂ ṕâǵê ĺôád̂. Íf̂ ýôúr̂ ĺât́êńĉý îś ĥíĝh́êŕ t̂h́âń 50 m̂ś, ûśêŕŝ ḿâý p̂ér̂ćêív̂é ŷóûŕ âṕp̂ áŝ ĺâǵĝý. [L̂éâŕn̂ ḿôŕê](https://web.dev/estimated-input-latency)." }, diff --git a/lighthouse-core/lib/icons.js b/lighthouse-core/lib/icons.js index 548af0d3fd33..1ef00965e098 100644 --- a/lighthouse-core/lib/icons.js +++ b/lighthouse-core/lib/icons.js @@ -66,7 +66,21 @@ function pngSizedAtLeast(sizeRequirement, manifest) { }); } +/** + * @param {NonNullable} manifest + * @return {boolean} Does the manifest icons value contain at least one icon with purpose including "maskable" + */ +function containsMaskableIcon(manifest) { + const iconValues = manifest.icons.value; + return iconValues.some(icon => { + return icon.value.purpose && + icon.value.purpose.value && + icon.value.purpose.value.includes('maskable'); + }); +} + module.exports = { doExist, pngSizedAtLeast, + containsMaskableIcon, }; diff --git a/lighthouse-core/lib/manifest-parser.js b/lighthouse-core/lib/manifest-parser.js index 64d4fd06b52d..1732d27ded5d 100644 --- a/lighthouse-core/lib/manifest-parser.js +++ b/lighthouse-core/lib/manifest-parser.js @@ -242,6 +242,17 @@ function parseIcon(raw, manifestUrl) { const type = parseString(raw.type, true); + const parsedPurpose = parseString(raw.purpose); + const purpose = { + raw: raw.purpose, + value: ['any'], + /** @type {string|undefined} */ + warning: undefined, + }; + if (parsedPurpose.value !== undefined) { + purpose.value = parsedPurpose.value.split(/\s+/).map(value => value.toLowerCase()); + } + const density = { raw: raw.density, value: 1, @@ -278,6 +289,7 @@ function parseIcon(raw, manifestUrl) { type, density, sizes, + purpose, }, warning: undefined, }; diff --git a/lighthouse-core/test/audits/maskable-icon-test.js b/lighthouse-core/test/audits/maskable-icon-test.js new file mode 100644 index 000000000000..b1c151e86815 --- /dev/null +++ b/lighthouse-core/test/audits/maskable-icon-test.js @@ -0,0 +1,61 @@ +/** + * @license Copyright 2020 Google Inc. 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 MaskableIconAudit = require('../../audits/maskable-icon.js'); +const manifestParser = require('../../lib/manifest-parser.js'); + +const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); +const manifestWithoutMaskableSrc = + JSON.stringify(require('../fixtures/manifest-no-maskable-icon.json')); +const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; +const EXAMPLE_DOC_URL = 'https://example.com/index.html'; + +/** + * @param {string} + */ +function generateMockArtifacts(src = manifestSrc) { + const exampleManifest = manifestParser(src, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + + return { + WebAppManifest: exampleManifest, + InstallabilityErrors: {errors: []}, + }; +} + +function generateMockAuditContext() { + return { + computedCache: new Map(), + }; +} + +/* eslint-env jest */ + +describe('Maskable Icon Audit', () => { + const context = generateMockAuditContext(); + + it('fails when the manifest fails to be parsed', async () => { + const artifacts = generateMockArtifacts(); + artifacts.WebAppManifest = null; + + const auditResult = await MaskableIconAudit.audit(artifacts, context); + expect(auditResult.score).toEqual(0); + }); + + it('fails when the manifest contains no maskable icons', async () => { + const artifacts = generateMockArtifacts(manifestWithoutMaskableSrc); + + const auditResult = await MaskableIconAudit.audit(artifacts, context); + expect(auditResult.score).toEqual(0); + }); + + it('passes when the manifest contains at least one maskable icon', async () => { + const artifacts = generateMockArtifacts(); + + const auditResult = await MaskableIconAudit.audit(artifacts, context); + expect(auditResult.score).toEqual(1); + }); +}); diff --git a/lighthouse-core/test/computed/manifest-values-test.js b/lighthouse-core/test/computed/manifest-values-test.js index 54baf74a9272..9c8fa1a15d3c 100644 --- a/lighthouse-core/test/computed/manifest-values-test.js +++ b/lighthouse-core/test/computed/manifest-values-test.js @@ -252,5 +252,43 @@ describe('ManifestValues computed artifact', () => { assert.equal(iconResults.every(i => i.passing === false), true); }); }); + + describe('manifest has at least one maskable icon', () => { + it('fails when no maskable icon exists', async () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + purpose: 'any', + }], + }); + const WebAppManifest = noUrlManifestParser(manifestSrc); + const InstallabilityErrors = {errors: []}; + const artifacts = {WebAppManifest, InstallabilityErrors}; + + const results = await ManifestValues.request(artifacts, getMockContext()); + const iconResults = results.allChecks.filter(i => i.id.includes('Maskable')); + + assert.equal(iconResults.every(i => i.passing === false), true); + }); + + it('passes when an icon has the maskable purpose property', async () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + }, { + src: 'icon2.png', + purpose: 'maskable', + }], + }); + const WebAppManifest = noUrlManifestParser(manifestSrc); + const InstallabilityErrors = {errors: []}; + const artifacts = {WebAppManifest, InstallabilityErrors}; + + const results = await ManifestValues.request(artifacts, getMockContext()); + const iconResults = results.allChecks.filter(i => i.id.includes('Maskable')); + + assert.equal(iconResults.every(i => i.passing === true), true); + }); + }); }); }); diff --git a/lighthouse-core/test/fixtures/manifest-bom.json b/lighthouse-core/test/fixtures/manifest-bom.json index db33c9c71392..1569e848b86b 100644 --- a/lighthouse-core/test/fixtures/manifest-bom.json +++ b/lighthouse-core/test/fixtures/manifest-bom.json @@ -11,9 +11,10 @@ { "src": "/images/chrome-touch-icon-192x192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" }, - { + { "src": "/images/chrome-touch-icon-512x512.png", "sizes": "512x512", "type": "image/png" diff --git a/lighthouse-core/test/fixtures/manifest-no-maskable-icon.json b/lighthouse-core/test/fixtures/manifest-no-maskable-icon.json new file mode 100644 index 000000000000..a9c826641151 --- /dev/null +++ b/lighthouse-core/test/fixtures/manifest-no-maskable-icon.json @@ -0,0 +1,31 @@ +{ + "short_name": "ExApp", + "name": "Example App", + "start_url": "./", + "icons": [ + { + "src": "/images/chrome-touch-icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/images/chrome-touch-icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/images/chrome-touch-icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/images/chrome-touch-icon-384x384.png", + "sizes": "128x128 384x384", + "type": "image/png" + } + ], + "background_color": "#FAFAFA", + "theme_color": "#123123", + "display": "standalone", + "orientation": "portrait" +} diff --git a/lighthouse-core/test/fixtures/manifest.json b/lighthouse-core/test/fixtures/manifest.json index 750ac6d021cb..806cf89f28eb 100644 --- a/lighthouse-core/test/fixtures/manifest.json +++ b/lighthouse-core/test/fixtures/manifest.json @@ -11,9 +11,10 @@ { "src": "/images/chrome-touch-icon-192x192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" }, - { + { "src": "/images/chrome-touch-icon-512x512.png", "sizes": "512x512", "type": "image/png" diff --git a/lighthouse-core/test/lib/icons-test.js b/lighthouse-core/test/lib/icons-test.js index e70e0614e789..7b9efafd2f60 100644 --- a/lighthouse-core/test/lib/icons-test.js +++ b/lighthouse-core/test/lib/icons-test.js @@ -64,7 +64,8 @@ describe('Icons helper', () => { // value: { // src: { raw: 'icon.png', value: 'icon.png' }, // density: { raw: undefined, value: 1 }, - // sizes: { raw: '192x192', value: ['192x192'] } + // sizes: { raw: '192x192', value: ['192x192'] }, + // purpose: { raw: 'any', value: ['any'] } // } // }] // } @@ -328,4 +329,73 @@ describe('Icons helper', () => { assert.equal(icons.pngSizedAtLeast(192, manifest.value).length, 1); }); }); + + describe('icons at least one maskable check', () => { + it('succeeds when at least one icon has a purpose value of maskable', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + sizes: '200x200', + type: 'image/png', + purpose: 'any', + }, { + src: 'icon-vector.svg', + sizes: '100x100', + purpose: 'maskable', + }], + }); + const manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + assert.equal(icons.containsMaskableIcon(manifest.value), true); + }); + + it('succeeds when multiple icons have a purpose value of maskable', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + sizes: '200x200', + type: 'image/png', + purpose: 'maskable', + }, { + src: 'icon-vector.svg', + sizes: '100x100', + purpose: 'maskable', + }], + }); + const manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + assert.equal(icons.containsMaskableIcon(manifest.value), true); + }); + + it('succeeds when an icon has multiple purpose values, including maskable', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + sizes: '200x200', + type: 'image/png', + purpose: 'any Maskable', + }, { + src: 'icon-vector.svg', + sizes: '100x100', + purpose: 'any', + }], + }); + const manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + assert.equal(icons.containsMaskableIcon(manifest.value), true); + }); + + it('fails when no icons have a purpose value of maskable', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + sizes: '200x200', + type: 'image/png', + purpose: 'any', + }, { + src: 'icon-vector.svg', + sizes: '100x100', + }], + }); + const manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + assert.equal(icons.containsMaskableIcon(manifest.value), false); + }); + }); }); diff --git a/lighthouse-core/test/report/html/renderer/category-renderer-test.js b/lighthouse-core/test/report/html/renderer/category-renderer-test.js index 4bba4bcc556b..72811c57203d 100644 --- a/lighthouse-core/test/report/html/renderer/category-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/category-renderer-test.js @@ -365,7 +365,7 @@ describe('CategoryRenderer', () => { const manualAudits = elem.querySelectorAll('.lh-clump--manual .lh-audit'); assert.equal(passedAudits.length, 2); - assert.equal(failedAudits.length, 9); + assert.equal(failedAudits.length, 10); assert.equal(warningAudits.length, 2); assert.equal(manualAudits.length, 3); }); @@ -379,7 +379,7 @@ describe('CategoryRenderer', () => { const failedAudits = elem.querySelectorAll('.lh-clump--failed .lh-audit'); assert.equal(passedAudits.length, 0); - assert.equal(failedAudits.length, 13); + assert.equal(failedAudits.length, 14); }); it('expands warning audit group', () => { diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index 625d696a0b78..d8cdaad698af 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -555,6 +555,14 @@ ] } }, + "maskable-icon": { + "id": "maskable-icon", + "title": "Manifest doesn't have a maskable icon", + "description": "A maskable icon ensures that the image fills the entire shape without being letterboxed when installing the app on a device. [Learn more](https://web.dev/maskable-icon/).", + "score": 0, + "scoreDisplayMode": "binary", + "explanation": "No manifest was fetched" + }, "content-width": { "id": "content-width", "title": "Content is sized correctly for the viewport", @@ -4292,6 +4300,11 @@ "weight": 1, "group": "pwa-optimized" }, + { + "id": "maskable-icon", + "weight": 1, + "group": "pwa-optimized" + }, { "id": "pwa-cross-browser", "weight": 0 @@ -4306,7 +4319,7 @@ } ], "id": "pwa", - "score": 0.41 + "score": 0.39 } }, "categoryGroups": { @@ -4735,6 +4748,12 @@ "duration": 100, "entryType": "measure" }, + { + "startTime": 0, + "name": "lh:audit:maskable-icon", + "duration": 100, + "entryType": "measure" + }, { "startTime": 0, "name": "lh:audit:content-width", @@ -5779,6 +5798,12 @@ "lighthouse-core/audits/themed-omnibox.js | description": [ "audits[themed-omnibox].description" ], + "lighthouse-core/audits/maskable-icon.js | failureTitle": [ + "audits[maskable-icon].title" + ], + "lighthouse-core/audits/maskable-icon.js | description": [ + "audits[maskable-icon].description" + ], "lighthouse-core/audits/content-width.js | title": [ "audits[content-width].title" ], diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json index cb3526ed5c86..1da1a17d3340 100644 --- a/proto/sample_v2_round_trip.json +++ b/proto/sample_v2_round_trip.json @@ -1557,6 +1557,14 @@ "scoreDisplayMode": "manual", "title": "The user's focus is directed to new content added to the page" }, + "maskable-icon": { + "description": "A maskable icon ensures that the image fills the entire shape without being letterboxed when installing the app on a device. [Learn more](https://web.dev/maskable-icon/).", + "explanation": "No manifest was fetched", + "id": "maskable-icon", + "score": 0.0, + "scoreDisplayMode": "binary", + "title": "Manifest doesn't have a maskable icon" + }, "max-potential-fid": { "description": "The maximum potential First Input Delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid).", "displayValue": "120\u00a0ms", @@ -4040,6 +4048,11 @@ "id": "apple-touch-icon", "weight": 1.0 }, + { + "group": "pwa-optimized", + "id": "maskable-icon", + "weight": 1.0 + }, { "id": "pwa-cross-browser", "weight": 0.0 @@ -4056,7 +4069,7 @@ "description": "These checks validate the aspects of a Progressive Web App. [Learn more](https://developers.google.com/web/progressive-web-apps/checklist).", "id": "pwa", "manualDescription": "These checks are required by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist) but are not automatically checked by Lighthouse. They do not affect your score but it's important that you verify them manually.", - "score": 0.41, + "score": 0.39, "title": "Progressive Web App" }, "seo": { @@ -4650,6 +4663,12 @@ "name": "lh:computed:ManifestValues", "startTime": 0.0 }, + { + "duration": 100.0, + "entryType": "measure", + "name": "lh:audit:maskable-icon", + "startTime": 0.0 + }, { "duration": 100.0, "entryType": "measure", diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index cdd8130913d4..699c286b7ca9 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -464,7 +464,7 @@ declare global { } } - export type ManifestValueCheckID = 'hasStartUrl'|'hasIconsAtLeast144px'|'hasIconsAtLeast512px'|'fetchesIcon'|'hasPWADisplayValue'|'hasBackgroundColor'|'hasThemeColor'|'hasShortName'|'hasName'|'shortNameLength'; + export type ManifestValueCheckID = 'hasStartUrl'|'hasIconsAtLeast144px'|'hasIconsAtLeast512px'|'fetchesIcon'|'hasPWADisplayValue'|'hasBackgroundColor'|'hasThemeColor'|'hasShortName'|'hasName'|'shortNameLength'|'hasMaskableIcon'; export type ManifestValues = { isParseFailure: false;