Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: special case AppManifest as baseArtifact #6957

Merged
merged 8 commits into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,19 +403,28 @@ class Driver {
/**
* @return {Promise<{url: string, data: string}|null>}
*/
getAppManifest() {
return 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 the data is empty, the page had no manifest.
return null;
}
async getAppManifest() {
this.setNextProtocolTimeout(3000);
const response = await this.sendCommand('Page.getAppManifest');
let data = response.data;

// 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 (!data) {
// If the data is empty, the page had no manifest.
return null;
}

return /** @type {Required<LH.Crdp.Page.GetAppManifestResponse>} */ (response);
});
const BOM_LENGTH = 3;
const BOM_FIRSTCHAR = 65279;
const isBomEncoded = data.charCodeAt(0) === BOM_FIRSTCHAR;

if (isBomEncoded) {
data = Buffer.from(data).slice(BOM_LENGTH).toString();
}

return {...response, data};
}

/**
Expand Down
30 changes: 27 additions & 3 deletions lighthouse-core/gather/gather-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
'use strict';

const log = require('lighthouse-logger');
const LHError = require('../lib/lh-error');
const URL = require('../lib/url-shim');
const manifestParser = require('../lib/manifest-parser.js');
const LHError = require('../lib/lh-error.js');
const URL = require('../lib/url-shim.js');
const NetworkRecorder = require('../lib/network-recorder.js');
const constants = require('../config/constants');
const constants = require('../config/constants.js');

const Driver = require('../gather/driver.js'); // eslint-disable-line no-unused-vars

Expand Down Expand Up @@ -400,6 +401,7 @@ class GatherRunner {
HostUserAgent: (await options.driver.getBrowserVersion()).userAgent,
NetworkUserAgent: '', // updated later
BenchmarkIndex: 0, // updated later
WebAppManifest: null, // updated later
traces: {},
devtoolsLogs: {},
settings: options.settings,
Expand All @@ -408,6 +410,24 @@ class GatherRunner {
};
}

/**
* Uses the debugger protocol to fetch the manifest from within the context of
* the target page, reusing any credentials, emulation, etc, already established
* there.
*
* Returns the parsed manifest or null if the page had no manifest. If the manifest
* was unparseable as JSON, manifest.value will be undefined and manifest.warning
* will have the reason. See manifest-parser.js for more information.
*
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts.Manifest|null>}
*/
static async getWebAppManifest(passContext) {
const response = await passContext.driver.getAppManifest();
if (!response) return null;
return manifestParser(response.data, response.url, passContext.url);
}

/**
* @param {Array<LH.Config.Pass>} passes
* @param {{driver: Driver, requestedUrl: string, settings: LH.Config.Settings}} options
Expand Down Expand Up @@ -437,6 +457,7 @@ class GatherRunner {
url: options.requestedUrl,
settings: options.settings,
passConfig,
baseArtifacts,
// *pass() functions and gatherers can push to this warnings array.
LighthouseRunWarnings: baseArtifacts.LighthouseRunWarnings,
};
Expand All @@ -448,6 +469,9 @@ class GatherRunner {
}
await GatherRunner.beforePass(passContext, gathererResults);
await GatherRunner.pass(passContext, gathererResults);
if (isFirstPass) {
baseArtifacts.WebAppManifest = await GatherRunner.getWebAppManifest(passContext);
}
const passData = await GatherRunner.afterPass(passContext, gathererResults);

// Save devtoolsLog, but networkRecords are discarded and not added onto artifacts.
Expand Down
27 changes: 2 additions & 25 deletions lighthouse-core/gather/gatherers/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,17 @@
'use strict';

const Gatherer = require('./gatherer');
const manifestParser = require('../../lib/manifest-parser');
const BOM_LENGTH = 3;
const BOM_FIRSTCHAR = 65279;

/**
* Uses the debugger protocol to fetch the manifest from within the context of
* the target page, reusing any credentials, emulation, etc, already established
* there. The artifact produced is the fetched string, if any, passed through
* the manifest parser.
* TODO(phulce): delete this file and move usages over to WebAppManifest
*/
class Manifest extends Gatherer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to keep this? TODO for followup to split up the changes nicely? Or is it worth keeping for some reason?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh, I'll just do it now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boy am I regretting that 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I'm gonna make this a TODO

/**
* Returns the parsed manifest or null if the page had no manifest. If the manifest
* was unparseable as JSON, manifest.value will be undefined and manifest.warning
* will have the reason. See manifest-parser.js for more information.
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['Manifest']>}
*/
async afterPass(passContext) {
const manifestPromise = passContext.driver.getAppManifest();
/** @type {Promise<void>} */
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000));

const response = await Promise.race([manifestPromise, timeoutPromise]);
if (!response) {
return null;
}

const isBomEncoded = response.data.charCodeAt(0) === BOM_FIRSTCHAR;
if (isBomEncoded) {
response.data = Buffer.from(response.data).slice(BOM_LENGTH).toString();
}

return manifestParser(response.data, response.url, passContext.url);
return passContext.baseArtifacts.WebAppManifest;
}
}

Expand Down
30 changes: 12 additions & 18 deletions lighthouse-core/gather/gatherers/start-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
'use strict';

const Gatherer = require('./gatherer');
const manifestParser = require('../../lib/manifest-parser');

/** @typedef {import('../driver.js')} Driver */

Expand All @@ -16,27 +15,21 @@ class StartUrl extends Gatherer {
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['StartUrl']>}
*/
afterPass(passContext) {
const driver = passContext.driver;
return driver.goOnline(passContext)
.then(() => driver.getAppManifest())
.then(response => driver.goOffline().then(() => response))
.then(response => response && manifestParser(response.data, response.url, passContext.url))
.then(manifest => {
const startUrlInfo = this._readManifestStartUrl(manifest);
if (startUrlInfo.isReadFailure) {
return {statusCode: -1, explanation: startUrlInfo.reason};
}
async afterPass(passContext) {
const manifest = passContext.baseArtifacts.WebAppManifest;
const startUrlInfo = this._readManifestStartUrl(manifest);
if (startUrlInfo.isReadFailure) {
return {statusCode: -1, explanation: startUrlInfo.reason};
}

return this._attemptManifestFetch(passContext.driver, startUrlInfo.startUrl);
}).catch(() => {
return {statusCode: -1, explanation: 'Unable to fetch start URL via service worker.'};
});
return this._attemptStartURLFetch(passContext.driver, startUrlInfo.startUrl).catch(() => {
return {statusCode: -1, explanation: 'Unable to fetch start URL via service worker.'};
});
}

/**
* Read the parsed manifest and return failure reasons or the startUrl
* @param {?{value?: {start_url: {value: string, warning?: string}}, warning?: string}} manifest
* @param {LH.Artifacts.Manifest|null} manifest
* @return {{isReadFailure: true, reason: string}|{isReadFailure: false, startUrl: string}}
*/
_readManifestStartUrl(manifest) {
Expand All @@ -61,7 +54,8 @@ class StartUrl extends Gatherer {
* @param {string} startUrl
* @return {Promise<{statusCode: number, explanation: string}>}
*/
_attemptManifestFetch(driver, startUrl) {
_attemptStartURLFetch(driver, startUrl) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, whoops

// TODO(phulce): clean up this setTimeout once the response has happened
// Wait up to 3s to get a matched network request from the fetch() to work
const timeoutPromise = new Promise(resolve =>
setTimeout(
Expand Down
38 changes: 35 additions & 3 deletions lighthouse-core/test/gather/driver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,11 @@ function sendCommandStub(command, params) {
/* eslint-env jest */

let driverStub;
let connectionStub;
beforeEach(() => {
const connection = new Connection();
connection.sendCommand = sendCommandStub;
driverStub = new Driver(connection);
connectionStub = new Connection();
connectionStub.sendCommand = sendCommandStub;
driverStub = new Driver(connectionStub);
});

describe('Browser Driver', () => {
Expand Down Expand Up @@ -329,11 +330,42 @@ describe('Browser Driver', () => {
assert.equal(sendCommandParams[0], undefined);
});
});

describe('.getAppManifest', () => {
it('should return null when no manifest', async () => {
connectionStub.sendCommand = jest.fn()
.mockResolvedValueOnce({data: undefined, url: '/manifest'});
const result = await driverStub.getAppManifest();
expect(result).toEqual(null);
});

it('should return the manifest', async () => {
const manifest = {name: 'The App'};
connectionStub.sendCommand = jest.fn()
.mockResolvedValueOnce({data: JSON.stringify(manifest), url: '/manifest'});
const result = await driverStub.getAppManifest();
expect(result).toEqual({data: JSON.stringify(manifest), url: '/manifest'});
});

it('should handle BOM-encoded manifest', async () => {
const fs = require('fs');
const manifestWithoutBOM = fs.readFileSync(__dirname + '/../fixtures/manifest.json')
.toString();
const manifestWithBOM = fs.readFileSync(__dirname + '/../fixtures/manifest-bom.json')
.toString();

connectionStub.sendCommand = jest.fn()
.mockResolvedValueOnce({data: manifestWithBOM, url: '/manifest'});
const result = await driverStub.getAppManifest();
expect(result).toEqual({data: manifestWithoutBOM, url: '/manifest'});
});
});
});

describe('Multiple tab check', () => {
beforeEach(() => {
sendCommandParams = [];
sendCommandMockResponses.clear();
});

it('will pass if there are no current service workers', () => {
Expand Down
3 changes: 3 additions & 0 deletions lighthouse-core/test/gather/fake-driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const fakeDriver = {
getBenchmarkIndex() {
return Promise.resolve(125.2);
},
getAppManifest() {
return Promise.resolve(null);
},
connect() {
return Promise.resolve();
},
Expand Down
32 changes: 32 additions & 0 deletions lighthouse-core/test/gather/gather-runner-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1070,4 +1070,36 @@ describe('GatherRunner', function() {
});
});
});

describe('.getWebAppManifest', () => {
const MANIFEST_URL = 'https://example.com/manifest.json';
let passContext;

beforeEach(() => {
passContext = {
url: 'https://example.com/index.html',
baseArtifacts: {},
driver: fakeDriver,
};
});

it('should pass through manifest when null', async () => {
const getAppManifest = jest.spyOn(fakeDriver, 'getAppManifest');
getAppManifest.mockResolvedValueOnce(null);
const result = await GatherRunner.getWebAppManifest(passContext);
expect(result).toEqual(null);
});

it('should parse the manifest when found', async () => {
const manifest = {name: 'App'};
const getAppManifest = jest.spyOn(fakeDriver, 'getAppManifest');
getAppManifest.mockResolvedValueOnce({data: JSON.stringify(manifest), url: MANIFEST_URL});
const result = await GatherRunner.getWebAppManifest(passContext);
expect(result).toHaveProperty('raw', JSON.stringify(manifest));
expect(result.value).toMatchObject({
name: {value: 'App', raw: 'App'},
start_url: {value: passContext.url, raw: undefined},
});
});
});
});
Loading