From 503496138a3e3cb9b2106097d71c3f5274e8a515 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Mon, 27 Mar 2017 17:04:47 -0700 Subject: [PATCH] Collapse the 9 manifest PWA audits into 3 (#1847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces 3 new audits: webapp-install-banner (née _add to homescreen_), themed-omnibox, splash-screen. These audits subsume and deprecate the following audits: theme-color-meta, manifest-{background-color, display, exists, icons-min-{144,192}, name, start_url, theme-color}. A new manifestValue computed artifact is introduced and used as helper for the 3 audits. --- .../offline-local/offline-expectations.js | 60 +----- lighthouse-cli/test/smokehouse/pwa-config.js | 13 +- .../test/smokehouse/pwa-expectations.js | 120 ++--------- .../audits/manifest-background-color.js | 64 ------ lighthouse-core/audits/manifest-display.js | 73 ------- lighthouse-core/audits/manifest-exists.js | 57 ----- .../audits/manifest-icons-min-144.js | 74 ------- .../audits/manifest-icons-min-192.js | 76 ------- lighthouse-core/audits/manifest-name.js | 56 ----- .../audits/manifest-short-name-length.js | 59 +++-- lighthouse-core/audits/manifest-short-name.js | 58 ----- lighthouse-core/audits/manifest-start-url.js | 57 ----- .../audits/manifest-theme-color.js | 57 ----- lighthouse-core/audits/multi-check-audit.js | 59 +++++ lighthouse-core/audits/service-worker.js | 20 +- lighthouse-core/audits/splash-screen.js | 76 +++++++ lighthouse-core/audits/theme-color-meta.js | 62 ------ lighthouse-core/audits/themed-omnibox.js | 73 +++++++ .../audits/webapp-install-banner.js | 92 ++++++++ lighthouse-core/config/default.json | 68 +----- .../gather/computed/manifest-values.js | 119 ++++++++++ lighthouse-core/runner.js | 1 + .../audits/manifest-background-color-test.js | 95 -------- .../test/audits/manifest-display-test.js | 73 ------- .../test/audits/manifest-exists-test.js | 83 ------- .../test/audits/manifest-icons-test.js | 139 ------------ .../test/audits/manifest-name-test.js | 66 ------ .../audits/manifest-short-name-length-test.js | 56 +++-- .../test/audits/manifest-short-name-test.js | 79 ------- .../test/audits/manifest-start-url-test.js | 75 ------- .../test/audits/manifest-theme-color-test.js | 76 ------- .../test/audits/splash-screen-test.js | 131 +++++++++++ .../test/audits/theme-color-meta-test.js | 53 ----- .../test/audits/themed-omnibox-test.js | 141 ++++++++++++ .../test/audits/webapp-install-banner-test.js | 139 ++++++++++++ lighthouse-core/test/fixtures/manifest.json | 5 + .../gather/computed/manifest-values-test.js | 203 ++++++++++++++++++ .../test/lib/manifest-parser-test.js | 8 +- 38 files changed, 1147 insertions(+), 1669 deletions(-) delete mode 100644 lighthouse-core/audits/manifest-background-color.js delete mode 100644 lighthouse-core/audits/manifest-display.js delete mode 100644 lighthouse-core/audits/manifest-exists.js delete mode 100644 lighthouse-core/audits/manifest-icons-min-144.js delete mode 100644 lighthouse-core/audits/manifest-icons-min-192.js delete mode 100644 lighthouse-core/audits/manifest-name.js delete mode 100644 lighthouse-core/audits/manifest-short-name.js delete mode 100644 lighthouse-core/audits/manifest-start-url.js delete mode 100644 lighthouse-core/audits/manifest-theme-color.js create mode 100644 lighthouse-core/audits/multi-check-audit.js create mode 100644 lighthouse-core/audits/splash-screen.js delete mode 100644 lighthouse-core/audits/theme-color-meta.js create mode 100644 lighthouse-core/audits/themed-omnibox.js create mode 100644 lighthouse-core/audits/webapp-install-banner.js create mode 100644 lighthouse-core/gather/computed/manifest-values.js delete mode 100644 lighthouse-core/test/audits/manifest-background-color-test.js delete mode 100644 lighthouse-core/test/audits/manifest-display-test.js delete mode 100644 lighthouse-core/test/audits/manifest-exists-test.js delete mode 100644 lighthouse-core/test/audits/manifest-icons-test.js delete mode 100644 lighthouse-core/test/audits/manifest-name-test.js delete mode 100644 lighthouse-core/test/audits/manifest-short-name-test.js delete mode 100644 lighthouse-core/test/audits/manifest-start-url-test.js delete mode 100644 lighthouse-core/test/audits/manifest-theme-color-test.js create mode 100644 lighthouse-core/test/audits/splash-screen-test.js delete mode 100644 lighthouse-core/test/audits/theme-color-meta-test.js create mode 100644 lighthouse-core/test/audits/themed-omnibox-test.js create mode 100644 lighthouse-core/test/audits/webapp-install-banner-test.js create mode 100644 lighthouse-core/test/gather/computed/manifest-values-test.js diff --git a/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js b/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js index 00f722dbebd8..df3e0130ac2f 100644 --- a/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js +++ b/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js @@ -41,9 +41,6 @@ module.exports = [ 'viewport': { score: true }, - 'manifest-display': { - score: false - }, 'without-javascript': { score: true }, @@ -55,34 +52,13 @@ module.exports = [ score: true, displayValue: '0' }, - 'manifest-exists': { - score: false - }, - 'manifest-background-color': { - score: false - }, - 'manifest-theme-color': { - score: false - }, - 'manifest-icons-min-192': { - score: false - }, - 'manifest-icons-min-144': { - score: false - }, - 'manifest-name': { - score: false - }, - 'manifest-short-name': { - score: false - }, - 'manifest-short-name-length': { + 'webapp-install-banner': { score: false }, - 'manifest-start-url': { + 'splash-screen': { score: false }, - 'theme-color-meta': { + 'themed-omnibox': { score: false }, 'aria-valid-attr': { @@ -128,9 +104,6 @@ module.exports = [ 'viewport': { score: true }, - 'manifest-display': { - score: false - }, 'without-javascript': { score: true }, @@ -142,34 +115,13 @@ module.exports = [ score: false, displayValue: '1' }, - 'manifest-exists': { - score: false - }, - 'manifest-background-color': { - score: false - }, - 'manifest-theme-color': { - score: false - }, - 'manifest-icons-min-192': { - score: false - }, - 'manifest-icons-min-144': { - score: false - }, - 'manifest-name': { - score: false - }, - 'manifest-short-name': { - score: false - }, - 'manifest-short-name-length': { + 'webapp-install-banner': { score: false }, - 'manifest-start-url': { + 'splash-screen': { score: false }, - 'theme-color-meta': { + 'themed-omnibox': { score: false }, 'aria-valid-attr': { diff --git a/lighthouse-cli/test/smokehouse/pwa-config.js b/lighthouse-cli/test/smokehouse/pwa-config.js index 0f90664acf6c..e17d739f355d 100644 --- a/lighthouse-cli/test/smokehouse/pwa-config.js +++ b/lighthouse-cli/test/smokehouse/pwa-config.js @@ -26,6 +26,7 @@ module.exports = { gatherers: [ 'url', 'https', + 'theme-color', 'manifest', // https://github.com/GoogleChrome/lighthouse/issues/566 // 'cache-contents' @@ -52,15 +53,9 @@ module.exports = { 'redirects-http', 'service-worker', 'works-offline', - 'manifest-display', - 'manifest-exists', - 'manifest-background-color', - 'manifest-theme-color', - 'manifest-icons-min-192', - 'manifest-icons-min-144', - 'manifest-name', - 'manifest-short-name', - 'manifest-start-url', + 'webapp-install-banner', + 'splash-screen', + 'themed-omnibox', // https://github.com/GoogleChrome/lighthouse/issues/566 // 'cache-start-url' ], diff --git a/lighthouse-cli/test/smokehouse/pwa-expectations.js b/lighthouse-cli/test/smokehouse/pwa-expectations.js index aec232e57afd..bd62e9f3712e 100644 --- a/lighthouse-cli/test/smokehouse/pwa-expectations.js +++ b/lighthouse-cli/test/smokehouse/pwa-expectations.js @@ -37,35 +37,13 @@ module.exports = [ 'works-offline': { score: true }, - 'manifest-display': { - score: true, - displayValue: 'standalone' - }, - 'manifest-exists': { + 'webapp-install-banner': { score: true }, - 'manifest-background-color': { - score: true, - extendedInfo: { - value: '#2196F3' - } - }, - 'manifest-theme-color': { + 'splash-screen': { score: true }, - 'manifest-icons-min-192': { - score: true - }, - 'manifest-icons-min-144': { - score: true - }, - 'manifest-name': { - score: true - }, - 'manifest-short-name': { - score: true - }, - 'manifest-start-url': { + 'themed-omnibox': { score: true }, // 'cache-start-url': { @@ -99,35 +77,13 @@ module.exports = [ 'works-offline': { score: false }, - 'manifest-display': { - score: true, - displayValue: 'standalone' - }, - 'manifest-exists': { + 'webapp-install-banner': { score: true }, - 'manifest-background-color': { - score: true, - extendedInfo: { - value: '#366597' - } - }, - 'manifest-theme-color': { - score: true - }, - 'manifest-icons-min-192': { - score: true - }, - 'manifest-icons-min-144': { + 'splash-screen': { score: true }, - 'manifest-name': { - score: true - }, - 'manifest-short-name': { - score: true - }, - 'manifest-start-url': { + 'themed-omnibox': { score: true }, // 'cache-start-url': { @@ -152,37 +108,13 @@ module.exports = [ 'works-offline': { score: true }, - 'manifest-display': { - score: true, - displayValue: 'standalone' - }, - 'manifest-exists': { - score: true - }, - 'manifest-background-color': { - score: true, - extendedInfo: { - value: '#bababa' - } - }, - 'manifest-theme-color': { - score: true - }, - 'manifest-icons-min-192': { - score: true, - displayValue: 'found sizes: 600x600', - }, - 'manifest-icons-min-144': { - score: true, - displayValue: 'found sizes: 600x600', - }, - 'manifest-name': { - score: true + 'webapp-install-banner': { + score: false }, - 'manifest-short-name': { + 'splash-screen': { score: true }, - 'manifest-start-url': { + 'themed-omnibox': { score: true }, // 'cache-start-url': { @@ -260,36 +192,14 @@ module.exports = [ 'works-offline': { score: true }, - 'manifest-display': { - score: true, - displayValue: 'standalone' - }, - 'manifest-exists': { - score: true - }, - 'manifest-background-color': { - score: true, - extendedInfo: { - value: '#383838' - } - }, - 'manifest-theme-color': { - score: true - }, - 'manifest-icons-min-192': { - score: true - }, - 'manifest-icons-min-144': { + 'webapp-install-banner': { score: true }, - 'manifest-name': { - score: true - }, - 'manifest-short-name': { - score: true + 'splash-screen': { + score: false }, - 'manifest-start-url': { - score: true + 'themed-omnibox': { + score: false }, // 'cache-start-url': { // score: true diff --git a/lighthouse-core/audits/manifest-background-color.js b/lighthouse-core/audits/manifest-background-color.js deleted file mode 100644 index 05c0b57fc52e..000000000000 --- a/lighthouse-core/audits/manifest-background-color.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright 2016 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'); -const Formatter = require('../report/formatter'); - -class ManifestBackgroundColor extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-background-color', - description: 'Manifest contains `background_color`', - helpText: 'When your app launches from a user\'s homescreen, the browser ' + - 'uses `background_color` to paint the background of the browser ' + - 'while your app loads for a smooth transition experience. ' + - '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-background_color).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false, - }; - } - - const manifest = artifacts.Manifest.value; - const bgColor = manifest.background_color.value; - return { - rawValue: !!bgColor, - extendedInfo: { - value: bgColor, - formatter: Formatter.SUPPORTED_FORMATS.NULL - } - }; - } -} - -module.exports = ManifestBackgroundColor; diff --git a/lighthouse-core/audits/manifest-display.js b/lighthouse-core/audits/manifest-display.js deleted file mode 100644 index 95c7dddb23e1..000000000000 --- a/lighthouse-core/audits/manifest-display.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2016 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'); - -class ManifestDisplay extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-display', - description: 'Manifest\'s `display` property is set', - helpText: 'Set the `display` property to specify how your app ' + - 'launches from the homescreen. [Learn ' + - 'more](https://developers.google.com/web/tools/lighthouse/audits/manifest-has-display-set).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {string|undefined} val - * @return {boolean} - */ - static hasRecommendedValue(val) { - return ['browser', 'fullscreen', 'minimal-ui', 'standalone'].includes(val); - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - const displayValue = manifest.display.value; - const hasRecommendedValue = ManifestDisplay.hasRecommendedValue(displayValue); - - const auditResult = { - rawValue: hasRecommendedValue, - displayValue - }; - if (!hasRecommendedValue) { - auditResult.debugString = 'Manifest display property should be set.'; - } - return auditResult; - } -} - -module.exports = ManifestDisplay; diff --git a/lighthouse-core/audits/manifest-exists.js b/lighthouse-core/audits/manifest-exists.js deleted file mode 100644 index 258875002d05..000000000000 --- a/lighthouse-core/audits/manifest-exists.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2016 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'); - -class ManifestExists extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-exists', - description: 'Manifest exists', - helpText: 'The web app manifest is the technology that enables users ' + - 'to add your web app to their homescreen. [Learn ' + - 'more](https://developers.google.com/web/tools/lighthouse/audits/manifest-exists).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest) { - // Page has no manifest. - return { - rawValue: false - }; - } - - return { - rawValue: typeof artifacts.Manifest.value !== 'undefined', - debugString: artifacts.Manifest.debugString - }; - } -} - -module.exports = ManifestExists; diff --git a/lighthouse-core/audits/manifest-icons-min-144.js b/lighthouse-core/audits/manifest-icons-min-144.js deleted file mode 100644 index f3120efa963a..000000000000 --- a/lighthouse-core/audits/manifest-icons-min-144.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @license - * Copyright 2016 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'); -const icons = require('../lib/icons'); - -class ManifestIconsMin144 extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-icons-min-144', - description: 'Manifest contains icons at least 144px', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - if (icons.doExist(manifest) === false) { - return { - rawValue: false - }; - } - - const matchingIcons = icons.sizeAtLeast(144, /** @type {!Manifest} */ (manifest)); - - let displayValue; - let debugString; - if (matchingIcons.length) { - displayValue = `found sizes: ${matchingIcons.join(', ')}`; - } else { - debugString = 'No icons are at least 144px'; - } - - return { - rawValue: !!matchingIcons.length, - displayValue, - debugString - }; - } -} - -module.exports = ManifestIconsMin144; - diff --git a/lighthouse-core/audits/manifest-icons-min-192.js b/lighthouse-core/audits/manifest-icons-min-192.js deleted file mode 100644 index e9cc0b26d88c..000000000000 --- a/lighthouse-core/audits/manifest-icons-min-192.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * Copyright 2016 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'); -const icons = require('../lib/icons'); - -class ManifestIconsMin192 extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-icons-min-192', - description: 'Manifest contains icons at least 192px', - helpText: 'A 192px icon ensures that your app\'s icon displays well ' + - 'on the homescreens of the largest mobile devices. [Learn ' + - 'more](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-192px-icon).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - if (icons.doExist(manifest) === false) { - return { - rawValue: false - }; - } - - const matchingIcons = icons.sizeAtLeast(192, /** @type {!Manifest} */ (manifest)); - - let displayValue; - let debugString; - if (matchingIcons.length) { - displayValue = `found sizes: ${matchingIcons.join(', ')}`; - } else { - debugString = 'No icons are at least 192px'; - } - - return { - rawValue: !!matchingIcons.length, - displayValue, - debugString - }; - } -} - -module.exports = ManifestIconsMin192; diff --git a/lighthouse-core/audits/manifest-name.js b/lighthouse-core/audits/manifest-name.js deleted file mode 100644 index 51b21654c6b2..000000000000 --- a/lighthouse-core/audits/manifest-name.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license - * Copyright 2016 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'); - -class ManifestName extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-name', - description: 'Manifest contains `name`', - helpText: 'The `name` property identifies your app and is required. ' + - '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-name).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - return { - rawValue: !!manifest.name.value - }; - } -} - -module.exports = ManifestName; diff --git a/lighthouse-core/audits/manifest-short-name-length.js b/lighthouse-core/audits/manifest-short-name-length.js index e18bc93992eb..0adf1d404ec3 100644 --- a/lighthouse-core/audits/manifest-short-name-length.js +++ b/lighthouse-core/audits/manifest-short-name-length.js @@ -35,45 +35,44 @@ class ManifestShortNameLength extends Audit { }; } + + static assessManifest(manifestValues, failures) { + if (manifestValues.isParseFailure) { + failures.push(manifestValues.parseFailureReason); + return; + } + + const themeColorCheck = manifestValues.allChecks.find(i => i.id === 'hasThemeColor'); + if (!themeColorCheck.passing) { + failures.push(themeColorCheck.failureText); + } + } + /** * @param {!Artifacts} artifacts * @return {!AuditResult} */ static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } + return artifacts.requestManifestValues(artifacts.Manifest).then(manifestValues => { + if (manifestValues.isParseFailure) { + return { + rawValue: false + }; + } - // When no shortname can be found we look for a name. - const manifest = artifacts.Manifest.value; - const shortNameValue = manifest.short_name.value || manifest.name.value; + const hasShortName = manifestValues.allChecks.find(i => i.id === 'hasShortName').passing; + if (!hasShortName) { + return { + rawValue: false, + debugString: 'No short_name found in manifest.' + }; + } - if (!shortNameValue) { + const isShortEnough = manifestValues.allChecks.find(i => i.id === 'shortNameLength').passing; return { - rawValue: false, - debugString: 'No short_name found in manifest.' + rawValue: isShortEnough }; - } - - // Historically, Chrome recommended 12 chars as the maximum length to prevent truncation. - // See #69 for more discussion. - // https://developer.chrome.com/apps/manifest/name#short_name - const suggestedLength = 12; - const isShortNameShortEnough = shortNameValue.length <= suggestedLength; - - let debugString; - if (!isShortNameShortEnough) { - debugString = `${suggestedLength} chars is the suggested maximum homescreen label length`; - debugString += ` (Found: ${shortNameValue.length} chars).`; - } - - return { - rawValue: isShortNameShortEnough, - debugString - }; + }); } } diff --git a/lighthouse-core/audits/manifest-short-name.js b/lighthouse-core/audits/manifest-short-name.js deleted file mode 100644 index 682f9c73231e..000000000000 --- a/lighthouse-core/audits/manifest-short-name.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2016 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'); - -class ManifestShortName extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-short-name', - description: 'Manifest contains `short_name`', - helpText: 'The `short_name` property is a requirement for Add ' + - 'To Homescreen. [Learn ' + - 'more](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-short_name).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - return { - // When no shortname can be found we look for a name. - rawValue: !!(manifest.short_name.value || manifest.name.value) - }; - } -} - -module.exports = ManifestShortName; diff --git a/lighthouse-core/audits/manifest-start-url.js b/lighthouse-core/audits/manifest-start-url.js deleted file mode 100644 index e78ad9d67afa..000000000000 --- a/lighthouse-core/audits/manifest-start-url.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2016 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'); - -class ManifestStartUrl extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-start-url', - description: 'Manifest contains `start_url`', - helpText: 'Add a `start_url` to instruct the browser to launch a ' + - 'specific URL whenever your app is launched from a homescreen. ' + - '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-start_url).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - return { - rawValue: !!manifest.start_url.value - }; - } -} - -module.exports = ManifestStartUrl; diff --git a/lighthouse-core/audits/manifest-theme-color.js b/lighthouse-core/audits/manifest-theme-color.js deleted file mode 100644 index 18b15decc85c..000000000000 --- a/lighthouse-core/audits/manifest-theme-color.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2016 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'); - -class ManifestThemeColor extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'Manifest', - name: 'manifest-theme-color', - description: 'Manifest contains `theme_color`', - helpText: 'Add a `theme_color` to set the color of the browser\'s ' + - 'address bar. [Learn ' + - 'more](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-theme_color).', - requiredArtifacts: ['Manifest'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - if (!artifacts.Manifest || !artifacts.Manifest.value) { - // Page has no manifest or was invalid JSON. - return { - rawValue: false - }; - } - - const manifest = artifacts.Manifest.value; - return { - rawValue: !!manifest.theme_color.value - }; - } -} - -module.exports = ManifestThemeColor; diff --git a/lighthouse-core/audits/multi-check-audit.js b/lighthouse-core/audits/multi-check-audit.js new file mode 100644 index 000000000000..83a02ef338da --- /dev/null +++ b/lighthouse-core/audits/multi-check-audit.js @@ -0,0 +1,59 @@ +/** + * @license Copyright 2017 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'; + +/** + * @fileoverview Base class for boolean audits that can have multiple reasons for failure + */ + +const Audit = require('./audit'); +const Formatter = require('../report/formatter'); + +class MultiCheckAudit extends Audit { + /** + * @param {!Artifacts} artifacts + * @return {!AuditResult} + */ + static audit(artifacts) { + return Promise.resolve(this.audit_(artifacts)).then(result => this.createAuditResult(result)); + } + + /** + * @param {!{failures: !Array, themeColor: ?string, manifestValues: ?Object, }} result + * @return {!AuditResult} + */ + static createAuditResult(result) { + const extendedInfo = { + value: result, + formatter: Formatter.SUPPORTED_FORMATS.NULL + }; + + // If we fail, share the failures + if (result.failures.length > 0) { + return { + rawValue: false, + debugString: `Failures: ${result.failures.join(', ')}.`, + extendedInfo + }; + } + + // Otherwise, we pass + return { + rawValue: true, + extendedInfo + }; + } + + /** + * @param {!Artifacts} artifacts + */ + static audit_() { + throw new Error('audit_ unimplemented'); + } +} + +module.exports = MultiCheckAudit; diff --git a/lighthouse-core/audits/service-worker.js b/lighthouse-core/audits/service-worker.js index e923575b5e88..72052a94e9e9 100644 --- a/lighthouse-core/audits/service-worker.js +++ b/lighthouse-core/audits/service-worker.js @@ -20,16 +20,6 @@ const URL = require('../lib/url-shim'); const Audit = require('./audit'); -/** - * @param {!Array} versions - * @param {string} url - * @return {(!ServiceWorkerVersion|undefined)} - */ -function getActivatedServiceWorker(versions, url) { - const origin = new URL(url).origin; - return versions.find(v => v.status === 'activated' && new URL(v.scriptURL).origin === origin); -} - class ServiceWorker extends Audit { /** * @return {!AuditMeta} @@ -53,11 +43,15 @@ class ServiceWorker extends Audit { static audit(artifacts) { // Find active service worker for this URL. Match against // artifacts.URL.finalUrl so audit accounts for any redirects. - const version = getActivatedServiceWorker( - artifacts.ServiceWorker.versions, artifacts.URL.finalUrl); + const versions = artifacts.ServiceWorker.versions; + const url = artifacts.URL.finalUrl; + + const origin = new URL(url).origin; + const matchingSW = versions.filter(v => v.status === 'activated') + .find(v => new URL(v.scriptURL).origin === origin); return { - rawValue: !!version + rawValue: !!matchingSW }; } } diff --git a/lighthouse-core/audits/splash-screen.js b/lighthouse-core/audits/splash-screen.js new file mode 100644 index 000000000000..328e95202ddf --- /dev/null +++ b/lighthouse-core/audits/splash-screen.js @@ -0,0 +1,76 @@ +/** + * @license Copyright 2017 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('./multi-check-audit'); + +/** + * @fileoverview + * Audits if a page is configured for a custom splash screen when launched + * https://github.com/GoogleChrome/lighthouse/issues/24 + * + * Requirements: + * * manifest is not empty + * * manifest has a valid name + * * manifest has a valid background_color + * * manifest has a valid theme_color + * * manifest contains icon that's a png and size >= 512px + */ + +class SplashScreen extends Audit { + + /** + * @return {!AuditMeta} + */ + static get meta() { + return { + category: 'PWA', + name: 'splash-screen', + description: 'Configured for a custom splash screen', + helpText: 'A default splash screen will be constructed for your app, but satisfying these requirements guarantee a high-quality [splash screen](https://developers.google.com/web/updates/2015/10/splashscreen) that transitions the user from tapping the home screen icon to your app\'s first paint', + requiredArtifacts: ['Manifest'] + }; + } + + static assessManifest(manifestValues, failures) { + if (manifestValues.isParseFailure) { + failures.push(manifestValues.parseFailureReason); + return; + } + + const splashScreenCheckIds = [ + 'hasName', + 'hasBackgroundColor', + 'hasThemeColor', + 'hasIconsAtLeast512px' + ]; + + manifestValues.allChecks + .filter(item => splashScreenCheckIds.includes(item.id)) + .forEach(item => { + if (!item.passing) { + failures.push(item.failureText); + } + }); + } + + + static audit_(artifacts) { + const failures = []; + + return artifacts.requestManifestValues(artifacts.Manifest).then(manifestValues => { + SplashScreen.assessManifest(manifestValues, failures); + + return { + failures, + manifestValues + }; + }); + } +} + +module.exports = SplashScreen; diff --git a/lighthouse-core/audits/theme-color-meta.js b/lighthouse-core/audits/theme-color-meta.js deleted file mode 100644 index 012fa90c417f..000000000000 --- a/lighthouse-core/audits/theme-color-meta.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright 2016 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 validColor = require('../lib/web-inspector').Color.parse; -const Audit = require('./audit'); - -class ThemeColor extends Audit { - /** - * @return {!AuditMeta} - */ - static get meta() { - return { - category: 'HTML', - name: 'theme-color-meta', - description: 'Has a `` tag', - requiredArtifacts: ['ThemeColor'] - }; - } - - /** - * @param {!Artifacts} artifacts - * @return {!AuditResult} - */ - static audit(artifacts) { - const themeColorMeta = artifacts.ThemeColor; - if (themeColorMeta === null) { - return { - rawValue: false - }; - } - - if (!validColor(themeColorMeta)) { - return { - displayValue: themeColorMeta, - rawValue: false, - debugString: 'The theme-color meta tag did not contain a valid CSS color.' - }; - } - - return { - displayValue: themeColorMeta, - rawValue: true - }; - } -} - -module.exports = ThemeColor; diff --git a/lighthouse-core/audits/themed-omnibox.js b/lighthouse-core/audits/themed-omnibox.js new file mode 100644 index 000000000000..0d1302934a92 --- /dev/null +++ b/lighthouse-core/audits/themed-omnibox.js @@ -0,0 +1,73 @@ +/** + * @license Copyright 2017 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('./multi-check-audit'); +const validColor = require('../lib/web-inspector').Color.parse; + +/** + * @fileoverview + * Audits if a page is configured for a themed address bar + * + * Requirements: + * * manifest is not empty + * * manifest has a valid theme_color + * * HTML has a valid theme-color meta + */ + +class ThemedOmnibox extends Audit { + + /** + * @return {!AuditMeta} + */ + static get meta() { + return { + category: 'PWA', + name: 'themed-omnibox', + description: 'Address bar matches brand colors', + helpText: 'The browser address bar can be themed to match your site. A `theme-color` [meta tag](https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android) will upgrade the address bar when a user browses the site, and the [manifest theme-color](https://developers.google.com/web/updates/2015/08/using-manifest-to-set-sitewide-theme-color) will apply the same theme site-wide once it\'s been added to homescreen.', + requiredArtifacts: ['Manifest', 'ThemeColor'] + }; + } + + static assessMetaThemecolor(themeColorMeta, failures) { + if (themeColorMeta === null) { + failures.push('No `` tag found'); + } else if (!validColor(themeColorMeta)) { + failures.push('The theme-color meta tag did not contain a valid CSS color'); + } + } + + static assessManifest(manifestValues, failures) { + if (manifestValues.isParseFailure) { + failures.push(manifestValues.parseFailureReason); + return; + } + + const themeColorCheck = manifestValues.allChecks.find(i => i.id === 'hasThemeColor'); + if (!themeColorCheck.passing) { + failures.push(themeColorCheck.failureText); + } + } + + static audit_(artifacts) { + const failures = []; + + return artifacts.requestManifestValues(artifacts.Manifest).then(manifestValues => { + ThemedOmnibox.assessManifest(manifestValues, failures); + ThemedOmnibox.assessMetaThemecolor(artifacts.ThemeColor, failures); + + return { + failures, + manifestValues, + themeColor: artifacts.ThemeColor + }; + }); + } +} + +module.exports = ThemedOmnibox; diff --git a/lighthouse-core/audits/webapp-install-banner.js b/lighthouse-core/audits/webapp-install-banner.js new file mode 100644 index 000000000000..376017aa58a5 --- /dev/null +++ b/lighthouse-core/audits/webapp-install-banner.js @@ -0,0 +1,92 @@ +/** + * @license Copyright 2017 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('./multi-check-audit'); +const SWAudit = require('./service-worker'); + +/** + * @fileoverview + * Audits if a page is configured to prompt users with the webapp install banner. + * https://github.com/GoogleChrome/lighthouse/issues/23#issuecomment-270453303 + * + * Requirements: + * * manifest is not empty + * * manifest has valid start url + * * manifest has a valid name + * * manifest has a valid shortname + * * manifest display property is standalone, minimal-ui, or fullscreen + * * manifest contains icon that's a png and size >= 192px + * * SW is registered, and it owns this page and the manifest's start url + * * Site engagement score of 2 or higher + + * This audit covers these requirements with the following exceptions: + * * it doesn't consider SW controlling the starturl + * * it doesn't consider the site engagement score (naturally) + */ + +class WebappInstallBanner extends Audit { + + /** + * @return {!AuditMeta} + */ + static get meta() { + return { + category: 'PWA', + name: 'webapp-install-banner', + description: 'User can be prompted to Install the Web App', + helpText: 'While users can manually add your site to their homescreen, the [prompt (aka app install banner)](https://developers.google.com/web/updates/2015/03/increasing-engagement-with-app-install-banners-in-chrome-for-android) will proactively prompt the user to install the app if the various requirements are met and the user has moderate engagement with your site.', + requiredArtifacts: ['URL', 'ServiceWorker', 'Manifest'] + }; + } + + static assessManifest(manifestValues, failures) { + if (manifestValues.isParseFailure) { + failures.push(manifestValues.parseFailureReason); + return; + } + + const bannerCheckIds = [ + 'hasName', + 'hasShortName', + 'hasStartUrl', + 'hasPWADisplayValue', + 'hasIconsAtLeast192px' + ]; + manifestValues.allChecks + .filter(item => bannerCheckIds.includes(item.id)) + .forEach(item => { + if (!item.passing) { + failures.push(item.failureText); + } + }); + } + + + static assessServiceWorker(artifacts, failures) { + const hasServiceWorker = SWAudit.audit(artifacts).rawValue; + if (!hasServiceWorker) { + failures.push('Site registers a Service Worker'); + } + } + + static audit_(artifacts) { + const failures = []; + + return artifacts.requestManifestValues(artifacts.Manifest).then(manifestValues => { + WebappInstallBanner.assessManifest(manifestValues, failures); + WebappInstallBanner.assessServiceWorker(artifacts, failures); + + return { + failures, + manifestValues + }; + }); + } +} + +module.exports = WebappInstallBanner; diff --git a/lighthouse-core/config/default.json b/lighthouse-core/config/default.json index a0fbb17e6845..4cfa3a2faaa7 100644 --- a/lighthouse-core/config/default.json +++ b/lighthouse-core/config/default.json @@ -61,7 +61,6 @@ "service-worker", "works-offline", "viewport", - "manifest-display", "without-javascript", "first-meaningful-paint", "load-fast-enough-for-pwa", @@ -71,16 +70,10 @@ "user-timings", "screenshots", "critical-request-chains", - "manifest-exists", - "manifest-background-color", - "manifest-theme-color", - "manifest-icons-min-192", - "manifest-icons-min-144", - "manifest-name", - "manifest-short-name", + "webapp-install-banner", + "splash-screen", + "themed-omnibox", "manifest-short-name-length", - "manifest-start-url", - "theme-color-meta", "content-width", "deprecations", "accessibility/accesskeys", @@ -208,69 +201,24 @@ } }, { "name": "User can be prompted to Add to Homescreen", - "description": "While users can manually add your site to their homescreen in the browser menu, the [prompt (aka app install banner)](https://developers.google.com/web/updates/2015/03/increasing-engagement-with-app-install-banners-in-chrome-for-android) will proactively prompt the user to install the app if the below requirements are met and the user has visited your site at least twice (with at least five minutes between visits).", - "see": "https://github.com/GoogleChrome/lighthouse/issues/23", "audits": { - "service-worker": { - "expectedValue": true, - "weight": 1 - }, - "manifest-exists": { - "expectedValue": true, - "weight": 1 - }, - "manifest-start-url": { - "expectedValue": true, - "weight": 1 - }, - "manifest-icons-min-144": { - "expectedValue": true, - "weight": 1 - }, - "manifest-short-name": { + "webapp-install-banner": { "expectedValue": true, "weight": 1 } } }, { "name": "Installed web app will launch with custom splash screen", - "description": "A default splash screen will be constructed, but meeting these requirements guarantee a high-quality and customizable [splash screen](https://developers.google.com/web/updates/2015/10/splashscreen) the user sees between tapping the home screen icon and your app's first paint.", - "see": "https://github.com/GoogleChrome/lighthouse/issues/24", "audits": { - "manifest-exists": { - "expectedValue": true, - "weight": 1 - }, - "manifest-name": { - "expectedValue": true, - "weight": 1 - }, - "manifest-background-color": { - "expectedValue": true, - "weight": 1 - }, - "manifest-theme-color": { - "expectedValue": true, - "weight": 1 - }, - "manifest-icons-min-192": { + "splash-screen": { "expectedValue": true, "weight": 1 } } }, { "name": "Address bar matches brand colors", - "description": "The browser address bar can be themed to match your site. A `theme-color` [meta tag](https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android) will upgrade the address bar when a user browses the site, and the [manifest theme-color](https://developers.google.com/web/updates/2015/08/using-manifest-to-set-sitewide-theme-color) will apply the same theme site-wide once it's been added to homescreen.", "audits": { - "manifest-exists": { - "expectedValue": true, - "weight": 1 - }, - "theme-color-meta": { - "expectedValue": true, - "weight": 1 - }, - "manifest-theme-color": { + "themed-omnibox": { "expectedValue": true, "weight": 1 } @@ -374,10 +322,6 @@ "manifest-short-name-length": { "expectedValue": true, "weight": 1 - }, - "manifest-display": { - "expectedValue": true, - "weight": 1 } } }] diff --git a/lighthouse-core/gather/computed/manifest-values.js b/lighthouse-core/gather/computed/manifest-values.js new file mode 100644 index 000000000000..340665f66b3f --- /dev/null +++ b/lighthouse-core/gather/computed/manifest-values.js @@ -0,0 +1,119 @@ +/** + * @license Copyright 2016 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 ComputedArtifact = require('./computed-artifact'); +const icons = require('../../lib/icons'); + +const PWA_DISPLAY_VALUES = ['minimal-ui', 'fullscreen', 'standalone']; + +// Historically, Chrome recommended 12 chars as the maximum short_name length to prevent truncation. +// See #69 for more discussion & https://developer.chrome.com/apps/manifest/name#short_name +const SUGGESTED_SHORTNAME_LENGTH = 12; + +class ManifestValues extends ComputedArtifact { + + get name() { + return 'ManifestValues'; + } + + static get validityIds() { + return ['hasManifest', 'hasParseableManifest']; + } + + static get manifestChecks() { + return [ + { + id: 'hasStartUrl', + failureText: 'Manifest does not contain a `start_url`', + validate: manifest => !!manifest.value.start_url.value + }, + { + id: 'hasIconsAtLeast192px', + failureText: 'Manifest does not have icons at least 192px', + validate: manifest => icons.doExist(manifest.value) && + icons.sizeAtLeast(192, /** @type {!Manifest} */ (manifest.value)).length > 0 + }, + { + id: 'hasIconsAtLeast512px', + failureText: 'Manifest does not have icons at least 512px', + validate: manifest => icons.doExist(manifest.value) && + icons.sizeAtLeast(512, /** @type {!Manifest} */ (manifest.value)).length > 0 + }, + { + id: 'hasPWADisplayValue', + failureText: 'Manifest\'s `display` value is not one of: ' + PWA_DISPLAY_VALUES.join(' | '), + validate: manifest => PWA_DISPLAY_VALUES.includes(manifest.value.display.value) + }, + { + id: 'hasBackgroundColor', + failureText: 'Manifest does not have `background_color`', + validate: manifest => !!manifest.value.background_color.value + }, + { + id: 'hasThemeColor', + failureText: 'Manifest does not have `theme_color`', + validate: manifest => !!manifest.value.theme_color.value + }, + { + id: 'hasShortName', + failureText: 'Manifest does not have `short_name`', + validate: manifest => !!manifest.value.short_name.value + }, + { + id: 'shortNameLength', + failureText: 'Manifest `short_name` will be truncated when displayed on the homescreen', + validate: manifest => manifest.value.short_name.value && + manifest.value.short_name.value.length <= SUGGESTED_SHORTNAME_LENGTH + }, + { + id: 'hasName', + failureText: 'Manifest does not have `name`', + validate: manifest => !!manifest.value.name.value + } + ]; + } + + /** + * Returns results of all manifest checks + * @param {Manifest} manifest + * @return {{isParseFailure: !boolean, parseFailureReason: ?string, allChecks: !Array}} + */ + compute_(manifest) { + // if the manifest isn't there or is invalid json, we report that and bail + let parseFailureReason; + + if (manifest === null) { + parseFailureReason = 'No manifest was fetched'; + } + if (manifest && manifest.value === undefined) { + parseFailureReason = 'Manifest failed to parse as valid JSON'; + } + if (parseFailureReason) { + return { + isParseFailure: true, + parseFailureReason, + allChecks: [] + }; + } + + // manifest is valid, so do the rest of the checks + const remainingChecks = ManifestValues.manifestChecks.map(item => { + item.passing = item.validate(manifest); + return item; + }); + + return { + isParseFailure: false, + parseFailureReason, + allChecks: remainingChecks + }; + } + +} + +module.exports = ManifestValues; diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js index c4f3ac9b6efd..ceed769dbf10 100644 --- a/lighthouse-core/runner.js +++ b/lighthouse-core/runner.js @@ -212,6 +212,7 @@ class Runner { const ignoredFiles = [ 'audit.js', 'accessibility/axe-audit.js', + 'multi-check-audit.js', 'byte-efficiency/byte-efficiency-audit.js' ]; diff --git a/lighthouse-core/test/audits/manifest-background-color-test.js b/lighthouse-core/test/audits/manifest-background-color-test.js deleted file mode 100644 index 5981ef82b10e..000000000000 --- a/lighthouse-core/test/audits/manifest-background-color-test.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright 2016 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 ManifestBackgroundColorAudit = require('../../audits/manifest-background-color.js'); -const assert = require('assert'); -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const manifestParser = require('../../lib/manifest-parser'); -const exampleManifest = manifestParser(manifestSrc, 'https://example.com/', 'https://example.com/'); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; - -/** - * Simple manifest parsing helper when the manifest URLs aren't material to the - * test. Uses example.com URLs for testing. - * @param {string} manifestSrc - * @return {!ManifestNode<(!Manifest|undefined)>} - */ -function noUrlManifestParser(manifestSrc) { - return manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); -} - -/* eslint-env mocha */ - -// Need to disable camelcase check for dealing with background_color. -/* eslint-disable camelcase */ -describe('Manifest: background color audit', () => { - it('fails with no debugString if page had no manifest', () => { - const result = ManifestBackgroundColorAudit.audit({ - Manifest: null - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('fails when an empty manifest is present', () => { - const artifacts = { - Manifest: noUrlManifestParser('{}') - }; - return assert.equal(ManifestBackgroundColorAudit.audit(artifacts).rawValue, false); - }); - - it('fails when a minimal manifest contains no background_color', () => { - const artifacts = { - Manifest: noUrlManifestParser(JSON.stringify({ - start_url: '/' - })) - }; - const output = ManifestBackgroundColorAudit.audit(artifacts); - assert.equal(output.rawValue, false); - assert.equal(output.debugString, undefined); - }); - - it('fails when a minimal manifest contains an invalid background_color', () => { - const artifacts = { - Manifest: noUrlManifestParser(JSON.stringify({ - background_color: 'no' - })) - }; - const output = ManifestBackgroundColorAudit.audit(artifacts); - assert.equal(output.rawValue, false); - assert.equal(output.debugString, undefined); - }); - - it('succeeds when a minimal manifest contains a valid background_color', () => { - const artifacts = { - Manifest: noUrlManifestParser(JSON.stringify({ - background_color: '#FAFAFA' - })) - }; - const output = ManifestBackgroundColorAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.extendedInfo.value, '#FAFAFA'); - }); - - it('succeeds when a complete manifest contains a background_color', () => { - const result = ManifestBackgroundColorAudit.audit({Manifest: exampleManifest}); - return assert.equal(result.rawValue, true); - }); -}); -/* eslint-enable */ diff --git a/lighthouse-core/test/audits/manifest-display-test.js b/lighthouse-core/test/audits/manifest-display-test.js deleted file mode 100644 index 3125713cbcb0..000000000000 --- a/lighthouse-core/test/audits/manifest-display-test.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright 2016 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 ManifestDisplayAudit = require('../../audits/manifest-display.js'); -const manifestParser = require('../../lib/manifest-parser'); -const assert = require('assert'); - -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; -const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - -/* eslint-env mocha */ - -describe('Mobile-friendly: display audit', () => { - it('only accepts when a value is set for the display prop', () => { - assert.equal(ManifestDisplayAudit.hasRecommendedValue('fullscreen'), true); - assert.equal(ManifestDisplayAudit.hasRecommendedValue('standalone'), true); - assert.equal(ManifestDisplayAudit.hasRecommendedValue('browser'), true); - assert.equal(ManifestDisplayAudit.hasRecommendedValue('minimal-ui'), true); - assert.equal(ManifestDisplayAudit.hasRecommendedValue('different'), false); - assert.equal(ManifestDisplayAudit.hasRecommendedValue(undefined), false); - }); - - it('fails with no debugString if page had no manifest', () => { - const result = ManifestDisplayAudit.audit({ - Manifest: null - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('falls back to the successful default when there is no manifest display property', () => { - const artifacts = { - Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestDisplayAudit.audit(artifacts); - - assert.equal(output.displayValue, 'browser'); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - it('succeeds when a manifest has a display property', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - display: 'standalone' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestDisplayAudit.audit(artifacts); - assert.equal(output.displayValue, 'standalone'); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - it('succeeds when a complete manifest contains a display property', () => { - return assert.equal(ManifestDisplayAudit.audit({Manifest: exampleManifest}).rawValue, true); - }); -}); diff --git a/lighthouse-core/test/audits/manifest-exists-test.js b/lighthouse-core/test/audits/manifest-exists-test.js deleted file mode 100644 index 37ca1681611b..000000000000 --- a/lighthouse-core/test/audits/manifest-exists-test.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 2016 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 ManifestExistsAudit = require('../../audits/manifest-exists.js'); -const assert = require('assert'); - -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const manifestParser = require('../../lib/manifest-parser'); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; -const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - -/* eslint-env mocha */ - -describe('Manifest: exists audit', () => { - it('fails with no debugString if page had no manifest', () => { - const result = ManifestExistsAudit.audit({ - Manifest: null - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('succeeds with a valid minimal manifest', () => { - const artifacts = { - Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestExistsAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - it('succeeds with a valid minimal manifest', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - name: 'Lighthouse PWA' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestExistsAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - it('correctly passes through debug strings', () => { - const debugString = 'No href found on .'; - - assert.equal(ManifestExistsAudit.audit({ - Manifest: { - value: {}, - debugString - } - }).debugString, debugString); - }); - - it('correctly passes through a JSON parsing failure', () => { - const artifacts = { - Manifest: manifestParser('{ \name: Definitely not valid JSON }', EXAMPLE_MANIFEST_URL, - EXAMPLE_DOC_URL) - }; - const output = ManifestExistsAudit.audit(artifacts); - assert.equal(output.rawValue, false); - assert.ok(output.debugString.includes('Unexpected token'), 'No JSON error message'); - }); - - it('succeeds with a complete manifest', () => { - return assert.equal(ManifestExistsAudit.audit({Manifest: exampleManifest}).rawValue, true); - }); -}); diff --git a/lighthouse-core/test/audits/manifest-icons-test.js b/lighthouse-core/test/audits/manifest-icons-test.js deleted file mode 100644 index 9626997b8a76..000000000000 --- a/lighthouse-core/test/audits/manifest-icons-test.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @license - * Copyright 2016 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 Audit144 = require('../../audits/manifest-icons-min-144.js'); -const Audit192 = require('../../audits/manifest-icons-min-192.js'); -const assert = require('assert'); -const manifestParser = require('../../lib/manifest-parser'); -const exampleManifest = JSON.stringify(require('../fixtures/manifest.json')); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; - -/* eslint-env mocha */ - -describe('Manifest: icons audits', () => { - it('fails with no debugString if page had no manifest', () => { - const result144 = Audit144.audit({ - Manifest: null - }); - assert.strictEqual(result144.rawValue, false); - assert.strictEqual(result144.debugString, undefined); - - const result192 = Audit192.audit({ - Manifest: null - }); - assert.strictEqual(result192.rawValue, false); - assert.strictEqual(result192.debugString, undefined); - }); - - describe('icons exist check', () => { - it('fails when a manifest contains no icons array', () => { - const manifestSrc = JSON.stringify({ - name: 'NoIconsHere' - }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - assert.equal(Audit144.audit({Manifest}).rawValue, false); - assert.equal(Audit192.audit({Manifest}).rawValue, false); - }); - - it('fails when a manifest contains no icons', () => { - const manifestSrc = JSON.stringify({ - icons: [] - }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - - const audit144 = Audit144.audit({Manifest}); - const audit192 = Audit192.audit({Manifest}); - - assert.equal(audit144.rawValue, false); - assert.equal(audit192.rawValue, false); - }); - }); - - describe('icons at least X size check', () => { - it('fails when a manifest contains an icon with no size', () => { - const manifestSrc = JSON.stringify({ - icons: [{ - src: 'icon.png' - }] - }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - - const audit144 = Audit144.audit({Manifest}); - const audit192 = Audit192.audit({Manifest}); - - assert.equal(audit144.rawValue, false); - assert.ok(audit144.debugString.match(/are at least 144px/)); - assert.equal(audit192.rawValue, false); - assert.ok(audit192.debugString.match(/are at least 192px/)); - }); - - it('succeeds when a manifest contains icons that are large enough', () => { - // stub manifest contains a 192 icon - const Manifest = manifestParser(exampleManifest, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - assert.equal(Audit144.audit({Manifest}).rawValue, true); - assert.equal(Audit192.audit({Manifest}).rawValue, true); - }); - - it('succeeds when there\'s one icon with multiple sizes, and one is valid', () => { - const manifestSrc = JSON.stringify({ - icons: [{ - src: 'icon.png', - sizes: '72x72 96x96 128x128 256x256' - }] - }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - - const audit144 = Audit144.audit({Manifest}); - const audit192 = Audit192.audit({Manifest}); - - assert.equal(audit144.rawValue, true); - assert.ok(audit144.displayValue); - assert.equal(audit192.rawValue, true); - assert.ok(audit192.displayValue); - }); - - it('succeeds when there\'s two icons, one without sizes; the other with a valid size', () => { - const manifestSrc = JSON.stringify({ - icons: [{ - src: 'icon.png' - }, { - src: 'icon2.png', - sizes: '256x256' - }] - }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - assert.equal(Audit144.audit({Manifest}).rawValue, true); - assert.equal(Audit192.audit({Manifest}).rawValue, true); - }); - - it('fails when an icon has a valid size, though it\'s non-square.', () => { - // See also: https://code.google.com/p/chromium/codesearch#chromium/src/chrome/browser/banners/app_banner_data_fetcher_unittest.cc&sq=package:chromium&type=cs&q=%22Non-square%20is%20okay%22%20file:%5Esrc/chrome/browser/banners/ - const manifestSrc = JSON.stringify({ - icons: [{ - src: 'icon-non-square.png', - sizes: '200x220' - }] - }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - assert.equal(Audit144.audit({Manifest}).rawValue, false); - assert.equal(Audit192.audit({Manifest}).rawValue, false); - }); - }); -}); diff --git a/lighthouse-core/test/audits/manifest-name-test.js b/lighthouse-core/test/audits/manifest-name-test.js deleted file mode 100644 index 7d1b0097b1c4..000000000000 --- a/lighthouse-core/test/audits/manifest-name-test.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2016 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 ManifestNameAudit = require('../../audits/manifest-name.js'); -const assert = require('assert'); -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const manifestParser = require('../../lib/manifest-parser'); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; -const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - -/* eslint-env mocha */ - -describe('Manifest: name audit', () => { - it('fails with no debugString if page had no manifest', () => { - const result = ManifestNameAudit.audit({ - Manifest: null, - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('fails when an empty manifest is present', () => { - const artifacts = { - Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - return assert.equal(ManifestNameAudit.audit(artifacts).rawValue, false); - }); - - it('fails when a manifest contains no name', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - display: '/' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - return assert.equal(ManifestNameAudit.audit(artifacts).rawValue, false); - }); - - it('succeeds when a minimal manifest contains a name', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - name: 'Lighthouse PWA' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - return assert.equal(ManifestNameAudit.audit(artifacts).rawValue, true); - }); - - it('succeeds when a complete manifest contains a name', () => { - return assert.equal(ManifestNameAudit.audit({Manifest: exampleManifest}).rawValue, true); - }); -}); diff --git a/lighthouse-core/test/audits/manifest-short-name-length-test.js b/lighthouse-core/test/audits/manifest-short-name-length-test.js index 27687ce59dd3..2f0b1ff41243 100644 --- a/lighthouse-core/test/audits/manifest-short-name-length-test.js +++ b/lighthouse-core/test/audits/manifest-short-name-length-test.js @@ -22,52 +22,72 @@ const manifestParser = require('../../lib/manifest-parser'); const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; const EXAMPLE_DOC_URL = 'https://example.com/index.html'; +const GatherRunner = require('../../gather/gather-runner.js'); + +function generateMockArtifacts() { + const computedArtifacts = GatherRunner.instantiateComputedArtifacts(); + const mockArtifacts = Object.assign({}, computedArtifacts, { + Manifest: null + }); + return mockArtifacts; +} + /* eslint-env mocha */ describe('Manifest: short_name_length audit', () => { it('fails with no debugString if page had no manifest', () => { - const result = ManifestShortNameLengthAudit.audit({ - Manifest: null + const artifacts = generateMockArtifacts(); + artifacts.Manifest = null; + + return ManifestShortNameLengthAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.strictEqual(result.debugString, undefined); }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); }); it('fails when an empty manifest is present', () => { - const Manifest = manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - const result = ManifestShortNameLengthAudit.audit({Manifest}); - assert.equal(result.rawValue, false); - assert.equal(result.debugString, 'No short_name found in manifest.'); + const artifacts = generateMockArtifacts(); + artifacts.Manifest = manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return ManifestShortNameLengthAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.equal(result.debugString, 'No short_name found in manifest.'); + }); }); it('fails when a manifest contains no short_name and too long name', () => { + const artifacts = generateMockArtifacts(); const manifestSrc = JSON.stringify({ name: 'i\'m much longer than the recommended size' }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - const out = ManifestShortNameLengthAudit.audit({Manifest}); - assert.equal(out.rawValue, false); - assert.notEqual(out.debugString, undefined); + artifacts.Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return ManifestShortNameLengthAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.notEqual(result.debugString, undefined); + }); }); // Need to disable camelcase check for dealing with short_name. /* eslint-disable camelcase */ it('fails when a manifest contains a too long short_name', () => { + const artifacts = generateMockArtifacts(); const manifestSrc = JSON.stringify({ short_name: 'i\'m much longer than the recommended size' }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - const out = ManifestShortNameLengthAudit.audit({Manifest}); - assert.equal(out.rawValue, false); - assert.notEqual(out.debugString, undefined); + artifacts.Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return ManifestShortNameLengthAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + }); }); it('succeeds when a manifest contains a short_name', () => { + const artifacts = generateMockArtifacts(); const manifestSrc = JSON.stringify({ short_name: 'Lighthouse' }); - const Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - return assert.equal(ManifestShortNameLengthAudit.audit({Manifest}).rawValue, true); + artifacts.Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return ManifestShortNameLengthAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, true); + }); }); /* eslint-enable camelcase */ }); diff --git a/lighthouse-core/test/audits/manifest-short-name-test.js b/lighthouse-core/test/audits/manifest-short-name-test.js deleted file mode 100644 index 9cdea1f93493..000000000000 --- a/lighthouse-core/test/audits/manifest-short-name-test.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright 2016 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 ManifestShortNameAudit = require('../../audits/manifest-short-name.js'); -const assert = require('assert'); -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const manifestParser = require('../../lib/manifest-parser'); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; -const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - -/* eslint-env mocha */ - -describe('Manifest: short_name audit', () => { - it('fails with no debugString if page had no manifest', () => { - const result = ManifestShortNameAudit.audit({ - Manifest: null, - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('fails when an empty manifest is present', () => { - const artifacts = { - Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - return assert.equal(ManifestShortNameAudit.audit(artifacts).rawValue, false); - }); - - // Need to disable camelcase check for dealing with short_name. - /* eslint-disable camelcase */ - it('fails when a manifest contains no short_name and no name', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - name: undefined, - short_name: undefined - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - - const output = ManifestShortNameAudit.audit(artifacts); - assert.equal(output.rawValue, false); - assert.equal(output.debugString, undefined); - }); - - it('succeeds when a manifest contains no short_name but a name', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - short_name: undefined, - name: 'Example App' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - - const output = ManifestShortNameAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - /* eslint-enable camelcase */ - - it('succeeds when a manifest contains a short_name', () => { - const output = ManifestShortNameAudit.audit({Manifest: exampleManifest}); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); -}); diff --git a/lighthouse-core/test/audits/manifest-start-url-test.js b/lighthouse-core/test/audits/manifest-start-url-test.js deleted file mode 100644 index e496e59fd13a..000000000000 --- a/lighthouse-core/test/audits/manifest-start-url-test.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright 2016 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 ManifestStartUrlAudit = require('../../audits/manifest-start-url.js'); -const assert = require('assert'); -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const manifestParser = require('../../lib/manifest-parser'); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; -const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - -/* eslint-env mocha */ - -describe('Manifest: start_url audit', () => { - it('fails with no debugString if page had no manifest', () => { - const result = ManifestStartUrlAudit.audit({ - Manifest: null, - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('fails when an empty manifest is present', () => { - const artifacts = { - Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestStartUrlAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - // Need to disable camelcase check for dealing with start_url. - /* eslint-disable camelcase */ - it('fails when a manifest contains no start_url', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - start_url: undefined - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestStartUrlAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - it('succeeds when a minimal manifest contains a start_url', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - start_url: '/' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestStartUrlAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - /* eslint-enable camelcase */ - - it('succeeds when a complete manifest contains a start_url', () => { - return assert.equal(ManifestStartUrlAudit.audit({Manifest: exampleManifest}).rawValue, true); - }); -}); diff --git a/lighthouse-core/test/audits/manifest-theme-color-test.js b/lighthouse-core/test/audits/manifest-theme-color-test.js deleted file mode 100644 index 0678dfcacf46..000000000000 --- a/lighthouse-core/test/audits/manifest-theme-color-test.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright 2016 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 ManifestThemeColorAudit = require('../../audits/manifest-theme-color.js'); -const assert = require('assert'); -const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); -const manifestParser = require('../../lib/manifest-parser'); - -const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; -const EXAMPLE_DOC_URL = 'https://example.com/index.html'; -const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); - -/* eslint-env mocha */ - -describe('Manifest: theme_color audit', () => { - it('fails with no debugString if page had no manifest', () => { - const result = ManifestThemeColorAudit.audit({ - Manifest: null - }); - assert.strictEqual(result.rawValue, false); - assert.strictEqual(result.debugString, undefined); - }); - - it('fails when an empty manifest is present', () => { - const artifacts = { - Manifest: manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestThemeColorAudit.audit(artifacts); - assert.equal(output.rawValue, false); - assert.equal(output.debugString, undefined); - }); - - // Need to disable camelcase check for dealing with theme_color. - /* eslint-disable camelcase */ - it('fails when a minimal manifest contains no theme_color', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - start_url: '/' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestThemeColorAudit.audit(artifacts); - assert.equal(output.rawValue, false); - assert.equal(output.debugString, undefined); - }); - - it('succeeds when a minimal manifest contains a theme_color', () => { - const artifacts = { - Manifest: manifestParser(JSON.stringify({ - theme_color: '#bada55' - }), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL) - }; - const output = ManifestThemeColorAudit.audit(artifacts); - assert.equal(output.rawValue, true); - assert.equal(output.debugString, undefined); - }); - - /* eslint-enable camelcase */ - - it('succeeds when a complete manifest contains a theme_color', () => { - return assert.equal(ManifestThemeColorAudit.audit({Manifest: exampleManifest}).rawValue, true); - }); -}); diff --git a/lighthouse-core/test/audits/splash-screen-test.js b/lighthouse-core/test/audits/splash-screen-test.js new file mode 100644 index 000000000000..d6c153fc76e1 --- /dev/null +++ b/lighthouse-core/test/audits/splash-screen-test.js @@ -0,0 +1,131 @@ +/** + * @license Copyright 2017 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 SplashScreenAudit = require('../../audits/splash-screen'); +const assert = require('assert'); +const manifestParser = require('../../lib/manifest-parser'); + +const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); +const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; +const EXAMPLE_DOC_URL = 'https://example.com/index.html'; +const exampleManifest = noUrlManifestParser(manifestSrc); + +const GatherRunner = require('../../gather/gather-runner.js'); + +function generateMockArtifacts() { + const computedArtifacts = GatherRunner.instantiateComputedArtifacts(); + const mockArtifacts = Object.assign({}, computedArtifacts, { + Manifest: exampleManifest + }); + return mockArtifacts; +} + +/** + * Simple manifest parsing helper when the manifest URLs aren't material to the + * test. Uses example.com URLs for testing. + * @param {string} manifestSrc + * @return {!ManifestNode<(!Manifest|undefined)>} + */ +function noUrlManifestParser(manifestSrc) { + return manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); +} + +/* eslint-env mocha */ +describe('PWA: splash screen audit', () => { + describe('basics', () => { + it('fails if page had no manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = null; + + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('No manifest was fetched'), result.debugString); + }); + }); + + it('fails with a non-parsable manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = manifestParser('{,:}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('failed to parse as valid JSON')); + }); + }); + + it('fails when an empty manifest is present', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString); + assert.strictEqual(result.extendedInfo.value.failures.length, 4); + }); + }); + + it('passes with complete manifest and SW', () => { + return SplashScreenAudit.audit(generateMockArtifacts()).then(result => { + assert.strictEqual(result.rawValue, true, result.debugString); + assert.strictEqual(result.debugString, undefined, result.debugString); + }); + }); + }); + + describe('one-off-failures', () => { + it('fails when a manifest contains no name', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.name.value = undefined; + + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('name'), result.debugString); + }); + }); + + it('fails when a manifest contains no background color', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.background_color.value = undefined; + + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('background_color'), result.debugString); + }); + }); + + it('fails when a manifest contains no background color', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = noUrlManifestParser(JSON.stringify({ + background_color: 'no' + })); + + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('background_color'), result.debugString); + }); + }); + + it('fails when a manifest contains no theme color', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.theme_color.value = undefined; + + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('theme_color'), result.debugString); + }); + }); + + it('fails if page had no icons in the manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.icons.value = []; + + return SplashScreenAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('icons'), result.debugString); + }); + }); + }); +}); diff --git a/lighthouse-core/test/audits/theme-color-meta-test.js b/lighthouse-core/test/audits/theme-color-meta-test.js deleted file mode 100644 index 164006ddce10..000000000000 --- a/lighthouse-core/test/audits/theme-color-meta-test.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright 2016 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('../../audits/theme-color-meta.js'); -const assert = require('assert'); - -/* eslint-env mocha */ - -describe('HTML: theme-color audit', () => { - it('fails and warns when no theme-color meta tag found', () => { - const nullColorAudit = Audit.audit({ - ThemeColor: null - }); - - assert.equal(nullColorAudit.rawValue, false); - }); - - it('fails and warns when theme-color has an invalid CSS color', () => { - const invalidColorAudit = Audit.audit({ - ThemeColor: '#1234567' - }); - - assert.equal(invalidColorAudit.rawValue, false); - assert(invalidColorAudit.debugString); - }); - - it('succeeds when theme-color present in the html', () => { - assert.equal(Audit.audit({ - ThemeColor: '#fafa33' - }).rawValue, true); - }); - - it('succeeds when theme-color has a CSS nickname content value', () => { - assert.equal(Audit.audit({ - ThemeColor: 'red' - }).rawValue, true); - }); -}); diff --git a/lighthouse-core/test/audits/themed-omnibox-test.js b/lighthouse-core/test/audits/themed-omnibox-test.js new file mode 100644 index 000000000000..71716411b7cb --- /dev/null +++ b/lighthouse-core/test/audits/themed-omnibox-test.js @@ -0,0 +1,141 @@ +/** + * @license Copyright 2017 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 ThemedOmniboxAudit = require('../../audits/themed-omnibox'); +const assert = require('assert'); +const manifestParser = require('../../lib/manifest-parser'); + +const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); +const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; +const EXAMPLE_DOC_URL = 'https://example.com/index.html'; +const exampleManifest = noUrlManifestParser(manifestSrc); + +const GatherRunner = require('../../gather/gather-runner.js'); + +function generateMockArtifacts() { + const computedArtifacts = GatherRunner.instantiateComputedArtifacts(); + const mockArtifacts = Object.assign({}, computedArtifacts, { + Manifest: exampleManifest, + ThemeColor: '#bada55' + }); + return mockArtifacts; +} + +/** + * Simple manifest parsing helper when the manifest URLs aren't material to the + * test. Uses example.com URLs for testing. + * @param {string} manifestSrc + * @return {!ManifestNode<(!Manifest|undefined)>} + */ +function noUrlManifestParser(manifestSrc) { + return manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); +} + +/* eslint-env mocha */ +describe('PWA: themed omnibox audit', () => { + it('fails if page had no manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = null; + + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('No manifest was fetched'), result.debugString); + }); + }); + + // Need to disable camelcase check for dealing with theme_color. + /* eslint-disable camelcase */ + it('fails when a minimal manifest contains no theme_color', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = noUrlManifestParser(JSON.stringify({ + start_url: '/' + })); + + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.ok(result.debugString); + }); + }); + + it('succeeds when a minimal manifest contains a theme_color', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = noUrlManifestParser(JSON.stringify({ + theme_color: '#bada55' + })); + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, true); + assert.equal(result.debugString, undefined); + }); + }); + + /* eslint-enable camelcase */ + it('succeeds when a complete manifest contains a theme_color', () => { + const artifacts = generateMockArtifacts(); + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, true); + assert.equal(result.debugString, undefined); + }); + }); + + it('fails and warns when no theme-color meta tag found', () => { + const artifacts = generateMockArtifacts(); + artifacts.ThemeColor = null; + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.ok(result.debugString); + }); + }); + + it('fails and warns when theme-color has an invalid CSS color', () => { + const artifacts = generateMockArtifacts(); + artifacts.ThemeColor = '#1234567'; + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.ok(result.debugString.includes('valid CSS color')); + }); + }); + + it('succeeds when theme-color present in the html', () => { + const artifacts = generateMockArtifacts(); + artifacts.ThemeColor = '#fafa33'; + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, true); + assert.equal(result.debugString, undefined); + }); + }); + + it('succeeds when theme-color has a CSS nickname content value', () => { + const artifacts = generateMockArtifacts(); + artifacts.ThemeColor = 'red'; + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, true); + assert.equal(result.debugString, undefined); + }); + }); + + + it('fails if HTML theme color is good, but manifest themecolor is bad', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = noUrlManifestParser(JSON.stringify({ + start_url: '/' + })); + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.ok(result.debugString.includes('does not have `theme_color`'), result.debugString); + }); + }); + + it('fails if HTML theme color is bad, and manifest themecolor is good', () => { + const artifacts = generateMockArtifacts(); + artifacts.ThemeColor = 'not a color'; + return ThemedOmniboxAudit.audit(artifacts).then(result => { + assert.equal(result.rawValue, false); + assert.ok(result.debugString.includes('theme-color meta tag'), result.debugString); + }); + }); +}); diff --git a/lighthouse-core/test/audits/webapp-install-banner-test.js b/lighthouse-core/test/audits/webapp-install-banner-test.js new file mode 100644 index 000000000000..ad13077ca031 --- /dev/null +++ b/lighthouse-core/test/audits/webapp-install-banner-test.js @@ -0,0 +1,139 @@ +/** + * @license Copyright 2017 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 WebappInstallBannerAudit = require('../../audits/webapp-install-banner'); +const assert = require('assert'); +const manifestParser = require('../../lib/manifest-parser'); + +const manifestSrc = JSON.stringify(require('../fixtures/manifest.json')); +const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; +const EXAMPLE_DOC_URL = 'https://example.com/index.html'; +const exampleManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + +const GatherRunner = require('../../gather/gather-runner.js'); + +function generateMockArtifacts() { + const computedArtifacts = GatherRunner.instantiateComputedArtifacts(); + const clonedArtifacts = JSON.parse(JSON.stringify({ + Manifest: exampleManifest, + ServiceWorker: { + versions: [{ + status: 'activated', + scriptURL: 'https://example.com/sw.js' + }] + }, + URL: {finalUrl: 'https://example.com'} + })); + const mockArtifacts = Object.assign({}, computedArtifacts, clonedArtifacts); + return mockArtifacts; +} + +/* eslint-env mocha */ +describe('PWA: webapp install banner audit', () => { + describe('basics', () => { + it('fails if page had no manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = null; + + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('No manifest was fetched'), result.debugString); + }); + }); + + it('fails with a non-parsable manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = manifestParser('{,:}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('failed to parse as valid JSON')); + }); + }); + + it('fails when an empty manifest is present', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest = manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString); + assert.strictEqual(result.extendedInfo.value.failures.length, 4); + }); + }); + + it('passes with complete manifest and SW', () => { + return WebappInstallBannerAudit.audit(generateMockArtifacts()).then(result => { + assert.strictEqual(result.rawValue, true, result.debugString); + assert.strictEqual(result.debugString, undefined, result.debugString); + }); + }); + }); + + describe('one-off-failures', () => { + /* eslint-disable camelcase */ // because start_url + it('fails when a manifest contains no start_url', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.start_url.value = undefined; + + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('start_url'), result.debugString); + const failures = result.extendedInfo.value.failures; + assert.strictEqual(failures.length, 1, failures); + }); + }); + + /* eslint-disable camelcase */ // because short_name + it('fails when a manifest contains no short_name', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.short_name.value = undefined; + + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('short_name'), result.debugString); + const failures = result.extendedInfo.value.failures; + assert.strictEqual(failures.length, 1, failures); + }); + }); + + it('fails when a manifest contains no name', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.name.value = undefined; + + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('name'), result.debugString); + const failures = result.extendedInfo.value.failures; + assert.strictEqual(failures.length, 1, failures); + }); + }); + + it('fails if page had no icons in the manifest', () => { + const artifacts = generateMockArtifacts(); + artifacts.Manifest.value.icons.value = []; + + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('icons'), result.debugString); + const failures = result.extendedInfo.value.failures; + assert.strictEqual(failures.length, 1, failures); + }); + }); + }); + + it('fails if page had no SW', () => { + const artifacts = generateMockArtifacts(); + artifacts.ServiceWorker.versions = []; + + return WebappInstallBannerAudit.audit(artifacts).then(result => { + assert.strictEqual(result.rawValue, false); + assert.ok(result.debugString.includes('Service Worker'), result.debugString); + const failures = result.extendedInfo.value.failures; + assert.strictEqual(failures.length, 1, failures); + }); + }); +}); diff --git a/lighthouse-core/test/fixtures/manifest.json b/lighthouse-core/test/fixtures/manifest.json index 6798f941e2a2..750ac6d021cb 100644 --- a/lighthouse-core/test/fixtures/manifest.json +++ b/lighthouse-core/test/fixtures/manifest.json @@ -12,6 +12,11 @@ "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", diff --git a/lighthouse-core/test/gather/computed/manifest-values-test.js b/lighthouse-core/test/gather/computed/manifest-values-test.js new file mode 100644 index 000000000000..5e6c91fda844 --- /dev/null +++ b/lighthouse-core/test/gather/computed/manifest-values-test.js @@ -0,0 +1,203 @@ +/** + * @license Copyright 2017 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'; + +/* eslint-env mocha */ + +const ManifestValues = require('../../../gather/computed/manifest-values'); +const assert = require('assert'); + +const manifestSrc = JSON.stringify(require('../../fixtures/manifest.json')); +const manifestParser = require('../../../lib/manifest-parser'); + +const manifestValues = new ManifestValues(); + +/** + * Simple manifest parsing helper when the manifest URLs aren't material to the + * test. Uses example.com URLs for testing. + * @param {string} manifestSrc + * @return {!ManifestNode<(!Manifest|undefined)>} + */ +function noUrlManifestParser(manifestSrc) { + const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json'; + const EXAMPLE_DOC_URL = 'https://example.com/index.html'; + + return manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); +} + +/* eslint-env mocha */ +describe('ManifestValues computed artifact', () => { + it('reports a parse failure if page had no manifest', () => { + const manifestArtifact = null; + const results = manifestValues.compute_(manifestArtifact); + assert.equal(results.isParseFailure, true); + assert.ok(results.parseFailureReason, 'No manifest was fetched'); + assert.equal(results.allChecks.length, 0); + }); + + it('reports a parse failure if page had an unparseable manifest', () => { + const manifestArtifact = noUrlManifestParser('{:,}'); + const results = manifestValues.compute_(manifestArtifact); + assert.equal(results.isParseFailure, true); + assert.ok(results.parseFailureReason.includes('failed to parse as valid JSON')); + assert.equal(results.allChecks.length, 0); + }); + + it('passes the parsing checks on an empty manifest', () => { + const manifestArtifact = noUrlManifestParser('{}'); + const results = manifestValues.compute_(manifestArtifact); + assert.equal(results.isParseFailure, false); + assert.equal(results.parseFailureReason, undefined); + }); + + it('passes the all checks with fixture manifest', () => { + const manifestArtifact = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(manifestArtifact); + assert.equal(results.isParseFailure, false); + assert.equal(results.parseFailureReason, undefined); + + assert.equal(results.allChecks.length, ManifestValues.manifestChecks.length); + assert.equal(results.allChecks.every(i => i.passing), true, 'not all checks passed'); + }); + + describe('color checks', () => { + it('fails when a minimal manifest contains no background_color', () => { + const Manifest = noUrlManifestParser(JSON.stringify({ + start_url: '/' + })); + const results = manifestValues.compute_(Manifest); + const colorResults = results.allChecks.filter(i => i.id.includes('Color')); + assert.equal(colorResults.every(i => i.passing === false), true); + }); + + it('fails when a minimal manifest contains an invalid background_color', () => { + const Manifest = noUrlManifestParser(JSON.stringify({ + background_color: 'no', + theme_color: 'no' + })); + + const results = manifestValues.compute_(Manifest); + const colorResults = results.allChecks.filter(i => i.id.includes('Color')); + assert.equal(colorResults.every(i => i.passing === false), true); + }); + + it('succeeds when a minimal manifest contains a valid background_color', () => { + const Manifest = noUrlManifestParser(JSON.stringify({ + background_color: '#FAFAFA', + theme_color: '#FAFAFA' + })); + + const results = manifestValues.compute_(Manifest); + const colorResults = results.allChecks.filter(i => i.id.includes('Color')); + assert.equal(colorResults.every(i => i.passing === true), true); + }); + }); + + describe('hasPWADisplayValue', () => { + const check = ManifestValues.manifestChecks.find(i => i.id === 'hasPWADisplayValue'); + + it('passes accepted values', () => { + let Manifest; + Manifest = noUrlManifestParser(JSON.stringify({display: 'minimal-ui'})); + assert.equal(check.validate(Manifest), true, 'doesnt pass minimal-ui'); + Manifest = noUrlManifestParser(JSON.stringify({display: 'standalone'})); + assert.equal(check.validate(Manifest), true, 'doesnt pass standalone'); + Manifest = noUrlManifestParser(JSON.stringify({display: 'fullscreen'})); + assert.equal(check.validate(Manifest), true, 'doesnt pass fullscreen'); + }); + it('fails invalid values', () => { + let Manifest; + Manifest = noUrlManifestParser(JSON.stringify({display: 'display'})); + assert.equal(check.validate(Manifest), false, 'doesnt fail display'); + Manifest = noUrlManifestParser(JSON.stringify({display: ''})); + assert.equal(check.validate(Manifest), false, 'doesnt fail empty string'); + }); + }); + + describe('icons checks', () => { + describe('icons exist check', () => { + it('fails when a manifest contains no icons array', () => { + const manifestSrc = JSON.stringify({ + name: 'NoIconsHere' + }); + const Manifest = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(Manifest); + const iconResults = results.allChecks.filter(i => i.id.includes('Icons')); + assert.equal(iconResults.every(i => i.passing === false), true); + }); + + it('fails when a manifest contains no icons', () => { + const manifestSrc = JSON.stringify({ + icons: [] + }); + const Manifest = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(Manifest); + const iconResults = results.allChecks.filter(i => i.id.includes('Icons')); + assert.equal(iconResults.every(i => i.passing === false), true); + }); + }); + + describe('icons at least X size check', () => { + it('fails when a manifest contains an icon with no size', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png' + }] + }); + const Manifest = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(Manifest); + const iconResults = results.allChecks.filter(i => i.id.includes('Icons')); + + assert.equal(iconResults.every(i => i.passing === false), true); + }); + + it('succeeds when there\'s one icon with multiple sizes, and one is valid', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png', + sizes: '72x72 96x96 128x128 256x256 1024x1024' + }] + }); + const Manifest = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(Manifest); + const iconResults = results.allChecks.filter(i => i.id.includes('Icons')); + + assert.equal(iconResults.every(i => i.passing === true), true); + }); + + it('succeeds when there\'s two icons, one without sizes; the other with a valid size', () => { + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon.png' + }, { + src: 'icon2.png', + sizes: '1256x1256' + }] + }); + const Manifest = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(Manifest); + const iconResults = results.allChecks.filter(i => i.id.includes('Icons')); + + assert.equal(iconResults.every(i => i.passing === true), true); + }); + + it('fails when an icon has a valid size, though it\'s non-square.', () => { + // See also: https://code.google.com/p/chromium/codesearch#chromium/src/chrome/browser/banners/app_banner_data_fetcher_unittest.cc&sq=package:chromium&type=cs&q=%22Non-square%20is%20okay%22%20file:%5Esrc/chrome/browser/banners/ + const manifestSrc = JSON.stringify({ + icons: [{ + src: 'icon-non-square.png', + sizes: '200x220' + }] + }); + const Manifest = noUrlManifestParser(manifestSrc); + const results = manifestValues.compute_(Manifest); + const iconResults = results.allChecks.filter(i => i.id.includes('Icons')); + + assert.equal(iconResults.every(i => i.passing === false), true); + }); + }); + }); +}); diff --git a/lighthouse-core/test/lib/manifest-parser-test.js b/lighthouse-core/test/lib/manifest-parser-test.js index 6017b2757d44..79834350cefe 100644 --- a/lighthouse-core/test/lib/manifest-parser-test.js +++ b/lighthouse-core/test/lib/manifest-parser-test.js @@ -90,11 +90,11 @@ describe('Manifest Parser', function() { assert.equal(icons.value.length, 1); }); - it('finds three icons in the stub manifest', function() { + it('finds four icons in the stub manifest', function() { const parsedManifest = manifestParser(JSON.stringify(manifestStub), EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL); assert(!parsedManifest.debugString); - assert.equal(parsedManifest.value.icons.value.length, 3); + assert.equal(parsedManifest.value.icons.value.length, 4); }); it('parses icons with extra whitespace', function() { @@ -250,10 +250,8 @@ describe('Manifest Parser', function() { assert.equal(parsedUrl.value, docUrl); }); - // TODO(bckenny): run these tests when we have a proper URL parser: - // https://github.com/GoogleChrome/lighthouse/issues/602 // 8.10(5) - it.skip('falls back to document URL and issues a warning for an invalid URL', () => { + it('falls back to document URL and issues a warning for an invalid URL', () => { // `new URL('/manifest.json', '')` is invalid and will throw. const manifestSrc = JSON.stringify({ start_url: '/manifest.json'