Skip to content

Commit

Permalink
Add audit to check if start_url is cached by SW (#2040)
Browse files Browse the repository at this point in the history
  • Loading branch information
wardpeet authored and paulirish committed May 9, 2017
1 parent d41ea07 commit 691157f
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 32 deletions.
3 changes: 2 additions & 1 deletion lighthouse-cli/test/smokehouse/pwa-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ module.exports = {
passName: 'offlinePass',
gatherers: [
'service-worker',
'offline'
'offline',
'start-url'
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-cli/test/smokehouse/pwa-expectations.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ module.exports = [
score: false
},
'webapp-install-banner': {
score: true
score: false
},
'splash-screen': {
score: true
Expand Down
10 changes: 9 additions & 1 deletion lighthouse-core/audits/webapp-install-banner.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class WebappInstallBanner extends MultiCheckAudit {
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/fundamentals/engage-and-retain/app-install-banners/) 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']
requiredArtifacts: ['URL', 'ServiceWorker', 'Manifest', 'StartUrl']
};
}

Expand Down Expand Up @@ -74,12 +74,20 @@ class WebappInstallBanner extends MultiCheckAudit {
}
}

static assessOfflineStartUrl(artifacts, failures) {
const hasOfflineStartUrl = artifacts.StartUrl === 200;
if (!hasOfflineStartUrl) {
failures.push('Start url is cached by a Service Worker');
}
}

static audit_(artifacts) {
const failures = [];

return artifacts.requestManifestValues(artifacts.Manifest).then(manifestValues => {
WebappInstallBanner.assessManifest(manifestValues, failures);
WebappInstallBanner.assessServiceWorker(artifacts, failures);
WebappInstallBanner.assessOfflineStartUrl(artifacts, failures);

return {
failures,
Expand Down
5 changes: 3 additions & 2 deletions lighthouse-core/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ module.exports = {
"useThrottling": false,
"gatherers": [
"service-worker",
"offline"
"offline",
"start-url",
]
},
{
"passName": "redirectPass",
"useThrottling": false,
"gatherers": [
"http-redirect",
"html-without-javascript"
"html-without-javascript",
]
}, {
"passName": "dbw",
Expand Down
22 changes: 22 additions & 0 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,28 @@ class Driver {
});
}

getAppManifest() {
return new Promise((resolve, reject) => {
this.sendCommand('Page.getAppManifest')
.then(response => {
// We're not reading `response.errors` however it may contain critical and noncritical
// errors from Blink's manifest parser:
// https://chromedevtools.github.io/debugger-protocol-viewer/tot/Page/#type-AppManifestError
if (!response.data) {
if (response.url) {
return reject(new Error(`Unable to retrieve manifest at ${response.url}.`));
}

// If both the data and the url are empty strings, the page had no manifest.
return reject('No web app manifest found.');
}

resolve(response);
})
.catch(err => reject(err));
});
}

getSecurityState() {
return new Promise((resolve, reject) => {
this.once('Security.securityStateChanged', data => {
Expand Down
17 changes: 6 additions & 11 deletions lighthouse-core/gather/gatherers/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,16 @@ class Manifest extends Gatherer {
* @return {!Promise<?Manifest>}
*/
afterPass(options) {
return options.driver.sendCommand('Page.getAppManifest')
return options.driver.getAppManifest()
.then(response => {
// We're not reading `response.errors` however it may contain critical and noncritical
// errors from Blink's manifest parser:
// https://chromedevtools.github.io/debugger-protocol-viewer/tot/Page/#type-AppManifestError
if (!response.data) {
if (response.url) {
throw new Error(`Unable to retrieve manifest at ${response.url}`);
}

// If both the data and the url are empty strings, the page had no manifest.
return manifestParser(response.data, response.url, options.url);
})
.catch(err => {
if (err === 'No web app manifest found.') {
return null;
}

return manifestParser(response.data, response.url, options.url);
return Promise.reject(err);
});
}
}
Expand Down
5 changes: 2 additions & 3 deletions lighthouse-core/gather/gatherers/offline.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ class Offline extends Gatherer {
record._fetchedViaServiceWorker;
}).pop(); // Take the last record that matches.

return options.driver.goOnline(options).then(_ => {
return navigationRecord ? navigationRecord.statusCode : -1;
});
return options.driver.goOnline(options)
.then(_ => navigationRecord ? navigationRecord.statusCode : -1);
}
}

Expand Down
94 changes: 94 additions & 0 deletions lighthouse-core/gather/gatherers/start-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* @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 Gatherer = require('./gatherer');
const URL = require('../../lib/url-shim');
const manifestParser = require('../../lib/manifest-parser');

class StartUrl extends Gatherer {
constructor() {
super();

this.startUrl = null;
this.err = null;
}

executeFetchRequest(driver, url) {
return new Promise((resolve, reject) => {
let requestId;
const fetchRequestId = (data) => {
if (URL.equalWithExcludedFragments(data.request.url, url)) {
requestId = data.requestId;
driver.off('Network.requestWillBeSent', fetchRequestId);
}
};
const fetchDone = (data) => {
if (data.requestId === requestId) {
driver.off('Network.loadingFinished', fetchDone);
driver.off('Network.loadingFailed', fetchDone);

resolve();
}
};

driver.on('Network.requestWillBeSent', fetchRequestId);
driver.on('Network.loadingFinished', fetchDone);
driver.on('Network.loadingFailed', fetchDone);
driver.evaluateAsync(
`fetch('${url}')
.then(response => response.status)
.catch(err => -1)`
).catch(err => reject(err));
});
}

pass(options) {
return options.driver.getAppManifest()
.then(response => {
return manifestParser(response.data, response.url, options.url);
})
.then(manifest => {
if (!manifest.value.start_url || !manifest.value.start_url.raw) {
return Promise.reject(new Error(`No web app manifest found on page ${options.url}`));
}

if (manifest.value.start_url.debugString) {
return Promise.reject(new Error(manifest.value.start_url.debugString));
}

this.startUrl = manifest.value.start_url.value;
}).then(_ => this.executeFetchRequest(options.driver, this.startUrl));
}

afterPass(options, tracingData) {
if (!this.startUrl) {
return Promise.reject(new Error('No start_url found inside the manifest'));
}

const networkRecords = tracingData.networkRecords;
const navigationRecord = networkRecords.filter(record => {
return URL.equalWithExcludedFragments(record._url, this.startUrl) &&
record._fetchedViaServiceWorker;
}).pop(); // Take the last record that matches.

return options.driver.goOnline(options)
.then(_ => navigationRecord ? navigationRecord.statusCode : -1);
}
}

module.exports = StartUrl;
15 changes: 15 additions & 0 deletions lighthouse-core/test/audits/webapp-install-banner-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function generateMockArtifacts() {
scriptURL: 'https://example.com/sw.js'
}]
},
StartUrl: 200,
URL: {finalUrl: 'https://example.com'}
}));
const mockArtifacts = Object.assign({}, computedArtifacts, clonedArtifacts);
Expand Down Expand Up @@ -128,11 +129,25 @@ describe('PWA: webapp install banner audit', () => {
it('fails if page had no SW', () => {
const artifacts = generateMockArtifacts();
artifacts.ServiceWorker.versions = [];
artifacts.StartUrl = -1;

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;
// start url will be -1 as well so failures will be 2
assert.strictEqual(failures.length, 2, failures);
});
});

it('fails if start_url is not cached', () => {
const artifacts = generateMockArtifacts();
artifacts.StartUrl = -1;

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);
});
});
Expand Down
21 changes: 8 additions & 13 deletions lighthouse-core/test/gather/gatherers/manifest-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('Manifest gatherer', () => {
it('returns an artifact', () => {
return manifestGather.afterPass({
driver: {
sendCommand() {
getAppManifest() {
return Promise.resolve({
data: '{}',
errors: [],
Expand All @@ -50,11 +50,10 @@ describe('Manifest gatherer', () => {
it('throws an error when unable to retrieve the manifest', () => {
return manifestGather.afterPass({
driver: {
sendCommand() {
return Promise.resolve({
errors: [],
url: EXAMPLE_MANIFEST_URL
});
getAppManifest() {
return Promise.reject(
new Error(`Unable to retrieve manifest at ${EXAMPLE_MANIFEST_URL}.`)
);
}
}
}).then(
Expand All @@ -65,12 +64,8 @@ describe('Manifest gatherer', () => {
it('returns null when the page had no manifest', () => {
return manifestGather.afterPass({
driver: {
sendCommand() {
return Promise.resolve({
data: '',
errors: [],
url: ''
});
getAppManifest() {
return Promise.reject('No web app manifest found.');
}
}
}).then(artifact => {
Expand All @@ -84,7 +79,7 @@ describe('Manifest gatherer', () => {
});
return manifestGather.afterPass({
driver: {
sendCommand() {
getAppManifest() {
return Promise.resolve({
errors: [],
data,
Expand Down
Loading

0 comments on commit 691157f

Please sign in to comment.