diff --git a/lighthouse-core/audits/geolocation-on-start.js b/lighthouse-core/audits/geolocation-on-start.js new file mode 100644 index 000000000000..a34c94eb406e --- /dev/null +++ b/lighthouse-core/audits/geolocation-on-start.js @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Audit = require('./audit'); + +class GeolocationOnStart extends Audit { + /** + * @return {!AuditMeta} + */ + static get meta() { + return { + category: 'UX', + name: 'geolocation', + description: 'Page does not automatically request geolocation', + requiredArtifacts: ['GeolocationOnStart'] + }; + } + + /** + * @param {!Artifacts} artifacts + * @return {!AuditResult} + */ + static audit(artifacts) { + if (typeof artifacts.GeolocationOnStart === 'undefined' || + artifacts.GeolocationOnStart === -1) { + return GeolocationOnStart.generateAuditResult({ + rawValue: false, + debugString: 'Unable to get geolocation values.' + }); + } + + return GeolocationOnStart.generateAuditResult({ + rawValue: artifacts.GeolocationOnStart + }); + } +} + +module.exports = GeolocationOnStart; diff --git a/lighthouse-core/closure/typedefs/Artifacts.js b/lighthouse-core/closure/typedefs/Artifacts.js index ab87f4fc65bd..cbd6d0c42e18 100644 --- a/lighthouse-core/closure/typedefs/Artifacts.js +++ b/lighthouse-core/closure/typedefs/Artifacts.js @@ -79,3 +79,6 @@ Artifacts.prototype.ContentWidth; /** @type {!Array} */ Artifacts.prototype.CacheContents; + +/** @type {boolean|number} */ +Artifacts.prototype.GeolocationOnStart; diff --git a/lighthouse-core/config/default.json b/lighthouse-core/config/default.json index 2d230bcbe9e0..064065acffc2 100644 --- a/lighthouse-core/config/default.json +++ b/lighthouse-core/config/default.json @@ -16,7 +16,8 @@ "critical-request-chains", "speedline", "content-width", - "cache-contents" + "cache-contents", + "geolocation-on-start" ] }, { @@ -66,7 +67,8 @@ "label", "tabindex", "content-width", - "cache-start-url" + "cache-start-url", + "geolocation-on-start" ], "aggregations": [{ @@ -277,6 +279,10 @@ "rawValue": true, "weight": 1 }, + "geolocation-on-start": { + "rawValue": true, + "weight": 1 + }, "serviceworker-push": { "rawValue": true, "weight": 0, diff --git a/lighthouse-core/driver/drivers/driver.js b/lighthouse-core/driver/drivers/driver.js index 70a84179b9cf..4ecfd60fc3f1 100644 --- a/lighthouse-core/driver/drivers/driver.js +++ b/lighthouse-core/driver/drivers/driver.js @@ -105,6 +105,12 @@ class DriverBase { return Promise.reject(new Error('Not implemented')); } + evaluateScriptOnLoad(scriptSource) { + return this.sendCommand('Page.addScriptToEvaluateOnLoad', { + scriptSource + }); + } + evaluateAsync(asyncExpression) { return new Promise((resolve, reject) => { let asyncTimeout; diff --git a/lighthouse-core/driver/gatherers/geolocation-on-start.js b/lighthouse-core/driver/gatherers/geolocation-on-start.js new file mode 100644 index 000000000000..d8134e30fdf6 --- /dev/null +++ b/lighthouse-core/driver/gatherers/geolocation-on-start.js @@ -0,0 +1,62 @@ +/** + * @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 Gather = require('./gather'); + +/** + * @fileoverview Tests whether the page attempts to request geolocation on page load. This often + * represents a poor user experience, since it lacks context. As such, if the page requests + * geolocation the gatherer will intercept the call and mark a boolean flag to true. The audit that + * corresponds with this gatherer then checks for the flag. + * @author Paul Lewis + */ + +/* global navigator, window, __returnResults */ + +/* istanbul ignore next */ +function overrideGeo() { + window.__didNotCallGeo = true; + // Override the geo functions so that if they're called they're intercepted and we know about it. + navigator.geolocation.getCurrentPosition = + navigator.geolocation.watchPosition = function() { + window.__didNotCallGeo = false; + }; +} + +function collectGeoState() { + __returnResults(window.__didNotCallGeo); +} + +class GeolocationOnStart extends Gather { + + beforePass(options) { + return options.driver.evaluateScriptOnLoad(`(${overrideGeo.toString()}())`); + } + + afterPass(options) { + return options.driver.evaluateAsync(`(${collectGeoState.toString()}())`) + .then(returnedValue => { + this.artifact = returnedValue; + }, _ => { + this.artifact = -1; + return; + }); + } +} + +module.exports = GeolocationOnStart; diff --git a/lighthouse-core/test/audits/geolocation-on-start.js b/lighthouse-core/test/audits/geolocation-on-start.js new file mode 100644 index 000000000000..5c6a74e3e4ca --- /dev/null +++ b/lighthouse-core/test/audits/geolocation-on-start.js @@ -0,0 +1,43 @@ +/** + * 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. + */ +const Audit = require('../../audits/geolocation-on-start.js'); +const assert = require('assert'); + +/* global describe, it*/ + +describe('UX: geolocation audit', () => { + it('fails when no input present', () => { + return assert.equal(Audit.audit({}).rawValue, false); + }); + + it('fails when no input present', () => { + return assert.equal(Audit.audit({ + GeolocationOnStart: -1 + }).rawValue, false); + }); + + it('fails when geolocation has been automatically requested', () => { + return assert.equal(Audit.audit({ + GeolocationOnStart: false + }).rawValue, false); + }); + + it('passes when geolocation has not been automatically requested', () => { + return assert.equal(Audit.audit({ + GeolocationOnStart: true + }).rawValue, true); + }); +}); diff --git a/lighthouse-core/test/driver/gatherers/geolocation-on-start.js b/lighthouse-core/test/driver/gatherers/geolocation-on-start.js new file mode 100644 index 000000000000..a6c16cfcbd9c --- /dev/null +++ b/lighthouse-core/test/driver/gatherers/geolocation-on-start.js @@ -0,0 +1,68 @@ +/** + * 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'; + +/* eslint-env mocha */ + +const GeolocationGatherer = require('../../../driver/gatherers/geolocation-on-start'); +const assert = require('assert'); +let geolocationGatherer; + +describe('Geolocation gatherer', () => { + // Reset the Gatherer before each test. + beforeEach(() => { + geolocationGatherer = new GeolocationGatherer(); + }); + + it('returns an artifact', () => { + return geolocationGatherer.beforePass({ + driver: { + evaluateScriptOnLoad() { + return Promise.resolve(); + } + } + }).then(_ => geolocationGatherer.afterPass({ + driver: { + evaluateAsync() { + return Promise.resolve(true); + } + } + })).then(_ => { + assert.ok(typeof geolocationGatherer.artifact === 'boolean'); + assert.equal(geolocationGatherer.artifact, true); + }); + }); + + it('handles driver failure', () => { + return geolocationGatherer.beforePass({ + driver: { + evaluateScriptOnLoad() { + return Promise.resolve(); + } + } + }).then(_ => geolocationGatherer.afterPass({ + driver: { + evaluateAsync() { + return Promise.reject('such a fail'); + } + } + })).then(_ => { + assert(false); + }).catch(_ => { + assert.equal(geolocationGatherer.artifact, -1); + }); + }); +});