From 2230f6a429df2dd52b4f7702a722735ad8adf627 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Mon, 21 Mar 2016 16:06:14 -0700 Subject: [PATCH 01/14] intial commit of manifest-parser as helper --- helpers/manifest-parser.js | 329 ++++++++++++++++++++++++++++++++++ test/manifest-parser-tests.js | 115 ++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 helpers/manifest-parser.js create mode 100644 test/manifest-parser-tests.js diff --git a/helpers/manifest-parser.js b/helpers/manifest-parser.js new file mode 100644 index 000000000000..0113d5b0d535 --- /dev/null +++ b/helpers/manifest-parser.js @@ -0,0 +1,329 @@ +/** + * 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'; + +var ManifestParser = (function() { + + var _jsonInput = {}; + var _manifest = {}; + var _logs = []; + var _tips = []; + var _success = true; + + var ALLOWED_DISPLAY_VALUES = ['fullscreen', + 'standalone', + 'minimal-ui', + 'browser']; + + var ALLOWED_ORIENTATION_VALUES = ['any', + 'natural', + 'landscape', + 'portrait', + 'portrait-primary', + 'portrait-secondary', + 'landscape-primary', + 'landscape-secondary']; + + function _parseString(args) { + var object = args.object; + var property = args.property; + if (!(property in object)) { + return undefined; + } + + if (typeof object[property] != 'string') { + _logs.push('ERROR: \'' + property + + '\' expected to be a string but is not.'); + return undefined; + } + + if (args.trim) { + return object[property].trim(); + } + return object[property]; + } + + function _parseBoolean(args) { + var object = args.object; + var property = args.property; + var defaultValue = args.defaultValue; + if (!(property in object)) { + return defaultValue; + } + + if (typeof object[property] != 'boolean') { + _logs.push('ERROR: \'' + property + + '\' expected to be a boolean but is not.'); + return defaultValue; + } + + return object[property]; + } + + function _parseURL(args) { + var object = args.object; + var property = args.property; + var baseURL = args.baseURL; + + var str = _parseString({object: object, property: property, trim: false}); + if (str === undefined) { + return undefined; + } + + // TODO: resolve url using baseURL + // new URL(object[property], baseURL); + return object[property]; + } + + function _parseColor(args) { + var object = args.object; + var property = args.property; + if (!(property in object)) { + return undefined; + } + + if (typeof object[property] != 'string') { + _logs.push('ERROR: \'' + property + + '\' expected to be a string but is not.'); + return undefined; + } + + // If style.color changes when set to the given color, it is valid. Testing + // against 'white' and 'black' in case of the given color is one of them. + var dummy = document.createElement('div'); + dummy.style.color = 'white'; + dummy.style.color = object[property]; + if (dummy.style.color != 'white') { + return object[property]; + } + dummy.style.color = 'black'; + dummy.style.color = object[property]; + if (dummy.style.color != 'black') { + return object[property]; + } + return undefined; + } + + function _parseName() { + return _parseString({object: _jsonInput, property: 'name', trim: true}); + } + + function _parseShortName() { + return _parseString({object: _jsonInput, + property: 'short_name', + trim: true}); + } + + function _parseStartUrl() { + // TODO: parse url using manifest_url as a base (missing). + return _parseURL({object: _jsonInput, property: 'start_url'}); + } + + function _parseDisplay() { + var display = _parseString({object: _jsonInput, + property: 'display', + trim: true}); + if (display === undefined) { + return display; + } + + if (ALLOWED_DISPLAY_VALUES.indexOf(display.toLowerCase()) == -1) { + _logs.push('ERROR: \'display\' has an invalid value, will be ignored.'); + return undefined; + } + + return display; + } + + function _parseOrientation() { + var orientation = _parseString({object: _jsonInput, + property: 'orientation', + trim: true}); + if (orientation === undefined) { + return orientation; + } + + if (ALLOWED_ORIENTATION_VALUES.indexOf(orientation.toLowerCase()) == -1) { + _logs.push('ERROR: \'orientation\' has an invalid value' + + ', will be ignored.'); + return undefined; + } + + return orientation; + } + + function _parseIcons() { + var property = 'icons'; + var icons = []; + + if (!(property in _jsonInput)) { + return icons; + } + + if (!Array.isArray(_jsonInput[property])) { + _logs.push('ERROR: \'' + property + + '\' expected to be an array but is not.'); + return icons; + } + + _jsonInput[property].forEach(function(object) { + var icon = {}; + if (!('src' in object)) { + return; + } + // TODO: pass manifest url as base. + icon.src = _parseURL({object: object, property: 'src'}); + icon.type = _parseString({object: object, + property: 'type', + trim: true}); + + icon.density = parseFloat(object.density); + if (isNaN(icon.density) || !isFinite(icon.density) || icon.density <= 0) { + icon.density = 1.0; + } + + if ('sizes' in object) { + var set = new Set(); + var link = document.createElement('link'); + link.sizes = object.sizes; + + for (var i = 0; i < link.sizes.length; ++i) { + set.add(link.sizes.item(i).toLowerCase()); + } + + if (set.size != 0) { + icon.sizes = set; + } + } + + icons.push(icon); + }); + + return icons; + } + + function _parseRelatedApplications() { + var property = 'related_applications'; + var applications = []; + + if (!(property in _jsonInput)) { + return applications; + } + + if (!Array.isArray(_jsonInput[property])) { + _logs.push('ERROR: \'' + property + + '\' expected to be an array but is not.'); + return applications; + } + + _jsonInput[property].forEach(function(object) { + var application = {}; + application.platform = _parseString({object: object, + property: 'platform', + trim: true}); + application.id = _parseString({object: object, + property: 'id', + trim: true}); + // TODO: pass manfiest url as base. + application.url = _parseURL({object: object, property: 'url'}); + applications.push(application); + }); + + return applications; + } + + function _parsePreferRelatedApplications() { + return _parseBoolean({object: _jsonInput, + property: 'prefer_related_applications', + defaultValue: false}); + } + + function _parseThemeColor() { + return _parseColor({object: _jsonInput, property: 'theme_color'}); + } + + function _parseBackgroundColor() { + return _parseColor({object: _jsonInput, property: 'background_color'}); + } + + function _parse(string) { + // TODO: temporary while ManifestParser is a collection of static methods. + _logs = []; + _tips = []; + _success = true; + + try { + _jsonInput = JSON.parse(string); + } catch (e) { + _logs.push('File isn\'t valid JSON: ' + e); + _tips.push('Your JSON failed to parse, these are the main reasons why ' + + 'JSON parsing usually fails:\n' + + '- Double quotes should be used around property names and for ' + + 'strings. Single quotes are not valid.\n' + + '- JSON specification disallow trailing comma after the last ' + + 'property even if some implementations allow it.'); + + _success = false; + return; + } + + _logs.push('JSON parsed successfully.'); + + _manifest.name = _parseName(); + /* eslint-disable camelcase */ + _manifest.short_name = _parseShortName(); + _manifest.start_url = _parseStartUrl(); + _manifest.display = _parseDisplay(); + _manifest.orientation = _parseOrientation(); + _manifest.icons = _parseIcons(); + _manifest.related_applications = _parseRelatedApplications(); + _manifest.prefer_related_applications = _parsePreferRelatedApplications(); + _manifest.theme_color = _parseThemeColor(); + _manifest.background_color = _parseBackgroundColor(); + + _logs.push('Parsed `name` property is: ' + + _manifest.name); + _logs.push('Parsed `short_name` property is: ' + + _manifest.short_name); + _logs.push('Parsed `start_url` property is: ' + + _manifest.start_url); + _logs.push('Parsed `display` property is: ' + + _manifest.display); + _logs.push('Parsed `orientation` property is: ' + + _manifest.orientation); + _logs.push('Parsed `icons` property is: ' + + JSON.stringify(_manifest.icons, null, 4)); + _logs.push('Parsed `related_applications` property is: ' + + JSON.stringify(_manifest.related_applications, null, 4)); + _logs.push('Parsed `prefer_related_applications` property is: ' + + JSON.stringify(_manifest.prefer_related_applications, null, 4)); + _logs.push('Parsed `theme_color` property is: ' + + _manifest.theme_color); + _logs.push('Parsed `background_color` property is: ' + + _manifest.background_color); + /* eslint-enable camelcase */ + } + + return { + parse: _parse, + manifest: function() { return _manifest; }, + logs: function() { return _logs; }, + tips: function() { return _tips; }, + success: function() { return _success; } + }; +})(); + +module.exports = ManifestParser; diff --git a/test/manifest-parser-tests.js b/test/manifest-parser-tests.js new file mode 100644 index 000000000000..c70e59b1a2a0 --- /dev/null +++ b/test/manifest-parser-tests.js @@ -0,0 +1,115 @@ +/** + * 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'; + +/* global describe, it */ + +var ManifestParser = require('../helpers/manifest-parser'); +var assert = require('assert'); + +describe('Manifest Parser', function() { + it('should not parse empty string input', function() { + ManifestParser.parse(''); + assert.equal(false, ManifestParser.success()); + }); + + it('has empty values when parsing empty manifest', function() { + ManifestParser.parse(''); + assert.equal(null, ManifestParser.manifest().name); + assert.equal(null, ManifestParser.manifest().short_name); + assert.equal(null, ManifestParser.manifest().start_url); + assert.equal(null, ManifestParser.manifest().display); + assert.equal(null, ManifestParser.manifest().orientation); + assert.equal(null, ManifestParser.manifest().theme_color); + assert.equal(null, ManifestParser.manifest().background_color); + }); + + it('accepts empty dictionary', function() { + ManifestParser.parse('{}'); + assert.equal(true, ManifestParser.success()); + assert.equal(null, ManifestParser.manifest().name); + assert.equal(null, ManifestParser.manifest().short_name); + assert.equal(null, ManifestParser.manifest().start_url); + assert.equal(null, ManifestParser.manifest().display); + assert.equal(null, ManifestParser.manifest().orientation); + assert.equal(null, ManifestParser.manifest().theme_color); + assert.equal(null, ManifestParser.manifest().background_color); + // TODO: + // icons + // related_applications + // prefer_related_applications + }); + + it('accepts unknown values', function() { + ManifestParser.parse('{}'); + assert.equal(true, ManifestParser.success()); + assert.equal(null, ManifestParser.manifest().name); + assert.equal(null, ManifestParser.manifest().short_name); + assert.equal(null, ManifestParser.manifest().start_url); + assert.equal(null, ManifestParser.manifest().display); + assert.equal(null, ManifestParser.manifest().orientation); + assert.equal(null, ManifestParser.manifest().theme_color); + assert.equal(null, ManifestParser.manifest().background_color); + }); + + describe('name parsing', function() { + it('it parses basic string', function() { + ManifestParser.parse('{"name":"foo"}'); + assert.equal(true, ManifestParser.success()); + assert.equal('foo', ManifestParser.manifest().name); + }); + + it('it trims whitespaces', function() { + ManifestParser.parse('{"name":" foo "}'); + assert.equal(true, ManifestParser.success()); + assert.equal('foo', ManifestParser.manifest().name); + }); + + it('doesn\'t parse non-string', function() { + ManifestParser.parse('{"name": {} }'); + assert.equal(true, ManifestParser.success()); + assert.equal(null, ManifestParser.manifest().name); + + ManifestParser.parse('{"name": 42 }'); + assert.equal(true, ManifestParser.success()); + assert.equal(null, ManifestParser.manifest().name); + }); + }); + + describe('short_name parsing', function() { + it('it parses basic string', function() { + ManifestParser.parse('{"short_name":"foo"}'); + assert.equal(true, ManifestParser.success()); + assert.equal('foo', ManifestParser.manifest().short_name); + }); + + it('it trims whitespaces', function() { + ManifestParser.parse('{"short_name":" foo "}'); + assert.equal(true, ManifestParser.success()); + assert.equal('foo', ManifestParser.manifest().short_name); + }); + + it('doesn\'t parse non-string', function() { + ManifestParser.parse('{"short_name": {} }'); + assert.equal(true, ManifestParser.success()); + assert.equal(null, ManifestParser.manifest().short_name); + + ManifestParser.parse('{"short_name": 42 }'); + assert.equal(true, ManifestParser.success()); + assert.equal(null, ManifestParser.manifest().short_name); + }); + }); +}); From 0382fe3c08d99e0acbacc9a2a7c551588f7b88e0 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Mon, 21 Mar 2016 16:07:29 -0700 Subject: [PATCH 02/14] update manifest-parser tests to use expect --- test/manifest-parser-tests.js | 83 ++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/test/manifest-parser-tests.js b/test/manifest-parser-tests.js index c70e59b1a2a0..44d6d4aa766d 100644 --- a/test/manifest-parser-tests.js +++ b/test/manifest-parser-tests.js @@ -18,35 +18,35 @@ /* global describe, it */ var ManifestParser = require('../helpers/manifest-parser'); -var assert = require('assert'); +var expect = require('chai').expect; describe('Manifest Parser', function() { it('should not parse empty string input', function() { ManifestParser.parse(''); - assert.equal(false, ManifestParser.success()); + expect(ManifestParser.success()).to.equal(false); }); it('has empty values when parsing empty manifest', function() { ManifestParser.parse(''); - assert.equal(null, ManifestParser.manifest().name); - assert.equal(null, ManifestParser.manifest().short_name); - assert.equal(null, ManifestParser.manifest().start_url); - assert.equal(null, ManifestParser.manifest().display); - assert.equal(null, ManifestParser.manifest().orientation); - assert.equal(null, ManifestParser.manifest().theme_color); - assert.equal(null, ManifestParser.manifest().background_color); + expect(ManifestParser.manifest().name).to.equal(undefined); + expect(ManifestParser.manifest().short_name).to.equal(undefined); + expect(ManifestParser.manifest().start_url).to.equal(undefined); + expect(ManifestParser.manifest().display).to.equal(undefined); + expect(ManifestParser.manifest().orientation).to.equal(undefined); + expect(ManifestParser.manifest().theme_color).to.equal(undefined); + expect(ManifestParser.manifest().background_color).to.equal(undefined); }); it('accepts empty dictionary', function() { ManifestParser.parse('{}'); - assert.equal(true, ManifestParser.success()); - assert.equal(null, ManifestParser.manifest().name); - assert.equal(null, ManifestParser.manifest().short_name); - assert.equal(null, ManifestParser.manifest().start_url); - assert.equal(null, ManifestParser.manifest().display); - assert.equal(null, ManifestParser.manifest().orientation); - assert.equal(null, ManifestParser.manifest().theme_color); - assert.equal(null, ManifestParser.manifest().background_color); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().name).to.equal(undefined); + expect(ManifestParser.manifest().short_name).to.equal(undefined); + expect(ManifestParser.manifest().start_url).to.equal(undefined); + expect(ManifestParser.manifest().display).to.equal(undefined); + expect(ManifestParser.manifest().orientation).to.equal(undefined); + expect(ManifestParser.manifest().theme_color).to.equal(undefined); + expect(ManifestParser.manifest().background_color).to.equal(undefined); // TODO: // icons // related_applications @@ -54,62 +54,63 @@ describe('Manifest Parser', function() { }); it('accepts unknown values', function() { + // TODO(bckenny): this is the same exact test as above ManifestParser.parse('{}'); - assert.equal(true, ManifestParser.success()); - assert.equal(null, ManifestParser.manifest().name); - assert.equal(null, ManifestParser.manifest().short_name); - assert.equal(null, ManifestParser.manifest().start_url); - assert.equal(null, ManifestParser.manifest().display); - assert.equal(null, ManifestParser.manifest().orientation); - assert.equal(null, ManifestParser.manifest().theme_color); - assert.equal(null, ManifestParser.manifest().background_color); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().name).to.equal(undefined); + expect(ManifestParser.manifest().short_name).to.equal(undefined); + expect(ManifestParser.manifest().start_url).to.equal(undefined); + expect(ManifestParser.manifest().display).to.equal(undefined); + expect(ManifestParser.manifest().orientation).to.equal(undefined); + expect(ManifestParser.manifest().theme_color).to.equal(undefined); + expect(ManifestParser.manifest().background_color).to.equal(undefined); }); describe('name parsing', function() { it('it parses basic string', function() { ManifestParser.parse('{"name":"foo"}'); - assert.equal(true, ManifestParser.success()); - assert.equal('foo', ManifestParser.manifest().name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().name).to.equal('foo'); }); it('it trims whitespaces', function() { ManifestParser.parse('{"name":" foo "}'); - assert.equal(true, ManifestParser.success()); - assert.equal('foo', ManifestParser.manifest().name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().name).to.equal('foo'); }); it('doesn\'t parse non-string', function() { ManifestParser.parse('{"name": {} }'); - assert.equal(true, ManifestParser.success()); - assert.equal(null, ManifestParser.manifest().name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().name).to.equal(undefined); ManifestParser.parse('{"name": 42 }'); - assert.equal(true, ManifestParser.success()); - assert.equal(null, ManifestParser.manifest().name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().name).to.equal(undefined); }); }); describe('short_name parsing', function() { it('it parses basic string', function() { ManifestParser.parse('{"short_name":"foo"}'); - assert.equal(true, ManifestParser.success()); - assert.equal('foo', ManifestParser.manifest().short_name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().short_name).to.equal('foo'); }); it('it trims whitespaces', function() { ManifestParser.parse('{"short_name":" foo "}'); - assert.equal(true, ManifestParser.success()); - assert.equal('foo', ManifestParser.manifest().short_name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().short_name).to.equal('foo'); }); it('doesn\'t parse non-string', function() { ManifestParser.parse('{"short_name": {} }'); - assert.equal(true, ManifestParser.success()); - assert.equal(null, ManifestParser.manifest().short_name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().short_name).to.equal(undefined); ManifestParser.parse('{"short_name": 42 }'); - assert.equal(true, ManifestParser.success()); - assert.equal(null, ManifestParser.manifest().short_name); + expect(ManifestParser.success()).to.equal(true); + expect(ManifestParser.manifest().short_name).to.equal(undefined); }); }); }); From 988edffa7f26a27d0e5f9aad64ab3dac0d8d7287 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Mon, 21 Mar 2016 16:09:41 -0700 Subject: [PATCH 03/14] manifest-parser eslint fixes --- helpers/manifest-parser.js | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/helpers/manifest-parser.js b/helpers/manifest-parser.js index 0113d5b0d535..0c82f9c269bc 100644 --- a/helpers/manifest-parser.js +++ b/helpers/manifest-parser.js @@ -15,8 +15,9 @@ */ 'use strict'; -var ManifestParser = (function() { +/* global document */ +var ManifestParser = (function() { var _jsonInput = {}; var _manifest = {}; var _logs = []; @@ -44,7 +45,7 @@ var ManifestParser = (function() { return undefined; } - if (typeof object[property] != 'string') { + if (typeof object[property] !== 'string') { _logs.push('ERROR: \'' + property + '\' expected to be a string but is not.'); return undefined; @@ -64,7 +65,7 @@ var ManifestParser = (function() { return defaultValue; } - if (typeof object[property] != 'boolean') { + if (typeof object[property] !== 'boolean') { _logs.push('ERROR: \'' + property + '\' expected to be a boolean but is not.'); return defaultValue; @@ -76,7 +77,6 @@ var ManifestParser = (function() { function _parseURL(args) { var object = args.object; var property = args.property; - var baseURL = args.baseURL; var str = _parseString({object: object, property: property, trim: false}); if (str === undefined) { @@ -84,6 +84,7 @@ var ManifestParser = (function() { } // TODO: resolve url using baseURL + // var baseURL = args.baseURL; // new URL(object[property], baseURL); return object[property]; } @@ -95,7 +96,7 @@ var ManifestParser = (function() { return undefined; } - if (typeof object[property] != 'string') { + if (typeof object[property] !== 'string') { _logs.push('ERROR: \'' + property + '\' expected to be a string but is not.'); return undefined; @@ -106,12 +107,12 @@ var ManifestParser = (function() { var dummy = document.createElement('div'); dummy.style.color = 'white'; dummy.style.color = object[property]; - if (dummy.style.color != 'white') { + if (dummy.style.color !== 'white') { return object[property]; } dummy.style.color = 'black'; dummy.style.color = object[property]; - if (dummy.style.color != 'black') { + if (dummy.style.color !== 'black') { return object[property]; } return undefined; @@ -140,7 +141,7 @@ var ManifestParser = (function() { return display; } - if (ALLOWED_DISPLAY_VALUES.indexOf(display.toLowerCase()) == -1) { + if (ALLOWED_DISPLAY_VALUES.indexOf(display.toLowerCase()) === -1) { _logs.push('ERROR: \'display\' has an invalid value, will be ignored.'); return undefined; } @@ -156,7 +157,7 @@ var ManifestParser = (function() { return orientation; } - if (ALLOWED_ORIENTATION_VALUES.indexOf(orientation.toLowerCase()) == -1) { + if (ALLOWED_ORIENTATION_VALUES.indexOf(orientation.toLowerCase()) === -1) { _logs.push('ERROR: \'orientation\' has an invalid value' + ', will be ignored.'); return undefined; @@ -204,7 +205,7 @@ var ManifestParser = (function() { set.add(link.sizes.item(i).toLowerCase()); } - if (set.size != 0) { + if (set.size !== 0) { icon.sizes = set; } } @@ -319,10 +320,10 @@ var ManifestParser = (function() { return { parse: _parse, - manifest: function() { return _manifest; }, - logs: function() { return _logs; }, - tips: function() { return _tips; }, - success: function() { return _success; } + manifest: _ => _manifest, + logs: _ => _logs, + tips: _ => _tips, + success: _ => _success }; })(); From eea93d2261be535283dafb0a3fde83ee1cc78ff9 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Tue, 22 Mar 2016 00:01:12 -0700 Subject: [PATCH 04/14] update manifest parser to return more detailed parsing information --- helpers/manifest-parser.js | 538 +++++++++++++++++----------------- test/manifest-parser-tests.js | 101 +++---- 2 files changed, 311 insertions(+), 328 deletions(-) diff --git a/helpers/manifest-parser.js b/helpers/manifest-parser.js index 0c82f9c269bc..6e496a30ab74 100644 --- a/helpers/manifest-parser.js +++ b/helpers/manifest-parser.js @@ -15,316 +15,310 @@ */ 'use strict'; -/* global document */ - -var ManifestParser = (function() { - var _jsonInput = {}; - var _manifest = {}; - var _logs = []; - var _tips = []; - var _success = true; - - var ALLOWED_DISPLAY_VALUES = ['fullscreen', - 'standalone', - 'minimal-ui', - 'browser']; - - var ALLOWED_ORIENTATION_VALUES = ['any', - 'natural', - 'landscape', - 'portrait', - 'portrait-primary', - 'portrait-secondary', - 'landscape-primary', - 'landscape-secondary']; - - function _parseString(args) { - var object = args.object; - var property = args.property; - if (!(property in object)) { - return undefined; +const ALLOWED_DISPLAY_VALUES = [ + 'fullscreen', + 'standalone', + 'minimal-ui', + 'browser' +]; + +const ALLOWED_ORIENTATION_VALUES = [ + 'any', + 'natural', + 'landscape', + 'portrait', + 'portrait-primary', + 'portrait-secondary', + 'landscape-primary', + 'landscape-secondary' +]; + +function parseString(raw, trim) { + let value; + let warning; + + if (typeof raw === 'string') { + value = trim ? raw.trim() : raw; + } else { + if (raw !== undefined) { + warning = 'ERROR: expected a string.'; } + value = undefined; + } - if (typeof object[property] !== 'string') { - _logs.push('ERROR: \'' + property + - '\' expected to be a string but is not.'); - return undefined; - } + return { + raw, + value, + warning + }; +} - if (args.trim) { - return object[property].trim(); - } - return object[property]; - } +function parseURL(raw) { + // TODO: resolve url using baseURL + // var baseURL = args.baseURL; + // new URL(parseString(raw).value, baseURL); + return parseString(raw, true); +} - function _parseBoolean(args) { - var object = args.object; - var property = args.property; - var defaultValue = args.defaultValue; - if (!(property in object)) { - return defaultValue; - } +function parseColor(raw) { + let color = parseString(raw); - if (typeof object[property] !== 'boolean') { - _logs.push('ERROR: \'' + property + - '\' expected to be a boolean but is not.'); - return defaultValue; - } + if (color.value === undefined) { + return color; + } - return object[property]; + // TODO(bckenny): validate color, but for reals + // If style.color changes when set to the given color, it is valid. Testing + // against 'white' and 'black' in case of the given color is one of them. + // var dummy = document.createElement('div'); + // dummy.style.color = 'white'; + // dummy.style.color = color.value; + // if (dummy.style.color !== 'white') { + // return color; + // } + // dummy.style.color = 'black'; + // dummy.style.color = color.value; + // if (dummy.style.color !== 'black') { + // return color; + // } + return color; + + // color.value = undefined; + // color.warning = 'ERROR: color parsing failed'; + + // return color; +} + +function parseName(jsonInput) { + return parseString(jsonInput.name, true); +} + +function parseShortName(jsonInput) { + return parseString(jsonInput.short_name, true); +} + +function parseStartUrl(jsonInput) { + // TODO: parse url using manifest_url as a base (missing). + // start_url must be same-origin as Document of the top-level browsing context. + return parseURL(jsonInput.start_url); +} + +function parseDisplay(jsonInput) { + var display = parseString(jsonInput.display, true); + + if (display.value && ALLOWED_DISPLAY_VALUES.indexOf(display.value.toLowerCase()) === -1) { + display.value = undefined; + display.warning = 'ERROR: \'display\' has an invalid value, will be ignored.'; } - function _parseURL(args) { - var object = args.object; - var property = args.property; + return display; +} - var str = _parseString({object: object, property: property, trim: false}); - if (str === undefined) { - return undefined; - } +function parseOrientation(jsonInput) { + var orientation = parseString(jsonInput.orientation, true); - // TODO: resolve url using baseURL - // var baseURL = args.baseURL; - // new URL(object[property], baseURL); - return object[property]; + if (orientation.value && + ALLOWED_ORIENTATION_VALUES.indexOf(orientation.value.toLowerCase()) === -1) { + orientation.value = undefined; + orientation.warning = 'ERROR: \'orientation\' has an invalid value, will be ignored.'; } - function _parseColor(args) { - var object = args.object; - var property = args.property; - if (!(property in object)) { - return undefined; - } + return orientation; +} - if (typeof object[property] !== 'string') { - _logs.push('ERROR: \'' + property + - '\' expected to be a string but is not.'); - return undefined; - } +function parseIcon(raw) { + // TODO: pass manifest url as base. + let src = parseURL(raw.src); + let type = parseString(raw.type, true); - // If style.color changes when set to the given color, it is valid. Testing - // against 'white' and 'black' in case of the given color is one of them. - var dummy = document.createElement('div'); - dummy.style.color = 'white'; - dummy.style.color = object[property]; - if (dummy.style.color !== 'white') { - return object[property]; - } - dummy.style.color = 'black'; - dummy.style.color = object[property]; - if (dummy.style.color !== 'black') { - return object[property]; + let density = { + raw: raw.density, + value: 1, + warning: undefined + }; + if (density.raw !== undefined) { + density.value = parseFloat(density.raw); + if (isNaN(density.value) || !isFinite(density.value) || density.value <= 0) { + density.value = 1; + density.warning = 'ERROR: icon density cannot be NaN, +∞, or less than or equal to +0.'; } - return undefined; } - function _parseName() { - return _parseString({object: _jsonInput, property: 'name', trim: true}); - } + let sizes = parseString(raw.sizes); + if (sizes.value !== undefined) { + let set = new Set(); + sizes.value.split(/\s/).forEach(size => set.add(size.toLowerCase())); - function _parseShortName() { - return _parseString({object: _jsonInput, - property: 'short_name', - trim: true}); + sizes.value = set.size > 0 ? set : undefined; } - function _parseStartUrl() { - // TODO: parse url using manifest_url as a base (missing). - return _parseURL({object: _jsonInput, property: 'start_url'}); + return { + raw, + value: { + src, + type, + density, + sizes + }, + warning: undefined + }; +} + +function parseIcons(jsonInput) { + const raw = jsonInput.icons; + let value; + + if (raw === undefined) { + return { + raw, + value, + warning: undefined + }; } - function _parseDisplay() { - var display = _parseString({object: _jsonInput, - property: 'display', - trim: true}); - if (display === undefined) { - return display; - } - - if (ALLOWED_DISPLAY_VALUES.indexOf(display.toLowerCase()) === -1) { - _logs.push('ERROR: \'display\' has an invalid value, will be ignored.'); - return undefined; - } - - return display; + if (!Array.isArray(raw)) { + return { + raw, + value, + warning: 'ERROR: \'icons\' expected to be an array but is not.' + }; } - function _parseOrientation() { - var orientation = _parseString({object: _jsonInput, - property: 'orientation', - trim: true}); - if (orientation === undefined) { - return orientation; - } - - if (ALLOWED_ORIENTATION_VALUES.indexOf(orientation.toLowerCase()) === -1) { - _logs.push('ERROR: \'orientation\' has an invalid value' + - ', will be ignored.'); - return undefined; - } + // TODO(bckenny): spec says to skip icons missing `src`. Warn instead? + value = raw.filter(icon => !!icon.src).map(parseIcon); - return orientation; - } - - function _parseIcons() { - var property = 'icons'; - var icons = []; - - if (!(property in _jsonInput)) { - return icons; - } + return { + raw, + value, + warning: undefined + }; +} - if (!Array.isArray(_jsonInput[property])) { - _logs.push('ERROR: \'' + property + - '\' expected to be an array but is not.'); - return icons; - } +function parseApplication(raw) { + let platform = parseString(raw.platform, true); + let id = parseString(raw.id, true); + // TODO: pass manfiest url as base. + let url = parseURL(raw.url); - _jsonInput[property].forEach(function(object) { - var icon = {}; - if (!('src' in object)) { - return; - } - // TODO: pass manifest url as base. - icon.src = _parseURL({object: object, property: 'src'}); - icon.type = _parseString({object: object, - property: 'type', - trim: true}); - - icon.density = parseFloat(object.density); - if (isNaN(icon.density) || !isFinite(icon.density) || icon.density <= 0) { - icon.density = 1.0; - } - - if ('sizes' in object) { - var set = new Set(); - var link = document.createElement('link'); - link.sizes = object.sizes; - - for (var i = 0; i < link.sizes.length; ++i) { - set.add(link.sizes.item(i).toLowerCase()); - } - - if (set.size !== 0) { - icon.sizes = set; - } - } - - icons.push(icon); - }); - - return icons; + return { + raw, + value: { + platform, + id, + url + }, + warning: undefined + }; +} + +function parseRelatedApplications(jsonInput) { + const raw = jsonInput.related_applications; + let value; + + if (raw === undefined) { + return { + raw, + value, + warning: undefined + }; } - function _parseRelatedApplications() { - var property = 'related_applications'; - var applications = []; - - if (!(property in _jsonInput)) { - return applications; - } - - if (!Array.isArray(_jsonInput[property])) { - _logs.push('ERROR: \'' + property + - '\' expected to be an array but is not.'); - return applications; - } - - _jsonInput[property].forEach(function(object) { - var application = {}; - application.platform = _parseString({object: object, - property: 'platform', - trim: true}); - application.id = _parseString({object: object, - property: 'id', - trim: true}); - // TODO: pass manfiest url as base. - application.url = _parseURL({object: object, property: 'url'}); - applications.push(application); - }); - - return applications; + if (!Array.isArray(raw)) { + return { + raw, + value, + warning: 'ERROR: \'related_applications\' expected to be an array but is not.' + }; } - function _parsePreferRelatedApplications() { - return _parseBoolean({object: _jsonInput, - property: 'prefer_related_applications', - defaultValue: false}); - } + // TODO(bckenny): spec says to skip apps missing `platform`. Warn instead? + value = raw.filter(application => !!application.platform).map(parseApplication); - function _parseThemeColor() { - return _parseColor({object: _jsonInput, property: 'theme_color'}); + return { + raw, + value, + warning: undefined + }; +} + +function parsePreferRelatedApplications(jsonInput) { + const raw = jsonInput.prefer_related_applications; + let value; + let warning; + + if (typeof raw === 'boolean') { + value = raw; + } else { + if (raw !== undefined) { + warning = 'ERROR: \'prefer_related_applications\' expected to be a boolean.'; + } + value = undefined; } - function _parseBackgroundColor() { - return _parseColor({object: _jsonInput, property: 'background_color'}); + return { + raw, + value, + warning + }; +} + +function parseThemeColor(jsonInput) { + return parseColor(jsonInput.theme_color); +} + +function parseBackgroundColor(jsonInput) { + return parseColor(jsonInput.background_color); +} + +function parse(string, logToConsole) { + let jsonInput; + + try { + jsonInput = JSON.parse(string); + } catch (e) { + return { + raw: string, + value: undefined, + warning: 'ERROR: file isn\'t valid JSON: ' + e + }; } - function _parse(string) { - // TODO: temporary while ManifestParser is a collection of static methods. - _logs = []; - _tips = []; - _success = true; - - try { - _jsonInput = JSON.parse(string); - } catch (e) { - _logs.push('File isn\'t valid JSON: ' + e); - _tips.push('Your JSON failed to parse, these are the main reasons why ' + - 'JSON parsing usually fails:\n' + - '- Double quotes should be used around property names and for ' + - 'strings. Single quotes are not valid.\n' + - '- JSON specification disallow trailing comma after the last ' + - 'property even if some implementations allow it.'); - - _success = false; - return; - } - - _logs.push('JSON parsed successfully.'); - - _manifest.name = _parseName(); - /* eslint-disable camelcase */ - _manifest.short_name = _parseShortName(); - _manifest.start_url = _parseStartUrl(); - _manifest.display = _parseDisplay(); - _manifest.orientation = _parseOrientation(); - _manifest.icons = _parseIcons(); - _manifest.related_applications = _parseRelatedApplications(); - _manifest.prefer_related_applications = _parsePreferRelatedApplications(); - _manifest.theme_color = _parseThemeColor(); - _manifest.background_color = _parseBackgroundColor(); - - _logs.push('Parsed `name` property is: ' + - _manifest.name); - _logs.push('Parsed `short_name` property is: ' + - _manifest.short_name); - _logs.push('Parsed `start_url` property is: ' + - _manifest.start_url); - _logs.push('Parsed `display` property is: ' + - _manifest.display); - _logs.push('Parsed `orientation` property is: ' + - _manifest.orientation); - _logs.push('Parsed `icons` property is: ' + - JSON.stringify(_manifest.icons, null, 4)); - _logs.push('Parsed `related_applications` property is: ' + - JSON.stringify(_manifest.related_applications, null, 4)); - _logs.push('Parsed `prefer_related_applications` property is: ' + - JSON.stringify(_manifest.prefer_related_applications, null, 4)); - _logs.push('Parsed `theme_color` property is: ' + - _manifest.theme_color); - _logs.push('Parsed `background_color` property is: ' + - _manifest.background_color); - /* eslint-enable camelcase */ + /* eslint-disable camelcase */ + let manifest = { + name: parseName(jsonInput), + short_name: parseShortName(jsonInput), + start_url: parseStartUrl(jsonInput), + display: parseDisplay(jsonInput), + orientation: parseOrientation(jsonInput), + icons: parseIcons(jsonInput), + related_applications: parseRelatedApplications(jsonInput), + prefer_related_applications: parsePreferRelatedApplications(jsonInput), + theme_color: parseThemeColor(jsonInput), + background_color: parseBackgroundColor(jsonInput) + }; + /* eslint-enable camelcase */ + + if (logToConsole) { + console.log('JSON parsed successfully.'); + console.log('Parsed `name` property is: ' + manifest.name); + console.log('Parsed `short_name` property is: ' + manifest.short_name); + console.log('Parsed `start_url` property is: ' + manifest.start_url); + console.log('Parsed `display` property is: ' + manifest.display); + console.log('Parsed `orientation` property is: ' + manifest.orientation); + console.log('Parsed `icons` property is: ' + JSON.stringify(manifest.icons, null, 4)); + console.log('Parsed `related_applications` property is: ' + + JSON.stringify(manifest.related_applications, null, 4)); + console.log('Parsed `prefer_related_applications` property is: ' + + JSON.stringify(manifest.prefer_related_applications, null, 4)); + console.log('Parsed `theme_color` property is: ' + manifest.theme_color); + console.log('Parsed `background_color` property is: ' + manifest.background_color); } return { - parse: _parse, - manifest: _ => _manifest, - logs: _ => _logs, - tips: _ => _tips, - success: _ => _success + raw: string, + value: manifest, + warning: undefined }; -})(); +} -module.exports = ManifestParser; +module.exports = parse; diff --git a/test/manifest-parser-tests.js b/test/manifest-parser-tests.js index 44d6d4aa766d..e8842a92f0d8 100644 --- a/test/manifest-parser-tests.js +++ b/test/manifest-parser-tests.js @@ -17,36 +17,25 @@ /* global describe, it */ -var ManifestParser = require('../helpers/manifest-parser'); +var manifestParser = require('../helpers/manifest-parser'); var expect = require('chai').expect; describe('Manifest Parser', function() { it('should not parse empty string input', function() { - ManifestParser.parse(''); - expect(ManifestParser.success()).to.equal(false); - }); - - it('has empty values when parsing empty manifest', function() { - ManifestParser.parse(''); - expect(ManifestParser.manifest().name).to.equal(undefined); - expect(ManifestParser.manifest().short_name).to.equal(undefined); - expect(ManifestParser.manifest().start_url).to.equal(undefined); - expect(ManifestParser.manifest().display).to.equal(undefined); - expect(ManifestParser.manifest().orientation).to.equal(undefined); - expect(ManifestParser.manifest().theme_color).to.equal(undefined); - expect(ManifestParser.manifest().background_color).to.equal(undefined); + let parsedManifest = manifestParser(''); + expect(!parsedManifest.warning).to.equal(false); }); it('accepts empty dictionary', function() { - ManifestParser.parse('{}'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().name).to.equal(undefined); - expect(ManifestParser.manifest().short_name).to.equal(undefined); - expect(ManifestParser.manifest().start_url).to.equal(undefined); - expect(ManifestParser.manifest().display).to.equal(undefined); - expect(ManifestParser.manifest().orientation).to.equal(undefined); - expect(ManifestParser.manifest().theme_color).to.equal(undefined); - expect(ManifestParser.manifest().background_color).to.equal(undefined); + let parsedManifest = manifestParser('{}'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.name.value).to.equal(undefined); + expect(parsedManifest.value.short_name.value).to.equal(undefined); + expect(parsedManifest.value.start_url.value).to.equal(undefined); + expect(parsedManifest.value.display.value).to.equal(undefined); + expect(parsedManifest.value.orientation.value).to.equal(undefined); + expect(parsedManifest.value.theme_color.value).to.equal(undefined); + expect(parsedManifest.value.background_color.value).to.equal(undefined); // TODO: // icons // related_applications @@ -55,62 +44,62 @@ describe('Manifest Parser', function() { it('accepts unknown values', function() { // TODO(bckenny): this is the same exact test as above - ManifestParser.parse('{}'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().name).to.equal(undefined); - expect(ManifestParser.manifest().short_name).to.equal(undefined); - expect(ManifestParser.manifest().start_url).to.equal(undefined); - expect(ManifestParser.manifest().display).to.equal(undefined); - expect(ManifestParser.manifest().orientation).to.equal(undefined); - expect(ManifestParser.manifest().theme_color).to.equal(undefined); - expect(ManifestParser.manifest().background_color).to.equal(undefined); + let parsedManifest = manifestParser('{}'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.name.value).to.equal(undefined); + expect(parsedManifest.value.short_name.value).to.equal(undefined); + expect(parsedManifest.value.start_url.value).to.equal(undefined); + expect(parsedManifest.value.display.value).to.equal(undefined); + expect(parsedManifest.value.orientation.value).to.equal(undefined); + expect(parsedManifest.value.theme_color.value).to.equal(undefined); + expect(parsedManifest.value.background_color.value).to.equal(undefined); }); describe('name parsing', function() { it('it parses basic string', function() { - ManifestParser.parse('{"name":"foo"}'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().name).to.equal('foo'); + let parsedManifest = manifestParser('{"name":"foo"}'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.name.value).to.equal('foo'); }); it('it trims whitespaces', function() { - ManifestParser.parse('{"name":" foo "}'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().name).to.equal('foo'); + let parsedManifest = manifestParser('{"name":" foo "}'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.name.value).to.equal('foo'); }); it('doesn\'t parse non-string', function() { - ManifestParser.parse('{"name": {} }'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().name).to.equal(undefined); + let parsedManifest = manifestParser('{"name": {} }'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.name.value).to.equal(undefined); - ManifestParser.parse('{"name": 42 }'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().name).to.equal(undefined); + parsedManifest = manifestParser('{"name": 42 }'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.name.value).to.equal(undefined); }); }); describe('short_name parsing', function() { it('it parses basic string', function() { - ManifestParser.parse('{"short_name":"foo"}'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().short_name).to.equal('foo'); + let parsedManifest = manifestParser('{"short_name":"foo"}'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.short_name.value).to.equal('foo'); }); it('it trims whitespaces', function() { - ManifestParser.parse('{"short_name":" foo "}'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().short_name).to.equal('foo'); + let parsedManifest = manifestParser('{"short_name":" foo "}'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.short_name.value).to.equal('foo'); }); it('doesn\'t parse non-string', function() { - ManifestParser.parse('{"short_name": {} }'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().short_name).to.equal(undefined); + let parsedManifest = manifestParser('{"short_name": {} }'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.short_name.value).to.equal(undefined); - ManifestParser.parse('{"short_name": 42 }'); - expect(ManifestParser.success()).to.equal(true); - expect(ManifestParser.manifest().short_name).to.equal(undefined); + parsedManifest = manifestParser('{"short_name": 42 }'); + expect(!parsedManifest.warning).to.equal(true); + expect(parsedManifest.value.short_name.value).to.equal(undefined); }); }); }); From f5d7e5c89992fc83c5aade06b8d1c1449691d149 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Tue, 22 Mar 2016 00:16:26 -0700 Subject: [PATCH 05/14] add manifest parsing to gather stage --- helpers/manifest-parser.js | 5 +++-- index.js | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/helpers/manifest-parser.js b/helpers/manifest-parser.js index 6e496a30ab74..067fcae54c3a 100644 --- a/helpers/manifest-parser.js +++ b/helpers/manifest-parser.js @@ -68,6 +68,7 @@ function parseColor(raw) { } // TODO(bckenny): validate color, but for reals + // possibly pull in devtools/front_end/common/Color.js // If style.color changes when set to the given color, it is valid. Testing // against 'white' and 'black' in case of the given color is one of them. // var dummy = document.createElement('div'); @@ -104,7 +105,7 @@ function parseStartUrl(jsonInput) { } function parseDisplay(jsonInput) { - var display = parseString(jsonInput.display, true); + let display = parseString(jsonInput.display, true); if (display.value && ALLOWED_DISPLAY_VALUES.indexOf(display.value.toLowerCase()) === -1) { display.value = undefined; @@ -115,7 +116,7 @@ function parseDisplay(jsonInput) { } function parseOrientation(jsonInput) { - var orientation = parseString(jsonInput.orientation, true); + let orientation = parseString(jsonInput.orientation, true); if (orientation.value && ALLOWED_ORIENTATION_VALUES.indexOf(orientation.value.toLowerCase()) === -1) { diff --git a/index.js b/index.js index b0673b251413..f5e3b392f9f3 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,8 @@ Promise.resolve(driver).then(gatherer([ require('./audits/viewport-meta-tag/gather'), require('./audits/minify-html/gather'), require('./audits/service-worker/gather'), - require('./gatherers/trace') + require('./gatherers/trace'), + require('./gatherers/js-in-context') ], URL)).then(auditor([ require('./audits/minify-html/audit'), require('./audits/service-worker/audit'), From 19edfe4eb14b9fc5ab616bb39f497a08c2a6b30d Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Tue, 22 Mar 2016 00:33:51 -0700 Subject: [PATCH 06/14] add js-in-context gatherer --- gatherers/js-in-context.js | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 gatherers/js-in-context.js diff --git a/gatherers/js-in-context.js b/gatherers/js-in-context.js new file mode 100644 index 000000000000..ea8c3289a464 --- /dev/null +++ b/gatherers/js-in-context.js @@ -0,0 +1,52 @@ +/** + * 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'; + +/* global document, XMLHttpRequest */ + +var manifestParser = require('../helpers/manifest-parser'); + +/** + * Gather data by running JS in page's context. + */ + +function fetchManifest() { + var link = document.querySelector('link[rel=manifest]'); + + if (!link) { + return 'Manifest link not found.'; + } + + var request = new XMLHttpRequest(); + request.open('GET', link.href, false); // `false` makes the request synchronous + request.send(null); + + if (request.status === 200) { + return request.responseText; + } + + return 'Unable to fetch manifest at ' + link; +} + +var JSGatherer = { + run: function(driver, url) { + return driver.evaluateFunction(url, fetchManifest) + .then(result => manifestParser(result.value)) + .then(parsedManifest => ({parsedManifest})); + } +}; + +module.exports = JSGatherer; From 432323b1035e0449bb1ebdc20f0ef53923365a3c Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Tue, 22 Mar 2016 01:05:10 -0700 Subject: [PATCH 07/14] array-ify icon sizes Set --- helpers/manifest-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/manifest-parser.js b/helpers/manifest-parser.js index 067fcae54c3a..570b923d20e4 100644 --- a/helpers/manifest-parser.js +++ b/helpers/manifest-parser.js @@ -150,7 +150,7 @@ function parseIcon(raw) { let set = new Set(); sizes.value.split(/\s/).forEach(size => set.add(size.toLowerCase())); - sizes.value = set.size > 0 ? set : undefined; + sizes.value = set.size > 0 ? Array.from(set) : undefined; } return { From 3f4a5f2377e603d416e28c56c41fc6553351d19c Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 15:26:24 +0000 Subject: [PATCH 08/14] Backports extension; refactors much. --- auditor.js | 18 +- audits/manifest/background-color.js | 47 +++ .../audit.js => manifest/exists.js} | 26 +- audits/manifest/icons-192.js | 50 +++ audits/manifest/icons.js | 47 +++ .../audit.js => manifest/name.js} | 46 ++- audits/manifest/short-name.js | 47 +++ audits/manifest/start-url.js | 47 +++ audits/manifest/theme-color.js | 47 +++ audits/minify-html/package.json | 12 - audits/mobile-friendly/viewport.js | 39 +++ audits/offline/service-worker.js | 43 +++ .../gather.js => security/is-on-https.js} | 25 +- audits/service-worker/package.json | 13 - audits/time-in-javascript/package.json | 13 - audits/viewport-meta-tag/audit.js | 29 -- audits/viewport-meta-tag/gather.js | 33 -- audits/viewport-meta-tag/package.json | 12 - extension/app/manifest.json | 3 +- .../app/scripts.babel/manifest-parser.js | 322 ------------------ extension/app/scripts.babel/pwa-check.js | 246 ++----------- extension/gulpfile.babel.js | 3 + extension/package.json | 3 + gatherer.js | 28 +- {audits/minify-html => gatherers}/gather.js | 12 +- gatherers/html.js | 34 ++ .../audit.js => gatherers/https.js | 27 +- gatherers/{trace.js => load-trace.js} | 25 +- gatherers/manifest.js | 78 +++++ gatherers/service-worker.js | 38 +++ helpers/browser/driver.js | 233 +++++-------- helpers/browser/driver.old.js | 297 ++++++++++++++++ helpers/extension/driver.js | 134 ++++++++ index.js | 62 ++-- package.json | 3 +- 35 files changed, 1240 insertions(+), 902 deletions(-) create mode 100644 audits/manifest/background-color.js rename audits/{time-in-javascript/audit.js => manifest/exists.js} (65%) create mode 100644 audits/manifest/icons-192.js create mode 100644 audits/manifest/icons.js rename audits/{minify-html/audit.js => manifest/name.js} (54%) create mode 100644 audits/manifest/short-name.js create mode 100644 audits/manifest/start-url.js create mode 100644 audits/manifest/theme-color.js delete mode 100644 audits/minify-html/package.json create mode 100644 audits/mobile-friendly/viewport.js create mode 100644 audits/offline/service-worker.js rename audits/{service-worker/gather.js => security/is-on-https.js} (68%) delete mode 100644 audits/service-worker/package.json delete mode 100644 audits/time-in-javascript/package.json delete mode 100644 audits/viewport-meta-tag/audit.js delete mode 100644 audits/viewport-meta-tag/gather.js delete mode 100644 audits/viewport-meta-tag/package.json delete mode 100644 extension/app/scripts.babel/manifest-parser.js rename {audits/minify-html => gatherers}/gather.js (74%) create mode 100644 gatherers/html.js rename audits/service-worker/audit.js => gatherers/https.js (61%) rename gatherers/{trace.js => load-trace.js} (73%) create mode 100644 gatherers/manifest.js create mode 100644 gatherers/service-worker.js create mode 100644 helpers/browser/driver.old.js create mode 100644 helpers/extension/driver.js diff --git a/auditor.js b/auditor.js index b1613b601202..65479643318f 100644 --- a/auditor.js +++ b/auditor.js @@ -15,12 +15,18 @@ */ 'use strict'; -module.exports = function(audits) { - return function audit(results) { - var flattenedAudits = results.reduce(function(prev, curr) { +class Auditor { + + flattenArtifacts_(artifacts) { + return artifacts.reduce(function(prev, curr) { return Object.assign(prev, curr); }, {}); + } + + audit(artifacts, audits) { + const flattenedArtifacts = this.flattenArtifacts_(artifacts); + return Promise.all(audits.map(audit => audit.audit(flattenedArtifacts))); + } +} - return Promise.all(audits.map(v => v(flattenedAudits))); - }; -}; +module.exports = Auditor; diff --git a/audits/manifest/background-color.js b/audits/manifest/background-color.js new file mode 100644 index 000000000000..2801b66b25ae --- /dev/null +++ b/audits/manifest/background-color.js @@ -0,0 +1,47 @@ +/** + * 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 manifestParser = require('../../helpers/manifest-parser'); + +class ManifestBackgroundColor { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains background_color'; + } + + static audit(inputs) { + let hasBackgroundColor = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest) { + hasBackgroundColor = (!!manifest.background_color); + } + + return { + value: hasBackgroundColor, + tags: ManifestBackgroundColor.tags, + description: ManifestBackgroundColor.description + }; + } +} + +module.exports = ManifestBackgroundColor; diff --git a/audits/time-in-javascript/audit.js b/audits/manifest/exists.js similarity index 65% rename from audits/time-in-javascript/audit.js rename to audits/manifest/exists.js index 82da6aa8a512..8fdaf07c1f18 100644 --- a/audits/time-in-javascript/audit.js +++ b/audits/manifest/exists.js @@ -13,14 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 'use strict'; -var traceProcessor = require('../../lib/processor'); +class ManifestExists { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Exists'; + } -module.exports = function(data) { - let results = traceProcessor.analyzeTrace(data.traceContents); + static audit(inputs) { + return { + value: inputs.manifest.length > 0, + tags: ManifestExists.tags, + description: ManifestExists.description + }; + } +} - return { - 'time-in-javascript': results[0].extendedInfo.javaScript - }; -}; +module.exports = ManifestExists; diff --git a/audits/manifest/icons-192.js b/audits/manifest/icons-192.js new file mode 100644 index 000000000000..2a1914bc7c5b --- /dev/null +++ b/audits/manifest/icons-192.js @@ -0,0 +1,50 @@ +/** + * 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 manifestParser = require('../../helpers/manifest-parser'); + +class ManifestIcons192 { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains 192px icons'; + } + + static audit(inputs) { + let hasIcons = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest && manifest.icons) { + const icons192 = manifest.icons.raw.find(function(i) { + return i.sizes === '192x192'; + }); + hasIcons = (!!icons192); + } + + return { + value: hasIcons, + tags: ManifestIcons192.tags, + description: ManifestIcons192.description + }; + } +} + +module.exports = ManifestIcons192; diff --git a/audits/manifest/icons.js b/audits/manifest/icons.js new file mode 100644 index 000000000000..252565ece1b4 --- /dev/null +++ b/audits/manifest/icons.js @@ -0,0 +1,47 @@ +/** + * 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 manifestParser = require('../../helpers/manifest-parser'); + +class ManifestIcons { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains icons'; + } + + static audit(inputs) { + let hasIcons = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest) { + hasIcons = (!!manifest.icons); + } + + return { + value: hasIcons, + tags: ManifestIcons.tags, + description: ManifestIcons.description + }; + } +} + +module.exports = ManifestIcons; diff --git a/audits/minify-html/audit.js b/audits/manifest/name.js similarity index 54% rename from audits/minify-html/audit.js rename to audits/manifest/name.js index a3f23a6d4003..03d8bd97f181 100644 --- a/audits/minify-html/audit.js +++ b/audits/manifest/name.js @@ -13,21 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 'use strict'; -module.exports = function(data) { - // See how compressed the HTML _could_ be if whitespace was removed. - // This could be a lot more aggressive. - let htmlNoWhiteSpaces = data.html - .replace(/\n/igm, '') - .replace(/\t/igm, '') - .replace(/\s+/igm, ' '); - - let htmlLen = Math.max(1, data.html.length); - let htmlNoWhiteSpacesLen = htmlNoWhiteSpaces.length; - let ratio = Math.min(1, (htmlNoWhiteSpacesLen / htmlLen)); - - return { - 'minify-html': ratio - }; -}; +const manifestParser = require('../../helpers/manifest-parser'); + +class ManifestName { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains name'; + } + + static audit(inputs) { + let hasName = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest) { + hasName = (!!manifest.name); + } + + return { + value: hasName, + tags: ManifestName.tags, + description: ManifestName.description + }; + } +} + +module.exports = ManifestName; diff --git a/audits/manifest/short-name.js b/audits/manifest/short-name.js new file mode 100644 index 000000000000..c0af3281fc37 --- /dev/null +++ b/audits/manifest/short-name.js @@ -0,0 +1,47 @@ +/** + * 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 manifestParser = require('../../helpers/manifest-parser'); + +class ManifestShortName { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains short_name'; + } + + static audit(inputs) { + let hasShortName = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest) { + hasShortName = (!!manifest.short_name); + } + + return { + value: hasShortName, + tags: ManifestShortName.tags, + description: ManifestShortName.description + }; + } +} + +module.exports = ManifestShortName; diff --git a/audits/manifest/start-url.js b/audits/manifest/start-url.js new file mode 100644 index 000000000000..3341324092ef --- /dev/null +++ b/audits/manifest/start-url.js @@ -0,0 +1,47 @@ +/** + * 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 manifestParser = require('../../helpers/manifest-parser'); + +class ManifestStartUrl { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains start_url'; + } + + static audit(inputs) { + let hasStartUrl = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest) { + hasStartUrl = (!!manifest.start_url); + } + + return { + value: hasStartUrl, + tags: ManifestStartUrl.tags, + description: ManifestStartUrl.description + }; + } +} + +module.exports = ManifestStartUrl; diff --git a/audits/manifest/theme-color.js b/audits/manifest/theme-color.js new file mode 100644 index 000000000000..73972e7199f6 --- /dev/null +++ b/audits/manifest/theme-color.js @@ -0,0 +1,47 @@ +/** + * 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 manifestParser = require('../../helpers/manifest-parser'); + +class ManifestThemeColor { + + static get tags() { + return ['Manifest']; + } + + static get description() { + return 'Contains theme_color'; + } + + static audit(inputs) { + let hasThemeColor = false; + const manifest = manifestParser(inputs.manifest).value; + + if (manifest) { + hasThemeColor = (!!manifest.theme_color); + } + + return { + value: hasThemeColor, + tags: ManifestThemeColor.tags, + description: ManifestThemeColor.description + }; + } +} + +module.exports = ManifestThemeColor; diff --git a/audits/minify-html/package.json b/audits/minify-html/package.json deleted file mode 100644 index 74bf030f7ffe..000000000000 --- a/audits/minify-html/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "minify-html", - "version": "1.0.0", - "description": "Tests the HTML to see if it is minified.", - "main": "index.js", - "keywords": [], - "author": "", - "license": "Apache-2.0", - "inputs": [ - "html" - ] -} diff --git a/audits/mobile-friendly/viewport.js b/audits/mobile-friendly/viewport.js new file mode 100644 index 000000000000..88375b32436e --- /dev/null +++ b/audits/mobile-friendly/viewport.js @@ -0,0 +1,39 @@ +/** + * 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'; + +class Viewport { + + static get tags() { + return ['Mobile Friendly']; + } + + static get description() { + return 'Site has a viewport meta tag'; + } + + static audit(inputs) { + const viewportExpression = / + reg.status === 'activated'); + + return { + value: (activatedRegistrations.length > 0), + tags: ServiceWorker.tags, + description: ServiceWorker.description + }; + } +} + +module.exports = ServiceWorker; diff --git a/audits/service-worker/gather.js b/audits/security/is-on-https.js similarity index 68% rename from audits/service-worker/gather.js rename to audits/security/is-on-https.js index 5041ffbc4c8a..ac031615df89 100644 --- a/audits/service-worker/gather.js +++ b/audits/security/is-on-https.js @@ -15,12 +15,23 @@ */ 'use strict'; -var ServiceWorkerGatherer = { - run: function(driver, url) { - return driver.gotoURL(url, driver.WAIT_FOR_LOAD) - .then(driver.getServiceWorkerRegistrations) - .then(serviceWorkerRegistrations => ({serviceWorkerRegistrations})); +class HTTPS { + + static get tags() { + return ['Security']; + } + + static get description() { + return 'Site is on HTTPS'; + } + + static audit(inputs) { + return { + value: inputs.https, + tags: HTTPS.tags, + description: HTTPS.description + }; } -}; +} -module.exports = ServiceWorkerGatherer; +module.exports = HTTPS; diff --git a/audits/service-worker/package.json b/audits/service-worker/package.json deleted file mode 100644 index 115d0b44ff4f..000000000000 --- a/audits/service-worker/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "service-worker", - "version": "1.0.0", - "description": "Checks if there's a Service Worker.", - "main": "index.js", - "keywords": [], - "author": "", - "license": "Apache-2.0", - "inputs": [ - "chrome", - "url" - ] -} diff --git a/audits/time-in-javascript/package.json b/audits/time-in-javascript/package.json deleted file mode 100644 index 538a970bb75e..000000000000 --- a/audits/time-in-javascript/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "time-in-javascript", - "version": "1.0.0", - "description": "Figures out how long the page spends in JS during load.", - "main": "index.js", - "keywords": [], - "author": "", - "license": "Apache-2.0", - "inputs": [ - "chrome", - "url" - ] -} diff --git a/audits/viewport-meta-tag/audit.js b/audits/viewport-meta-tag/audit.js deleted file mode 100644 index 716a09df3b3e..000000000000 --- a/audits/viewport-meta-tag/audit.js +++ /dev/null @@ -1,29 +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'; - -module.exports = function(data) { - let viewportElement = data.viewportElement; - let hasValidViewport = viewportElement.type === 'object' && - viewportElement.subtype === 'node' && - viewportElement.props.content.includes('width='); - - return { - 'viewport-meta-tag': hasValidViewport - }; -}; - diff --git a/audits/viewport-meta-tag/gather.js b/audits/viewport-meta-tag/gather.js deleted file mode 100644 index 7aac3ce946a9..000000000000 --- a/audits/viewport-meta-tag/gather.js +++ /dev/null @@ -1,33 +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'; - -/* global document */ - -// must be defined as a standalone function expression to be stringified successfully. -function findMetaViewport() { - return document.head.querySelector('meta[name="viewport"]'); -} - -var ViewportHasMetaContent = { - run: function(driver, url) { - return driver.evaluateFunction(url, findMetaViewport) - .then(viewportElement => ({viewportElement})); - } -}; - -module.exports = ViewportHasMetaContent; diff --git a/audits/viewport-meta-tag/package.json b/audits/viewport-meta-tag/package.json deleted file mode 100644 index 771c867c061d..000000000000 --- a/audits/viewport-meta-tag/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "viewport-meta-tag", - "version": "1.0.0", - "description": "Tests the existence and validity of the viewport's meta tag", - "main": "index.js", - "keywords": [], - "author": "", - "license": "Apache-2.0", - "inputs": [ - "chrome" - ] -} diff --git a/extension/app/manifest.json b/extension/app/manifest.json index 7437e4853d73..4c7d32391600 100644 --- a/extension/app/manifest.json +++ b/extension/app/manifest.json @@ -15,7 +15,8 @@ ] }, "permissions": [ - "activeTab" + "activeTab", + "debugger" ], "browser_action": { "default_icon": { diff --git a/extension/app/scripts.babel/manifest-parser.js b/extension/app/scripts.babel/manifest-parser.js deleted file mode 100644 index 1288e993ead0..000000000000 --- a/extension/app/scripts.babel/manifest-parser.js +++ /dev/null @@ -1,322 +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. - */ - -var ManifestParser = function() { - 'use strict'; - - var _jsonInput = {}; - var _manifest = {}; - var _logs = []; - var _tips = []; - var _success = true; - - var ALLOWED_DISPLAY_VALUES = ['fullscreen', - 'standalone', - 'minimal-ui', - 'browser']; - - var ALLOWED_ORIENTATION_VALUES = ['any', - 'natural', - 'landscape', - 'portrait', - 'portrait-primary', - 'portrait-secondary', - 'landscape-primary', - 'landscape-secondary']; - - function _parseString(args) { - var object = args.object; - var property = args.property; - if (!(property in object)) { - return undefined; - } - - if (typeof object[property] !== 'string') { - _logs.push('ERROR: \'' + property + - '\' expected to be a string but is not.'); - return undefined; - } - - if (args.trim) { - return object[property].trim(); - } - return object[property]; - } - - function _parseBoolean(args) { - var object = args.object; - var property = args.property; - var defaultValue = args.defaultValue; - if (!(property in object)) { - return defaultValue; - } - - if (typeof object[property] !== 'boolean') { - _logs.push('ERROR: \'' + property + - '\' expected to be a boolean but is not.'); - return defaultValue; - } - - return object[property]; - } - - function _parseURL(args) { - var object = args.object; - var property = args.property; - - var str = _parseString({object: object, property: property, trim: false}); - if (str === undefined) { - return undefined; - } - - return object[property]; - } - - function _parseColor(args) { - var object = args.object; - var property = args.property; - if (!(property in object)) { - return undefined; - } - - if (typeof object[property] !== 'string') { - _logs.push('ERROR: \'' + property + - '\' expected to be a string but is not.'); - return undefined; - } - - // If style.color changes when set to the given color, it is valid. Testing - // against 'white' and 'black' in case of the given color is one of them. - var dummy = document.createElement('div'); - dummy.style.color = 'white'; - dummy.style.color = object[property]; - if (dummy.style.color !== 'white') { - return object[property]; - } - dummy.style.color = 'black'; - dummy.style.color = object[property]; - if (dummy.style.color !== 'black') { - return object[property]; - } - return undefined; - } - - function _parseName() { - return _parseString({object: _jsonInput, property: 'name', trim: true}); - } - - function _parseShortName() { - return _parseString({object: _jsonInput, - property: 'short_name', - trim: true}); - } - - function _parseStartUrl() { - return _parseURL({object: _jsonInput, property: 'start_url'}); - } - - function _parseDisplay() { - var display = _parseString({object: _jsonInput, - property: 'display', - trim: true}); - if (display === undefined) { - return display; - } - - if (ALLOWED_DISPLAY_VALUES.indexOf(display.toLowerCase()) === -1) { - _logs.push('ERROR: \'display\' has an invalid value, will be ignored.'); - return undefined; - } - - return display; - } - - function _parseOrientation() { - var orientation = _parseString({object: _jsonInput, - property: 'orientation', - trim: true}); - if (orientation === undefined) { - return orientation; - } - - if (ALLOWED_ORIENTATION_VALUES.indexOf(orientation.toLowerCase()) === -1) { - _logs.push('ERROR: \'orientation\' has an invalid value' + - ', will be ignored.'); - return undefined; - } - - return orientation; - } - - function _parseIcons() { - var property = 'icons'; - var icons = []; - - if (!(property in _jsonInput)) { - return icons; - } - - if (!Array.isArray(_jsonInput[property])) { - _logs.push('ERROR: \'' + property + - '\' expected to be an array but is not.'); - return icons; - } - - _jsonInput[property].forEach(function(object) { - var icon = {}; - if (!('src' in object)) { - return; - } - icon.src = _parseURL({object: object, property: 'src'}); - icon.type = _parseString({object: object, - property: 'type', - trim: true}); - - icon.density = parseFloat(object.density); - if (isNaN(icon.density) || !isFinite(icon.density) || icon.density <= 0) { - icon.density = 1.0; - } - - if ('sizes' in object) { - var set = new Set(); - var link = document.createElement('link'); - link.sizes = object.sizes; - - for (var i = 0; i < link.sizes.length; ++i) { - set.add(link.sizes.item(i).toLowerCase()); - } - - if (set.size !== 0) { - icon.sizes = set; - } - } - - icons.push(icon); - }); - - return icons; - } - - function _parseRelatedApplications() { - var property = 'related_applications'; - var applications = []; - - if (!(property in _jsonInput)) { - return applications; - } - - if (!Array.isArray(_jsonInput[property])) { - _logs.push('ERROR: \'' + property + - '\' expected to be an array but is not.'); - return applications; - } - - _jsonInput[property].forEach(function(object) { - var application = {}; - application.platform = _parseString({object: object, - property: 'platform', - trim: true}); - application.id = _parseString({object: object, - property: 'id', - trim: true}); - application.url = _parseURL({object: object, property: 'url'}); - applications.push(application); - }); - - return applications; - } - - function _parsePreferRelatedApplications() { - return _parseBoolean({object: _jsonInput, - property: 'prefer_related_applications', - defaultValue: false}); - } - - function _parseThemeColor() { - return _parseColor({object: _jsonInput, property: 'theme_color'}); - } - - function _parseBackgroundColor() { - return _parseColor({object: _jsonInput, property: 'background_color'}); - } - - function _parse(string) { - _logs = []; - _tips = []; - _success = true; - - try { - _jsonInput = JSON.parse(string); - } catch (e) { - _logs.push('File isn\'t valid JSON: ' + e); - _tips.push('Your JSON failed to parse, these are the main reasons why ' + - 'JSON parsing usually fails:\n' + - '- Double quotes should be used around property names and for ' + - 'strings. Single quotes are not valid.\n' + - '- JSON specification disallow trailing comma after the last ' + - 'property even if some implementations allow it.'); - - _success = false; - return; - } - - _logs.push('JSON parsed successfully.'); - - _manifest.name = _parseName(); - /*eslint-disable*/ - _manifest.short_name = _parseShortName(); - _manifest.start_url = _parseStartUrl(); - _manifest.display = _parseDisplay(); - _manifest.orientation = _parseOrientation(); - _manifest.icons = _parseIcons(); - _manifest.related_applications = _parseRelatedApplications(); - _manifest.prefer_related_applications = _parsePreferRelatedApplications(); - _manifest.theme_color = _parseThemeColor(); - _manifest.background_color = _parseBackgroundColor(); - - _logs.push('Parsed `name` property is: ' + - _manifest.name); - _logs.push('Parsed `short_name` property is: ' + - _manifest.short_name); - _logs.push('Parsed `start_url` property is: ' + - _manifest.start_url); - _logs.push('Parsed `display` property is: ' + - _manifest.display); - _logs.push('Parsed `orientation` property is: ' + - _manifest.orientation); - _logs.push('Parsed `icons` property is: ' + - JSON.stringify(_manifest.icons, null, 4)); - _logs.push('Parsed `related_applications` property is: ' + - JSON.stringify(_manifest.related_applications, null, 4)); - _logs.push('Parsed `prefer_related_applications` property is: ' + - JSON.stringify(_manifest.prefer_related_applications, null, 4)); - _logs.push('Parsed `theme_color` property is: ' + - _manifest.theme_color); - _logs.push('Parsed `background_color` property is: ' + - _manifest.background_color); - /*eslint-enable*/ - } - - return { - parse: _parse, - manifest: _ => _manifest, - logs: _ => _logs, - tips: _ => _tips, - success: _ => _success - }; -}; - -export {ManifestParser}; diff --git a/extension/app/scripts.babel/pwa-check.js b/extension/app/scripts.babel/pwa-check.js index 0acef70aa85d..22d28adafe05 100644 --- a/extension/app/scripts.babel/pwa-check.js +++ b/extension/app/scripts.babel/pwa-check.js @@ -14,197 +14,34 @@ * limitations under the License. */ -import {ManifestParser} from './manifest-parser.js'; - -var hasManifest = _ => { - return !!document.querySelector('link[rel=manifest]'); -}; - -var parseManifest = function() { - var link = document.querySelector('link[rel=manifest]'); - - if (!link) { - return {}; - } - - var request = new XMLHttpRequest(); - request.open('GET', link.href, false); // `false` makes the request synchronous - request.send(null); - - if (request.status === 200) { - /* eslint-disable new-cap*/ - let parserInstance = (window.__lighthouse.ManifestParser)(); - /* eslint-enable new-cap*/ - parserInstance.parse(request.responseText); - return parserInstance.manifest(); - } - - throw new Error('Unable to fetch manifest at ' + link); -}; - -var hasManifestThemeColor = _ => { - let manifest = __lighthouse.parseManifest(); - - return !!manifest.theme_color; -}; - -var hasManifestBackgroundColor = _ => { - let manifest = __lighthouse.parseManifest(); - - return !!manifest.background_color; -}; - -var hasManifestIcons = _ => { - let manifest = __lighthouse.parseManifest(); - - return !!manifest.icons; -}; - -var hasManifestIcons192 = _ => { - let manifest = __lighthouse.parseManifest(); - - if (!manifest.icons) { - return false; - } - - return !!manifest.icons.find(function(i) { - return i.sizes.has('192x192'); - }); -}; - -var hasManifestShortName = _ => { - let manifest = __lighthouse.parseManifest(); - - return !!manifest.short_name; -}; - -var hasManifestName = _ => { - let manifest = __lighthouse.parseManifest(); - - return !!manifest.name; -}; - -var hasManifestStartUrl = _ => { - let manifest = __lighthouse.parseManifest(); - - return !!manifest.start_url; -}; - -// quoth kinlan >> re: canonical, it is always good to have them, but not needed. This test is more of a warning than anything else to let you know you should consider it. -var hasCanonicalUrl = _ => { - var link = document.querySelector('link[rel=canonical]'); - - return !!link; -}; - -var hasServiceWorkerRegistration = _ => { - return new Promise((resolve, reject) => { - navigator.serviceWorker.getRegistration().then(r => { - // Fail immediately for non-existent registrations. - if (typeof r === 'undefined') { - return resolve(false); - } - - // If there's an active SW call this done. - if (r.active) { - return resolve(!!r.active); - } - - // Give any installing SW chance to install. - r.installing.onstatechange = function() { - resolve(this.state === 'installed'); - }; - }).catch(_ => { - resolve(false); - }); - }); -}; - -var isOnHTTPS = _ => location.protocol === 'https:'; - -function injectIntoTab(chrome, fnPair) { - var singleLineFn = fnPair[1].toString(); - - return new Promise((res, reject) => { - chrome.tabs.executeScript(null, { - code: `window.__lighthouse = window.__lighthouse || {}; - window.__lighthouse['${fnPair[0]}'] = ${singleLineFn}` - }, ret => { - if (chrome.runtime.lastError) { - return reject(chrome.runtime.lastError); - } - - res(ret); - }); - }); -} - -function convertAuditsToPromiseStrings(audits) { - const auditNames = Object.keys(audits); - - return auditNames.reduce((prevAuditGroup, auditName, auditGroupIndex) => { - // Then within each group, reduce each audit down to a Promise. - return prevAuditGroup + (auditGroupIndex > 0 ? ',' : '') + - audits[auditName].reduce((prevAudit, audit, auditIndex) => { - return prevAudit + (auditIndex > 0 ? ',' : '') + - convertAuditToPromiseString(auditName, audit); - }, ''); - }, ''); -} - -function convertAuditToPromiseString(auditName, audit) { - return `Promise.all([ - Promise.resolve("${auditName}"), - Promise.resolve("${audit[1]}"), - (${audit[0].toString()})() - ])`; -} - -function runAudits(chrome, audits) { - // Reduce each group of audits. - const fnString = convertAuditsToPromiseStrings(audits); - - // Ask the tab to run the promises, and beacon back the results. - chrome.tabs.executeScript(null, { - code: `Promise.all([${fnString}]).then(__lighthouse.postAuditResults)` - }, function() { - if (chrome.runtime.lastError) { - throw chrome.runtime.lastError; - } - }); -} - -function postAuditResults(results) { - chrome.runtime.sendMessage({onAuditsComplete: results}); -} - -var functionsToInject = [ - ['ManifestParser', ManifestParser], - ['parseManifest', parseManifest], - ['postAuditResults', postAuditResults] +'use strict'; + +const ExtensionProtocol = require('../../../helpers/extension/driver.js'); +const Auditor = require('../../../auditor'); +const Gatherer = require('../../../gatherer'); + +const driver = new ExtensionProtocol(); +const gatherer = new Gatherer(); +const auditor = new Auditor(); +const gatherers = [ + require('../../../gatherers/https'), + require('../../../gatherers/service-worker'), + require('../../../gatherers/html'), + require('../../../gatherers/manifest') +]; +const audits = [ + require('../../../audits/security/is-on-https'), + require('../../../audits/offline/service-worker'), + require('../../../audits/mobile-friendly/viewport'), + require('../../../audits/manifest/exists'), + require('../../../audits/manifest/background-color'), + require('../../../audits/manifest/theme-color'), + require('../../../audits/manifest/icons'), + require('../../../audits/manifest/icons-192'), + require('../../../audits/manifest/name'), + require('../../../audits/manifest/short-name'), + require('../../../audits/manifest/start-url') ]; - -var audits = { - Security: [ - [isOnHTTPS, 'Served over HTTPS'] - ], - Offline: [ - [hasServiceWorkerRegistration, 'Has a service worker registration'] - ], - Manifest: [ - [hasManifest, 'Exists'], - [hasManifestThemeColor, 'Contains theme_color'], - [hasManifestBackgroundColor, 'Contains background_color'], - [hasManifestStartUrl, 'Contains start_url'], - [hasManifestShortName, 'Contains short_name'], - [hasManifestName, 'Contains name'], - [hasManifestIcons, 'Contains icons defined'], - [hasManifestIcons192, 'Contains 192px icon'], - ], - Miscellaneous: [ - [hasCanonicalUrl, 'Site has a Canonical URL'] - ] -}; function createResultsHTML(results) { const resultsGroup = {}; @@ -212,14 +49,14 @@ function createResultsHTML(results) { // Go through each group and restructure the results accordingly. results.forEach(result => { - groupName = result[0]; + groupName = result.tags; if (!resultsGroup[groupName]) { resultsGroup[groupName] = []; } resultsGroup[groupName].push({ - title: result[1], - value: result[2] + title: result.description, + value: result.value }); }); @@ -251,22 +88,11 @@ function createResultsHTML(results) { return resultsHTML; } -export function runPwaAudits(chrome) { - return new Promise((resolve, reject) => { - chrome.runtime.onMessage.addListener(message => { - if (!message.onAuditsComplete) { - return; - } - resolve(createResultsHTML(message.onAuditsComplete)); +export function runPwaAudits() { + return gatherer + .gather(gatherers, {driver}) + .then(artifacts => auditor.audit(artifacts, audits)) + .then(results => { + return createResultsHTML(results); }); - - Promise.all(functionsToInject.map(fnPair => injectIntoTab(chrome, fnPair))) - .then(_ => runAudits(chrome, audits), - err => { - throw err; - }) - .catch(err => { - reject(err); - }); - }); } diff --git a/extension/gulpfile.babel.js b/extension/gulpfile.babel.js index c8c4d7e9415a..8f03a84d6565 100644 --- a/extension/gulpfile.babel.js +++ b/extension/gulpfile.babel.js @@ -2,6 +2,7 @@ import gulp from 'gulp'; import gulpLoadPlugins from 'gulp-load-plugins'; import del from 'del'; +import browserify from 'gulp-browserify'; import runSequence from 'run-sequence'; import {stream as wiredep} from 'wiredep'; @@ -86,6 +87,7 @@ gulp.task('chromeManifest', () => { }); gulp.task('babel', () => { + console.log('Babeling up...'); return gulp.src([ 'app/scripts.babel/app.js', 'app/scripts.babel/chromereload.js', @@ -96,6 +98,7 @@ gulp.task('babel', () => { .pipe($.babel({ presets: ['es2015'] })) + .pipe(browserify()) .pipe(gulp.dest('app/scripts')) .pipe(gulp.dest('dist/scripts')); }); diff --git a/extension/package.json b/extension/package.json index 52a601c6374f..0c130caa48ac 100644 --- a/extension/package.json +++ b/extension/package.json @@ -11,9 +11,12 @@ "devDependencies": { "babel-core": "^6.5.2", "babel-preset-es2015": "^6.5.0", + "babelify": "^7.2.0", + "browserify": "^13.0.0", "del": "^2.2.0", "gulp": "^3.9.1", "gulp-babel": "^6.1.2", + "gulp-browserify": "^0.5.1", "gulp-cache": "^0.4.2", "gulp-chrome-manifest": "0.0.13", "gulp-debug": "^2.1.2", diff --git a/gatherer.js b/gatherer.js index 6c7ce8a142e2..01e55f9b2cc0 100644 --- a/gatherer.js +++ b/gatherer.js @@ -15,20 +15,22 @@ */ 'use strict'; -/** - * @param {Array<{run: function}>} gatherers - * @returns {Promise} - */ -module.exports = function(gatherers, url) { - return function gather(driver) { - let artifacts = []; +class Gatherer { + + gather(gatherers, options) { + const driver = options.driver; + const artifacts = []; // Execute gatherers sequentially and return results array when complete. - return gatherers.reduce((prev, curr) => { - return prev - .then(_ => curr.run(driver, url)) + return gatherers.reduce((chain, gatherer) => { + return chain + .then(_ => gatherer.gather(options)) .then(artifact => artifacts.push(artifact)); - }, Promise.resolve()) + }, driver.connect()) + .then(_ => driver.disconnect()) .then(_ => artifacts); - }; -}; + } + +} + +module.exports = Gatherer; diff --git a/audits/minify-html/gather.js b/gatherers/gather.js similarity index 74% rename from audits/minify-html/gather.js rename to gatherers/gather.js index c8af5f711ee1..31662c44bce2 100644 --- a/audits/minify-html/gather.js +++ b/gatherers/gather.js @@ -15,12 +15,10 @@ */ 'use strict'; -var MinifyHtmlGatherer = { - run: function(driver, url) { - return driver.gotoURL(url, driver.WAIT_FOR_LOAD) - .then(driver.getPageHTML) - .then(html => ({html})); +class Gather { + static gather() { + throw new Error('Must be overridden'); } -}; +} -module.exports = MinifyHtmlGatherer; +module.exports = Gather; diff --git a/gatherers/html.js b/gatherers/html.js new file mode 100644 index 000000000000..9bbcce644176 --- /dev/null +++ b/gatherers/html.js @@ -0,0 +1,34 @@ +/** + * 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 Gather = require('./gather'); + +class HTML extends Gather { + + static gather(options) { + const driver = options.driver; + + return driver.sendCommand('DOM.getDocument') + .then(result => result.root.nodeId) + .then(nodeId => driver.sendCommand('DOM.getOuterHTML', { + nodeId: nodeId + })) + .then(nodeHTML => ({html: nodeHTML.outerHTML})); + } +} + +module.exports = HTML; diff --git a/audits/service-worker/audit.js b/gatherers/https.js similarity index 61% rename from audits/service-worker/audit.js rename to gatherers/https.js index 3aefff67b99e..b079b2fccac5 100644 --- a/audits/service-worker/audit.js +++ b/gatherers/https.js @@ -15,13 +15,22 @@ */ 'use strict'; -module.exports = function(data) { - // Test the Service Worker registrations for validity. - let registrations = data.serviceWorkerRegistrations; - let activatedRegistrations = registrations.versions.filter(reg => - reg.status === 'activated'); +const Gather = require('./gather'); - return { - 'service-worker': activatedRegistrations.length > 0 - }; -}; +class HTTPS extends Gather { + + static gather(options) { + const driver = options.driver; + + return driver + .sendCommand('Runtime.evaluate', { + expression: 'window.location.protocol' + }).then(options => { + return { + https: options.result.value === 'https:' + }; + }); + } +} + +module.exports = HTTPS; diff --git a/gatherers/trace.js b/gatherers/load-trace.js similarity index 73% rename from gatherers/trace.js rename to gatherers/load-trace.js index e2426770958c..118f649029c0 100644 --- a/gatherers/trace.js +++ b/gatherers/load-trace.js @@ -17,15 +17,18 @@ 'use strict'; const PAUSE_AFTER_LOAD = 3000; +const Gather = require('./gather'); -var TraceGatherer = { - run: function(driver, url) { - let artifacts = {}; +class LoadTrace extends Gather { - return driver.disableCaching() - // Begin trace and network recording. - .then(driver.beginTrace) - .then(driver.beginNetworkCollect) + static gather(options) { + const url = options.url; + const driver = options.driver; + const artifacts = {}; + + // Begin trace and network recording. + return driver.beginTrace() + .then(_ => driver.beginNetworkCollect()) // Go to the URL. .then(_ => driver.gotoURL(url, driver.WAIT_FOR_LOAD)) @@ -36,17 +39,17 @@ var TraceGatherer = { })) // Stop recording and save the results. - .then(driver.endNetworkCollect) + .then(_ => driver.endNetworkCollect()) .then(networkRecords => { artifacts.networkRecords = networkRecords; }) - .then(driver.endTrace) + .then(_ => driver.endTrace()) .then(traceContents => { artifacts.traceContents = traceContents; }) .then(_ => artifacts); } -}; +} -module.exports = TraceGatherer; +module.exports = LoadTrace; diff --git a/gatherers/manifest.js b/gatherers/manifest.js new file mode 100644 index 000000000000..1e4e5bcb066a --- /dev/null +++ b/gatherers/manifest.js @@ -0,0 +1,78 @@ +/** + * 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'; + +/* global window, fetch */ + +const Gather = require('./gather'); + +class Manifest extends Gather { + + static loadFromURL(options, manifestURL) { + if (typeof window !== 'undefined') { + const finalURL = (new window.URL(options.driver.url).origin) + manifestURL; + return fetch(finalURL).then(response => response.text()); + } + + return new Promise((resolve, reject) => { + const url = require('url'); + const request = require('request'); + const finalURL = url.resolve(options.url, manifestURL); + + request(finalURL, function(err, response, body) { + if (err) { + return resolve(''); + } + + resolve(body); + }); + }); + } + + static gather(options) { + const driver = options.driver; + + return driver.sendCommand('DOM.getDocument') + .then(result => result.root.nodeId) + .then(nodeId => driver.sendCommand('DOM.querySelector', { + nodeId: nodeId, + selector: 'link[rel="manifest"]' + })) + .then(manifestNode => manifestNode.nodeId) + .then(manifestNodeId => { + if (manifestNodeId === 0) { + return ''; + } + + return driver.sendCommand('DOM.getAttributes', { + nodeId: manifestNodeId + }) + .then(manifestAttributes => manifestAttributes.attributes) + .then(attributes => { + const hrefIndex = attributes.indexOf('href'); + if (hrefIndex === -1) { + return ''; + } + + return attributes[hrefIndex + 1]; + }) + .then(manifestURL => Manifest.loadFromURL(options, manifestURL)); + }) + .then(manifest => ({manifest})); + } +} + +module.exports = Manifest; diff --git a/gatherers/service-worker.js b/gatherers/service-worker.js new file mode 100644 index 000000000000..5d1e86203a75 --- /dev/null +++ b/gatherers/service-worker.js @@ -0,0 +1,38 @@ +/** + * 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 Gather = require('./gather'); + +class ServiceWorker extends Gather { + + static gather(options) { + const driver = options.driver; + + return new Promise((resolve, reject) => { + // Register for the event. + driver.on('ServiceWorker.workerVersionUpdated', data => { + resolve({ + serviceWorkers: data + }); + }); + + driver.sendCommand('ServiceWorker.enable'); + }); + } +} + +module.exports = ServiceWorker; diff --git a/helpers/browser/driver.js b/helpers/browser/driver.js index e88754a758ee..40ef7dd8b9ff 100644 --- a/helpers/browser/driver.js +++ b/helpers/browser/driver.js @@ -17,16 +17,19 @@ 'use strict'; const chromeRemoteInterface = require('chrome-remote-interface'); - -var NetworkRecorder = require('../network-recorder'); - -const EXECUTION_CONTEXT_TIMEOUT = 4000; +const NetworkRecorder = require('../network-recorder'); class ChromeProtocol { - constructor(opts) { - opts = opts || {}; - this.categories = [ + get WAIT_FOR_LOAD() { + return true; + } + + constructor() { + this.url_ = null; + this.chrome_ = null; + this.traceEvents_ = []; + this.traceCategories_ = [ '-*', // exclude default 'toplevel', 'blink.console', @@ -39,188 +42,110 @@ class ChromeProtocol { 'disabled-by-default-v8.cpu_profile' ]; - this._traceEvents = []; - this._currentURL = null; - this._instance = null; - this._networkRecords = []; - this._networkRecorder = null; - - this.getPageHTML = this.getPageHTML.bind(this); - this.evaluateFunction = this.evaluateFunction.bind(this); - this.evaluateScript = this.evaluateScript.bind(this); - this.getServiceWorkerRegistrations = - this.getServiceWorkerRegistrations.bind(this); - this.beginTrace = this.beginTrace.bind(this); - this.endTrace = this.endTrace.bind(this); - this.beginNetworkCollect = this.beginNetworkCollect.bind(this); - this.endNetworkCollect = this.endNetworkCollect.bind(this); + this.timeoutID = null; } - get WAIT_FOR_LOAD() { - return true; + get url() { + return this.url_; } - getInstance() { - if (!this._instance) { - this._instance = new Promise((resolve, reject) => { - // @see: github.com/cyrus-and/chrome-remote-interface#moduleoptions-callback - const OPTIONS = {}; - chromeRemoteInterface(OPTIONS, - resolve - ).on('error', e => reject(e)); - }); - } + set url(url_) { + this.url_ = url_; + } - return this._instance; + connect() { + return new Promise((resolve, reject) => { + if (this.chrome_) { + return resolve(this.chrome_); + } + + chromeRemoteInterface({}, chrome => { + this.chrome_ = chrome; + resolve(chrome); + }).on('error', e => reject(e)); + }); } - resetFailureTimeout(reject) { + disconnect() { + if (this.chrome_ === null) { + return; + } + if (this.timeoutID) { clearTimeout(this.timeoutID); + this.timeoutID = null; } - this.timeoutID = setTimeout(_ => { - // FIXME - // this.discardTab(); - reject(new Error('Trace retrieval timed out')); - }, 15000); + this.chrome_.close(); + this.chrome_ = null; + this.url = null; } - getServiceWorkerRegistrations() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.ServiceWorker.enable(); - chrome.on('ServiceWorker.workerVersionUpdated', data => { - resolve(data); - }); - }); - }); - } - - getPageHTML() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.send('DOM.getDocument', null, (docErr, docResult) => { - if (docErr) { - return reject(docErr); - } - - let nodeId = { - nodeId: docResult.root.nodeId - }; - - chrome.send('DOM.getOuterHTML', nodeId, (htmlErr, htmlResult) => { - if (htmlErr) { - return reject(htmlErr); - } + on(eventName, cb) { + if (this.chrome_ === null) { + return; + } - resolve(htmlResult.outerHTML); - }); - }); - }); - }); + this.chrome_.on(eventName, cb); } - static getEvaluationContextFor(chrome, url) { + sendCommand(command, params) { return new Promise((resolve, reject) => { - var errorTimeout = setTimeout((_ => - reject(new Error(`Timed out waiting for ${url} execution context`))), - EXECUTION_CONTEXT_TIMEOUT); - - chrome.Runtime.enable(); - chrome.on('Runtime.executionContextCreated', evalContext => { - // console.info(`executionContext: "${evalContext.context.origin}"`); - if (evalContext.context.origin.indexOf(url) !== -1) { - clearTimeout(errorTimeout); - resolve(evalContext.context.id); + this.chrome_.send(command, params, (err, result) => { + if (err) { + return reject(err); } + + resolve(result); }); }); } - evaluateFunction(url, fn) { - let wrappedScriptStr = '(' + fn.toString() + ')()'; - return this.evaluateScript(url, wrappedScriptStr); - } + gotoURL(url, waitForLoad) { + return new Promise((resolve, reject) => { + this.chrome_.Page.enable(); + this.chrome_.Page.navigate({url}, (err, response) => { + if (err) { + reject(err); + } - evaluateScript(url, scriptSrc) { - return this.getInstance().then(chrome => { - // Set up executionContext listener before navigation. - let contextListener = ChromeProtocol.getEvaluationContextFor(chrome, url); - - return this.gotoURL(url, this.WAIT_FOR_LOAD) - .then(_ => contextListener) - .then(contextId => new Promise((resolve, reject) => { - let evalOpts = { - expression: scriptSrc, - contextId: contextId - }; - chrome.Runtime.evaluate(evalOpts, (err, evalResult) => { - if (err || evalResult.wasThrown) { - return reject(evalResult); - } - - let result = evalResult.result; - - chrome.Runtime.getProperties({ - objectId: result.objectId - }, (err, propsResult) => { - if (err) { - /* continue anyway */ - } - result.props = {}; - if (Array.isArray(propsResult.result)) { - propsResult.result.forEach(prop => { - result.props[prop.name] = prop.value ? prop.value.value : - prop.get.description; - }); - } - resolve(result); - }); - }); - })); - }); - } + this.url = url; - gotoURL(url, waitForLoad) { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.Page.enable(); - chrome.Page.navigate({url}, (err, response) => { - if (err) { - reject(err); - } - - if (waitForLoad) { - chrome.Page.loadEventFired(_ => { - this._currentURL = url; - resolve(response); - }); - } else { + if (waitForLoad) { + this.chrome_.Page.loadEventFired(_ => { resolve(response); - } - }); + }); + } else { + resolve(response); + } }); }); } - disableCaching() { - // TODO(paullewis): implement. - return Promise.resolve(); + resetFailureTimeout(reject) { + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } + + this.timeoutID = setTimeout(_ => { + this.disconnect(); + reject(new Error('Trace retrieval timed out')); + }, 15000); } beginTrace() { - this._traceEvents = []; + this.traceEvents_ = []; - return this.getInstance().then(chrome => { + return this.connect().then(chrome => { chrome.Page.enable(); chrome.Tracing.start({ - categories: this.categories.join(','), + categories: this.traceCategories_.join(','), options: 'sampling-frequency=10000' // 1000 is default and too slow. }); chrome.Tracing.dataCollected(data => { - this._traceEvents.push(...data.value); + this.traceEvents_.push(...data.value); }); return true; @@ -228,20 +153,20 @@ class ChromeProtocol { } endTrace() { - return this.getInstance().then(chrome => { + return this.connect().then(chrome => { return new Promise((resolve, reject) => { chrome.Tracing.end(); this.resetFailureTimeout(reject); chrome.Tracing.tracingComplete(_ => { - resolve(this._traceEvents); + resolve(this.traceEvents_); }); }); }); } beginNetworkCollect() { - return this.getInstance().then(chrome => { + return this.connect().then(chrome => { return new Promise((resolve, reject) => { this._networkRecords = []; this._networkRecorder = new NetworkRecorder(this._networkRecords); @@ -268,7 +193,7 @@ class ChromeProtocol { } endNetworkCollect() { - return this.getInstance().then(chrome => { + return this.connect().then(chrome => { return new Promise((resolve, reject) => { chrome.removeListener('Network.requestWillBeSent', this._networkRecorder.onRequestWillBeSent); diff --git a/helpers/browser/driver.old.js b/helpers/browser/driver.old.js new file mode 100644 index 000000000000..e88754a758ee --- /dev/null +++ b/helpers/browser/driver.old.js @@ -0,0 +1,297 @@ +/** + * @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 chromeRemoteInterface = require('chrome-remote-interface'); + +var NetworkRecorder = require('../network-recorder'); + +const EXECUTION_CONTEXT_TIMEOUT = 4000; + +class ChromeProtocol { + constructor(opts) { + opts = opts || {}; + + this.categories = [ + '-*', // exclude default + 'toplevel', + 'blink.console', + 'blink.user_timing', + 'devtools.timeline', + 'disabled-by-default-devtools.timeline', + 'disabled-by-default-devtools.timeline.frame', + 'disabled-by-default-devtools.timeline.stack', + 'disabled-by-default-devtools.screenshot', + 'disabled-by-default-v8.cpu_profile' + ]; + + this._traceEvents = []; + this._currentURL = null; + this._instance = null; + this._networkRecords = []; + this._networkRecorder = null; + + this.getPageHTML = this.getPageHTML.bind(this); + this.evaluateFunction = this.evaluateFunction.bind(this); + this.evaluateScript = this.evaluateScript.bind(this); + this.getServiceWorkerRegistrations = + this.getServiceWorkerRegistrations.bind(this); + this.beginTrace = this.beginTrace.bind(this); + this.endTrace = this.endTrace.bind(this); + this.beginNetworkCollect = this.beginNetworkCollect.bind(this); + this.endNetworkCollect = this.endNetworkCollect.bind(this); + } + + get WAIT_FOR_LOAD() { + return true; + } + + getInstance() { + if (!this._instance) { + this._instance = new Promise((resolve, reject) => { + // @see: github.com/cyrus-and/chrome-remote-interface#moduleoptions-callback + const OPTIONS = {}; + chromeRemoteInterface(OPTIONS, + resolve + ).on('error', e => reject(e)); + }); + } + + return this._instance; + } + + resetFailureTimeout(reject) { + if (this.timeoutID) { + clearTimeout(this.timeoutID); + } + + this.timeoutID = setTimeout(_ => { + // FIXME + // this.discardTab(); + reject(new Error('Trace retrieval timed out')); + }, 15000); + } + + getServiceWorkerRegistrations() { + return this.getInstance().then(chrome => { + return new Promise((resolve, reject) => { + chrome.ServiceWorker.enable(); + chrome.on('ServiceWorker.workerVersionUpdated', data => { + resolve(data); + }); + }); + }); + } + + getPageHTML() { + return this.getInstance().then(chrome => { + return new Promise((resolve, reject) => { + chrome.send('DOM.getDocument', null, (docErr, docResult) => { + if (docErr) { + return reject(docErr); + } + + let nodeId = { + nodeId: docResult.root.nodeId + }; + + chrome.send('DOM.getOuterHTML', nodeId, (htmlErr, htmlResult) => { + if (htmlErr) { + return reject(htmlErr); + } + + resolve(htmlResult.outerHTML); + }); + }); + }); + }); + } + + static getEvaluationContextFor(chrome, url) { + return new Promise((resolve, reject) => { + var errorTimeout = setTimeout((_ => + reject(new Error(`Timed out waiting for ${url} execution context`))), + EXECUTION_CONTEXT_TIMEOUT); + + chrome.Runtime.enable(); + chrome.on('Runtime.executionContextCreated', evalContext => { + // console.info(`executionContext: "${evalContext.context.origin}"`); + if (evalContext.context.origin.indexOf(url) !== -1) { + clearTimeout(errorTimeout); + resolve(evalContext.context.id); + } + }); + }); + } + + evaluateFunction(url, fn) { + let wrappedScriptStr = '(' + fn.toString() + ')()'; + return this.evaluateScript(url, wrappedScriptStr); + } + + evaluateScript(url, scriptSrc) { + return this.getInstance().then(chrome => { + // Set up executionContext listener before navigation. + let contextListener = ChromeProtocol.getEvaluationContextFor(chrome, url); + + return this.gotoURL(url, this.WAIT_FOR_LOAD) + .then(_ => contextListener) + .then(contextId => new Promise((resolve, reject) => { + let evalOpts = { + expression: scriptSrc, + contextId: contextId + }; + chrome.Runtime.evaluate(evalOpts, (err, evalResult) => { + if (err || evalResult.wasThrown) { + return reject(evalResult); + } + + let result = evalResult.result; + + chrome.Runtime.getProperties({ + objectId: result.objectId + }, (err, propsResult) => { + if (err) { + /* continue anyway */ + } + result.props = {}; + if (Array.isArray(propsResult.result)) { + propsResult.result.forEach(prop => { + result.props[prop.name] = prop.value ? prop.value.value : + prop.get.description; + }); + } + resolve(result); + }); + }); + })); + }); + } + + gotoURL(url, waitForLoad) { + return this.getInstance().then(chrome => { + return new Promise((resolve, reject) => { + chrome.Page.enable(); + chrome.Page.navigate({url}, (err, response) => { + if (err) { + reject(err); + } + + if (waitForLoad) { + chrome.Page.loadEventFired(_ => { + this._currentURL = url; + resolve(response); + }); + } else { + resolve(response); + } + }); + }); + }); + } + + disableCaching() { + // TODO(paullewis): implement. + return Promise.resolve(); + } + + beginTrace() { + this._traceEvents = []; + + return this.getInstance().then(chrome => { + chrome.Page.enable(); + chrome.Tracing.start({ + categories: this.categories.join(','), + options: 'sampling-frequency=10000' // 1000 is default and too slow. + }); + + chrome.Tracing.dataCollected(data => { + this._traceEvents.push(...data.value); + }); + + return true; + }); + } + + endTrace() { + return this.getInstance().then(chrome => { + return new Promise((resolve, reject) => { + chrome.Tracing.end(); + this.resetFailureTimeout(reject); + + chrome.Tracing.tracingComplete(_ => { + resolve(this._traceEvents); + }); + }); + }); + } + + beginNetworkCollect() { + return this.getInstance().then(chrome => { + return new Promise((resolve, reject) => { + this._networkRecords = []; + this._networkRecorder = new NetworkRecorder(this._networkRecords); + + chrome.on('Network.requestWillBeSent', + this._networkRecorder.onRequestWillBeSent); + chrome.on('Network.requestServedFromCache', + this._networkRecorder.onRequestServedFromCache); + chrome.on('Network.responseReceived', + this._networkRecorder.onResponseReceived); + chrome.on('Network.dataReceived', + this._networkRecorder.onDataReceived); + chrome.on('Network.loadingFinished', + this._networkRecorder.onLoadingFinished); + chrome.on('Network.loadingFailed', + this._networkRecorder.onLoadingFailed); + + chrome.Network.enable(); + chrome.once('ready', _ => { + resolve(); + }); + }); + }); + } + + endNetworkCollect() { + return this.getInstance().then(chrome => { + return new Promise((resolve, reject) => { + chrome.removeListener('Network.requestWillBeSent', + this._networkRecorder.onRequestWillBeSent); + chrome.removeListener('Network.requestServedFromCache', + this._networkRecorder.onRequestServedFromCache); + chrome.removeListener('Network.responseReceived', + this._networkRecorder.onResponseReceived); + chrome.removeListener('Network.dataReceived', + this._networkRecorder.onDataReceived); + chrome.removeListener('Network.loadingFinished', + this._networkRecorder.onLoadingFinished); + chrome.removeListener('Network.loadingFailed', + this._networkRecorder.onLoadingFailed); + + chrome.Network.disable(); + chrome.once('ready', _ => { + resolve(this._networkRecords); + this._networkRecorder = null; + this._networkRecords = []; + }); + }); + }); + } +} + +module.exports = ChromeProtocol; diff --git a/helpers/extension/driver.js b/helpers/extension/driver.js new file mode 100644 index 000000000000..ced7952f913f --- /dev/null +++ b/helpers/extension/driver.js @@ -0,0 +1,134 @@ +/** + * @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'; + +/* globals chrome */ + +class ExtensionProtocol { + + constructor() { + this.listeners_ = {}; + this.tabId_ = null; + this.url_ = null; + chrome.debugger.onEvent.addListener(this.onEvent_.bind(this)); + } + + get url() { + return this.url_; + } + + set url(url_) { + this.url_ = url_; + } + + connect() { + return this.queryCurrentTab_() + .then(tabId => { + this.tabId_ = tabId; + return this.attachDebugger_(tabId); + }); + } + + disconnect() { + if (this.tabId_ === null) { + return; + } + + this.detachDebugger_(this.tabId_) + .then(_ => { + this.tabId_ = null; + this.url = null; + }); + } + + on(eventName, cb) { + if (typeof this.listeners_[eventName] === 'undefined') { + this.listeners_[eventName] = []; + } + + this.listeners_[eventName].push(cb); + } + + onEvent_(source, method, params) { + if (typeof this.listeners_[method] === 'undefined') { + return; + } + + this.listeners_[method].forEach(cb => { + cb(params); + }); + + // Reset the listeners; + this.listeners_[method].length = 0; + } + + sendCommand(command, params) { + return new Promise((resolve, reject) => { + chrome.debugger.sendCommand({tabId: this.tabId_}, command, params, result => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + resolve(result); + }); + }); + } + + queryCurrentTab_() { + const currentTab = { + active: true, + windowId: chrome.windows.WINDOW_ID_CURRENT + }; + + return new Promise((resolve, reject) => { + chrome.tabs.query(currentTab, tabs => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + this.url = tabs[0].url; + resolve(tabs[0].id); + }); + }); + } + + attachDebugger_(tabId) { + return new Promise((resolve, reject) => { + chrome.debugger.attach({tabId}, '1.1', _ => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + resolve(tabId); + }); + }); + } + + detachDebugger_(tabId) { + return new Promise((resolve, reject) => { + chrome.debugger.detach({tabId}, _ => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + resolve(tabId); + }); + }); + } +} + +module.exports = ExtensionProtocol; diff --git a/index.js b/index.js index f5e3b392f9f3..ad6f1f6ba9fb 100644 --- a/index.js +++ b/index.js @@ -15,33 +15,43 @@ */ 'use strict'; -let URL = 'https://voice-memos.appspot.com'; +const url = 'https://voice-memos.appspot.com'; +const ChromeProtocol = require('./helpers/browser/driver'); -let gatherer = require('./gatherer'); -let auditor = require('./auditor'); -let ChromeProtocol = require('./helpers/browser/driver'); +const Auditor = require('./auditor'); +const Gatherer = require('./gatherer'); const driver = new ChromeProtocol(); +const gatherer = new Gatherer(); +const auditor = new Auditor(); +const gatherers = [ + require('./gatherers/load-trace'), + require('./gatherers/https'), + require('./gatherers/service-worker'), + require('./gatherers/html'), + require('./gatherers/manifest') +]; +const audits = [ + require('./audits/security/is-on-https'), + require('./audits/offline/service-worker'), + require('./audits/mobile-friendly/viewport'), + require('./audits/manifest/exists'), + require('./audits/manifest/background-color'), + require('./audits/manifest/theme-color'), + require('./audits/manifest/icons'), + require('./audits/manifest/icons-192'), + require('./audits/manifest/name'), + require('./audits/manifest/short-name'), + require('./audits/manifest/start-url') +]; -Promise.resolve(driver).then(gatherer([ - require('./audits/viewport-meta-tag/gather'), - require('./audits/minify-html/gather'), - require('./audits/service-worker/gather'), - require('./gatherers/trace'), - require('./gatherers/js-in-context') -], URL)).then(auditor([ - require('./audits/minify-html/audit'), - require('./audits/service-worker/audit'), - require('./audits/time-in-javascript/audit'), - require('./audits/viewport-meta-tag/audit'), - require('./metrics/first-meaningful-paint/audit') -])).then(function(results) { - console.log('all done'); - console.log(results); - // driver.discardTab(); // FIXME: close connection later - // process.exit(0); -}).catch(function(err) { - console.log('error encountered', err); - console.log(err.stack); - throw err; -}); +gatherer + .gather(gatherers, {url, driver}) + .then(artifacts => auditor.audit(artifacts, audits)) + .then(results => { + console.log(results); + }).catch(function(err) { + console.log('error encountered', err); + console.log(err.stack); + throw err; + }); diff --git a/package.json b/package.json index 47828d487a9d..e55f82e78b7e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "chai": "^3.4.0", "eslint": "^2.4.0", "eslint-config-google": "^0.4.0", - "mocha": "^2.3.3" + "mocha": "^2.3.3", + "request": "^2.69.0" } } From c1a4c2d18a55b386ab2961ba2dfbeb34e2ebe8ec Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 15:32:37 +0000 Subject: [PATCH 09/14] Removes tmp old driver file. --- helpers/browser/driver.old.js | 297 ---------------------------------- 1 file changed, 297 deletions(-) delete mode 100644 helpers/browser/driver.old.js diff --git a/helpers/browser/driver.old.js b/helpers/browser/driver.old.js deleted file mode 100644 index e88754a758ee..000000000000 --- a/helpers/browser/driver.old.js +++ /dev/null @@ -1,297 +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 chromeRemoteInterface = require('chrome-remote-interface'); - -var NetworkRecorder = require('../network-recorder'); - -const EXECUTION_CONTEXT_TIMEOUT = 4000; - -class ChromeProtocol { - constructor(opts) { - opts = opts || {}; - - this.categories = [ - '-*', // exclude default - 'toplevel', - 'blink.console', - 'blink.user_timing', - 'devtools.timeline', - 'disabled-by-default-devtools.timeline', - 'disabled-by-default-devtools.timeline.frame', - 'disabled-by-default-devtools.timeline.stack', - 'disabled-by-default-devtools.screenshot', - 'disabled-by-default-v8.cpu_profile' - ]; - - this._traceEvents = []; - this._currentURL = null; - this._instance = null; - this._networkRecords = []; - this._networkRecorder = null; - - this.getPageHTML = this.getPageHTML.bind(this); - this.evaluateFunction = this.evaluateFunction.bind(this); - this.evaluateScript = this.evaluateScript.bind(this); - this.getServiceWorkerRegistrations = - this.getServiceWorkerRegistrations.bind(this); - this.beginTrace = this.beginTrace.bind(this); - this.endTrace = this.endTrace.bind(this); - this.beginNetworkCollect = this.beginNetworkCollect.bind(this); - this.endNetworkCollect = this.endNetworkCollect.bind(this); - } - - get WAIT_FOR_LOAD() { - return true; - } - - getInstance() { - if (!this._instance) { - this._instance = new Promise((resolve, reject) => { - // @see: github.com/cyrus-and/chrome-remote-interface#moduleoptions-callback - const OPTIONS = {}; - chromeRemoteInterface(OPTIONS, - resolve - ).on('error', e => reject(e)); - }); - } - - return this._instance; - } - - resetFailureTimeout(reject) { - if (this.timeoutID) { - clearTimeout(this.timeoutID); - } - - this.timeoutID = setTimeout(_ => { - // FIXME - // this.discardTab(); - reject(new Error('Trace retrieval timed out')); - }, 15000); - } - - getServiceWorkerRegistrations() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.ServiceWorker.enable(); - chrome.on('ServiceWorker.workerVersionUpdated', data => { - resolve(data); - }); - }); - }); - } - - getPageHTML() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.send('DOM.getDocument', null, (docErr, docResult) => { - if (docErr) { - return reject(docErr); - } - - let nodeId = { - nodeId: docResult.root.nodeId - }; - - chrome.send('DOM.getOuterHTML', nodeId, (htmlErr, htmlResult) => { - if (htmlErr) { - return reject(htmlErr); - } - - resolve(htmlResult.outerHTML); - }); - }); - }); - }); - } - - static getEvaluationContextFor(chrome, url) { - return new Promise((resolve, reject) => { - var errorTimeout = setTimeout((_ => - reject(new Error(`Timed out waiting for ${url} execution context`))), - EXECUTION_CONTEXT_TIMEOUT); - - chrome.Runtime.enable(); - chrome.on('Runtime.executionContextCreated', evalContext => { - // console.info(`executionContext: "${evalContext.context.origin}"`); - if (evalContext.context.origin.indexOf(url) !== -1) { - clearTimeout(errorTimeout); - resolve(evalContext.context.id); - } - }); - }); - } - - evaluateFunction(url, fn) { - let wrappedScriptStr = '(' + fn.toString() + ')()'; - return this.evaluateScript(url, wrappedScriptStr); - } - - evaluateScript(url, scriptSrc) { - return this.getInstance().then(chrome => { - // Set up executionContext listener before navigation. - let contextListener = ChromeProtocol.getEvaluationContextFor(chrome, url); - - return this.gotoURL(url, this.WAIT_FOR_LOAD) - .then(_ => contextListener) - .then(contextId => new Promise((resolve, reject) => { - let evalOpts = { - expression: scriptSrc, - contextId: contextId - }; - chrome.Runtime.evaluate(evalOpts, (err, evalResult) => { - if (err || evalResult.wasThrown) { - return reject(evalResult); - } - - let result = evalResult.result; - - chrome.Runtime.getProperties({ - objectId: result.objectId - }, (err, propsResult) => { - if (err) { - /* continue anyway */ - } - result.props = {}; - if (Array.isArray(propsResult.result)) { - propsResult.result.forEach(prop => { - result.props[prop.name] = prop.value ? prop.value.value : - prop.get.description; - }); - } - resolve(result); - }); - }); - })); - }); - } - - gotoURL(url, waitForLoad) { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.Page.enable(); - chrome.Page.navigate({url}, (err, response) => { - if (err) { - reject(err); - } - - if (waitForLoad) { - chrome.Page.loadEventFired(_ => { - this._currentURL = url; - resolve(response); - }); - } else { - resolve(response); - } - }); - }); - }); - } - - disableCaching() { - // TODO(paullewis): implement. - return Promise.resolve(); - } - - beginTrace() { - this._traceEvents = []; - - return this.getInstance().then(chrome => { - chrome.Page.enable(); - chrome.Tracing.start({ - categories: this.categories.join(','), - options: 'sampling-frequency=10000' // 1000 is default and too slow. - }); - - chrome.Tracing.dataCollected(data => { - this._traceEvents.push(...data.value); - }); - - return true; - }); - } - - endTrace() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.Tracing.end(); - this.resetFailureTimeout(reject); - - chrome.Tracing.tracingComplete(_ => { - resolve(this._traceEvents); - }); - }); - }); - } - - beginNetworkCollect() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - this._networkRecords = []; - this._networkRecorder = new NetworkRecorder(this._networkRecords); - - chrome.on('Network.requestWillBeSent', - this._networkRecorder.onRequestWillBeSent); - chrome.on('Network.requestServedFromCache', - this._networkRecorder.onRequestServedFromCache); - chrome.on('Network.responseReceived', - this._networkRecorder.onResponseReceived); - chrome.on('Network.dataReceived', - this._networkRecorder.onDataReceived); - chrome.on('Network.loadingFinished', - this._networkRecorder.onLoadingFinished); - chrome.on('Network.loadingFailed', - this._networkRecorder.onLoadingFailed); - - chrome.Network.enable(); - chrome.once('ready', _ => { - resolve(); - }); - }); - }); - } - - endNetworkCollect() { - return this.getInstance().then(chrome => { - return new Promise((resolve, reject) => { - chrome.removeListener('Network.requestWillBeSent', - this._networkRecorder.onRequestWillBeSent); - chrome.removeListener('Network.requestServedFromCache', - this._networkRecorder.onRequestServedFromCache); - chrome.removeListener('Network.responseReceived', - this._networkRecorder.onResponseReceived); - chrome.removeListener('Network.dataReceived', - this._networkRecorder.onDataReceived); - chrome.removeListener('Network.loadingFinished', - this._networkRecorder.onLoadingFinished); - chrome.removeListener('Network.loadingFailed', - this._networkRecorder.onLoadingFailed); - - chrome.Network.disable(); - chrome.once('ready', _ => { - resolve(this._networkRecords); - this._networkRecorder = null; - this._networkRecords = []; - }); - }); - }); - } -} - -module.exports = ChromeProtocol; From 7c2ce062014991a0b14bd88e59a86763c5b1e3e0 Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 15:55:04 +0000 Subject: [PATCH 10/14] Style nits. Removes unnecessary gather. --- auditor.js | 4 +-- audits/manifest/icons-192.js | 4 +-- audits/offline/service-worker.js | 1 - gatherers/js-in-context.js | 52 -------------------------------- gatherers/manifest.js | 4 +-- helpers/browser/driver.js | 50 +++++++++++++++--------------- helpers/extension/driver.js | 38 +++++++++++------------ 7 files changed, 49 insertions(+), 104 deletions(-) delete mode 100644 gatherers/js-in-context.js diff --git a/auditor.js b/auditor.js index 65479643318f..3782d1af54a2 100644 --- a/auditor.js +++ b/auditor.js @@ -17,14 +17,14 @@ class Auditor { - flattenArtifacts_(artifacts) { + _flattenArtifacts(artifacts) { return artifacts.reduce(function(prev, curr) { return Object.assign(prev, curr); }, {}); } audit(artifacts, audits) { - const flattenedArtifacts = this.flattenArtifacts_(artifacts); + const flattenedArtifacts = this._flattenArtifacts(artifacts); return Promise.all(audits.map(audit => audit.audit(flattenedArtifacts))); } } diff --git a/audits/manifest/icons-192.js b/audits/manifest/icons-192.js index 2a1914bc7c5b..e653a0d83ccf 100644 --- a/audits/manifest/icons-192.js +++ b/audits/manifest/icons-192.js @@ -33,9 +33,7 @@ class ManifestIcons192 { const manifest = manifestParser(inputs.manifest).value; if (manifest && manifest.icons) { - const icons192 = manifest.icons.raw.find(function(i) { - return i.sizes === '192x192'; - }); + const icons192 = manifest.icons.raw.find(i => i.sizes === '192x192'); hasIcons = (!!icons192); } diff --git a/audits/offline/service-worker.js b/audits/offline/service-worker.js index edd61708fcc3..40d6ca956a40 100644 --- a/audits/offline/service-worker.js +++ b/audits/offline/service-worker.js @@ -27,7 +27,6 @@ class ServiceWorker { } static audit(inputs) { - // Test the Service Worker registrations for validity. const registrations = inputs.serviceWorkers.versions; const activatedRegistrations = registrations.filter(reg => reg.status === 'activated'); diff --git a/gatherers/js-in-context.js b/gatherers/js-in-context.js deleted file mode 100644 index ea8c3289a464..000000000000 --- a/gatherers/js-in-context.js +++ /dev/null @@ -1,52 +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'; - -/* global document, XMLHttpRequest */ - -var manifestParser = require('../helpers/manifest-parser'); - -/** - * Gather data by running JS in page's context. - */ - -function fetchManifest() { - var link = document.querySelector('link[rel=manifest]'); - - if (!link) { - return 'Manifest link not found.'; - } - - var request = new XMLHttpRequest(); - request.open('GET', link.href, false); // `false` makes the request synchronous - request.send(null); - - if (request.status === 200) { - return request.responseText; - } - - return 'Unable to fetch manifest at ' + link; -} - -var JSGatherer = { - run: function(driver, url) { - return driver.evaluateFunction(url, fetchManifest) - .then(result => manifestParser(result.value)) - .then(parsedManifest => ({parsedManifest})); - } -}; - -module.exports = JSGatherer; diff --git a/gatherers/manifest.js b/gatherers/manifest.js index 1e4e5bcb066a..1d024b3d2e71 100644 --- a/gatherers/manifest.js +++ b/gatherers/manifest.js @@ -21,7 +21,7 @@ const Gather = require('./gather'); class Manifest extends Gather { - static loadFromURL(options, manifestURL) { + static _loadFromURL(options, manifestURL) { if (typeof window !== 'undefined') { const finalURL = (new window.URL(options.driver.url).origin) + manifestURL; return fetch(finalURL).then(response => response.text()); @@ -69,7 +69,7 @@ class Manifest extends Gather { return attributes[hrefIndex + 1]; }) - .then(manifestURL => Manifest.loadFromURL(options, manifestURL)); + .then(manifestURL => Manifest._loadFromURL(options, manifestURL)); }) .then(manifest => ({manifest})); } diff --git a/helpers/browser/driver.js b/helpers/browser/driver.js index 40ef7dd8b9ff..6a30a7e97470 100644 --- a/helpers/browser/driver.js +++ b/helpers/browser/driver.js @@ -26,10 +26,10 @@ class ChromeProtocol { } constructor() { - this.url_ = null; - this.chrome_ = null; - this.traceEvents_ = []; - this.traceCategories_ = [ + this._url = null; + this._chrome = null; + this._traceEvents = []; + this._traceCategories = [ '-*', // exclude default 'toplevel', 'blink.console', @@ -46,28 +46,28 @@ class ChromeProtocol { } get url() { - return this.url_; + return this._url; } - set url(url_) { - this.url_ = url_; + set url(_url) { + this._url = _url; } connect() { return new Promise((resolve, reject) => { - if (this.chrome_) { - return resolve(this.chrome_); + if (this._chrome) { + return resolve(this._chrome); } chromeRemoteInterface({}, chrome => { - this.chrome_ = chrome; + this._chrome = chrome; resolve(chrome); }).on('error', e => reject(e)); }); } disconnect() { - if (this.chrome_ === null) { + if (this._chrome === null) { return; } @@ -76,22 +76,22 @@ class ChromeProtocol { this.timeoutID = null; } - this.chrome_.close(); - this.chrome_ = null; + this._chrome.close(); + this._chrome = null; this.url = null; } on(eventName, cb) { - if (this.chrome_ === null) { + if (this._chrome === null) { return; } - this.chrome_.on(eventName, cb); + this._chrome.on(eventName, cb); } sendCommand(command, params) { return new Promise((resolve, reject) => { - this.chrome_.send(command, params, (err, result) => { + this._chrome.send(command, params, (err, result) => { if (err) { return reject(err); } @@ -103,8 +103,8 @@ class ChromeProtocol { gotoURL(url, waitForLoad) { return new Promise((resolve, reject) => { - this.chrome_.Page.enable(); - this.chrome_.Page.navigate({url}, (err, response) => { + this._chrome.Page.enable(); + this._chrome.Page.navigate({url}, (err, response) => { if (err) { reject(err); } @@ -112,7 +112,7 @@ class ChromeProtocol { this.url = url; if (waitForLoad) { - this.chrome_.Page.loadEventFired(_ => { + this._chrome.Page.loadEventFired(_ => { resolve(response); }); } else { @@ -122,7 +122,7 @@ class ChromeProtocol { }); } - resetFailureTimeout(reject) { + _resetFailureTimeout(reject) { if (this.timeoutID) { clearTimeout(this.timeoutID); this.timeoutID = null; @@ -135,17 +135,17 @@ class ChromeProtocol { } beginTrace() { - this.traceEvents_ = []; + this._traceEvents = []; return this.connect().then(chrome => { chrome.Page.enable(); chrome.Tracing.start({ - categories: this.traceCategories_.join(','), + categories: this._traceCategories.join(','), options: 'sampling-frequency=10000' // 1000 is default and too slow. }); chrome.Tracing.dataCollected(data => { - this.traceEvents_.push(...data.value); + this._traceEvents.push(...data.value); }); return true; @@ -156,10 +156,10 @@ class ChromeProtocol { return this.connect().then(chrome => { return new Promise((resolve, reject) => { chrome.Tracing.end(); - this.resetFailureTimeout(reject); + this._resetFailureTimeout(reject); chrome.Tracing.tracingComplete(_ => { - resolve(this.traceEvents_); + resolve(this._traceEvents); }); }); }); diff --git a/helpers/extension/driver.js b/helpers/extension/driver.js index ced7952f913f..11501d637793 100644 --- a/helpers/extension/driver.js +++ b/helpers/extension/driver.js @@ -21,64 +21,64 @@ class ExtensionProtocol { constructor() { - this.listeners_ = {}; - this.tabId_ = null; - this.url_ = null; - chrome.debugger.onEvent.addListener(this.onEvent_.bind(this)); + this._listeners = {}; + this._tabId = null; + this._url = null; + chrome.debugger.onEvent.addListener(this._onEvent.bind(this)); } get url() { - return this.url_; + return this._url; } - set url(url_) { - this.url_ = url_; + set url(_url) { + this._url = _url; } connect() { return this.queryCurrentTab_() .then(tabId => { - this.tabId_ = tabId; + this._tabId = tabId; return this.attachDebugger_(tabId); }); } disconnect() { - if (this.tabId_ === null) { + if (this._tabId === null) { return; } - this.detachDebugger_(this.tabId_) + this.detachDebugger_(this._tabId) .then(_ => { - this.tabId_ = null; + this._tabId = null; this.url = null; }); } on(eventName, cb) { - if (typeof this.listeners_[eventName] === 'undefined') { - this.listeners_[eventName] = []; + if (typeof this._listeners[eventName] === 'undefined') { + this._listeners[eventName] = []; } - this.listeners_[eventName].push(cb); + this._listeners[eventName].push(cb); } - onEvent_(source, method, params) { - if (typeof this.listeners_[method] === 'undefined') { + _onEvent(source, method, params) { + if (typeof this._listeners[method] === 'undefined') { return; } - this.listeners_[method].forEach(cb => { + this._listeners[method].forEach(cb => { cb(params); }); // Reset the listeners; - this.listeners_[method].length = 0; + this._listeners[method].length = 0; } sendCommand(command, params) { return new Promise((resolve, reject) => { - chrome.debugger.sendCommand({tabId: this.tabId_}, command, params, result => { + chrome.debugger.sendCommand({tabId: this._tabId}, command, params, result => { if (chrome.runtime.lastError) { return reject(chrome.runtime.lastError); } From efa41a8cb88a022e5782025581e17d8d9c4f1319 Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 15:57:10 +0000 Subject: [PATCH 11/14] Removes log. --- extension/gulpfile.babel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/extension/gulpfile.babel.js b/extension/gulpfile.babel.js index 8f03a84d6565..16c30dc86831 100644 --- a/extension/gulpfile.babel.js +++ b/extension/gulpfile.babel.js @@ -87,7 +87,6 @@ gulp.task('chromeManifest', () => { }); gulp.task('babel', () => { - console.log('Babeling up...'); return gulp.src([ 'app/scripts.babel/app.js', 'app/scripts.babel/chromereload.js', From ee00670ba789ebdb4ff0cedf268e49a1dfa1f2bf Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 16:02:28 +0000 Subject: [PATCH 12/14] Updates the manifest for tidiness. --- extension/app/manifest.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/app/manifest.json b/extension/app/manifest.json index 4c7d32391600..4b427b728146 100644 --- a/extension/app/manifest.json +++ b/extension/app/manifest.json @@ -1,8 +1,8 @@ { - "name": "__MSG_appName__", - "version": "0.0.3", + "name": "Lighthouse", + "version": "0.0.5", "manifest_version": 2, - "description": "__MSG_appDescription__", + "description": "Helping you avoid Progressive Web App woes.", "icons": { "16": "images/icon-16.png", "128": "images/icon-128.png" @@ -22,7 +22,7 @@ "default_icon": { "38": "images/icon-38.png" }, - "default_title": "lighthouse", + "default_title": "Lighthouse", "default_popup": "popup.html" } } From 06166c3cac53671a0da993c3f70befb4df4158db Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 16:24:21 +0000 Subject: [PATCH 13/14] Adds URL gatherer. --- audits/offline/service-worker.js | 6 ++++-- extension/app/scripts.babel/pwa-check.js | 1 + gatherers/url.js | 26 ++++++++++++++++++++++++ index.js | 1 + 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 gatherers/url.js diff --git a/audits/offline/service-worker.js b/audits/offline/service-worker.js index 40d6ca956a40..2780fc4918f3 100644 --- a/audits/offline/service-worker.js +++ b/audits/offline/service-worker.js @@ -28,8 +28,10 @@ class ServiceWorker { static audit(inputs) { const registrations = inputs.serviceWorkers.versions; - const activatedRegistrations = registrations.filter(reg => - reg.status === 'activated'); + const activatedRegistrations = registrations.filter(reg => { + return reg.status === 'activated' && + reg.scriptURL.startsWith(inputs.url); + }); return { value: (activatedRegistrations.length > 0), diff --git a/extension/app/scripts.babel/pwa-check.js b/extension/app/scripts.babel/pwa-check.js index 22d28adafe05..677284ead4f8 100644 --- a/extension/app/scripts.babel/pwa-check.js +++ b/extension/app/scripts.babel/pwa-check.js @@ -24,6 +24,7 @@ const driver = new ExtensionProtocol(); const gatherer = new Gatherer(); const auditor = new Auditor(); const gatherers = [ + require('../../../gatherers/url'), require('../../../gatherers/https'), require('../../../gatherers/service-worker'), require('../../../gatherers/html'), diff --git a/gatherers/url.js b/gatherers/url.js new file mode 100644 index 000000000000..144acc7851ee --- /dev/null +++ b/gatherers/url.js @@ -0,0 +1,26 @@ +/** + * 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 Gather = require('./gather'); + +class URL extends Gather { + static gather(options) { + return Promise.resolve({url: options.url}); + } +} + +module.exports = URL; diff --git a/index.js b/index.js index ad6f1f6ba9fb..6b45d774894c 100644 --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ const driver = new ChromeProtocol(); const gatherer = new Gatherer(); const auditor = new Auditor(); const gatherers = [ + require('./gatherers/url'), require('./gatherers/load-trace'), require('./gatherers/https'), require('./gatherers/service-worker'), From 8b054759f959eb3a3bc12ca5491b2b9c8bb353d9 Mon Sep 17 00:00:00 2001 From: Paul Lewis Date: Tue, 22 Mar 2016 16:24:35 +0000 Subject: [PATCH 14/14] Fixes PageAgent global for network recorder. --- helpers/network-recorder.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/helpers/network-recorder.js b/helpers/network-recorder.js index cb5360995970..b15397798415 100644 --- a/helpers/network-recorder.js +++ b/helpers/network-recorder.js @@ -61,6 +61,25 @@ global.SecurityAgent = { } }; +// From https://chromium.googlesource.com/chromium/src/third_party/WebKit/Source/devtools/+/master/protocol.json#93 +global.PageAgent = { + ResourceType: { + Document: 'document', + Stylesheet: 'stylesheet', + Image: 'image', + Media: 'media', + Font: 'font', + Script: 'script', + TextTrack: 'texttrack', + XHR: 'xhr', + Fetch: 'fetch', + EventSource: 'eventsource', + WebSocket: 'websocket', + Manifest: 'manifest', + Other: 'other' + } +}; + require('chrome-devtools-frontend/front_end/common/Object.js'); require('chrome-devtools-frontend/front_end/common/ParsedURL.js'); require('chrome-devtools-frontend/front_end/common/ResourceType.js');