diff --git a/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html b/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html index ff0f21ee9eaf..fb38dddc70ba 100644 --- a/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html +++ b/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html @@ -208,39 +208,6 @@

Do better web tester page

const db = openDatabase('mydb', '1.0', 'my first database', 1024); } -function mutationEvenTest() { - // FAIL - document.addEventListener('DOMNodeInserted', function(e) { - console.log('DOMNodeInserted'); - }); - - // FAIL - document.addEventListener('DOMNodeRemoved', function(e) { - console.log('DOMNodeRemoved'); - }); - - // FAIL - document.body.addEventListener('DOMNodeInserted', function(e) { - console.log('DOMNodeInserted'); - }); - - // FAIL - const el = document.querySelector('#touchmove-section'); - el.addEventListener('DOMNodeInserted', function(e) { - console.log('DOMNodeInserted on element'); - }); - - // FAIL - window.addEventListener('DOMNodeInserted', function(e) { - console.log('DOMNodeInserted'); - }); - - // PASS - not MutationEvents - window.addEventListener('DOMContentLoaded', function(e) { - console.log('DOMContentLoaded'); - }); -} - function passiveEventsListenerTest() { // FAIL window.addEventListener('wheel', function(e) { @@ -364,7 +331,6 @@

Do better web tester page

dateNowTest(); consoleTimeTest(); websqlTest(); - mutationEvenTest(); passiveEventsListenerTest(); geolocationOnStartTest(); notificationOnStartTest(); @@ -387,9 +353,6 @@

Do better web tester page

if (params.has('websql')) { websqlTest(); } - if (params.has('mutationEvents')) { - mutationEvenTest(); - } if (params.has('passiveEvents')) { passiveEventsListenerTest(); } diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js index 3e287e4a55db..a67ea62b9df7 100644 --- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js +++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js @@ -61,14 +61,6 @@ module.exports = [ }, }, }, - 'no-mutation-events': { - score: 0, - details: { - items: { - length: 6, - }, - }, - }, 'no-vulnerable-libraries': { score: 0, details: { diff --git a/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js b/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js index c325ba59d781..8020f93f02b7 100644 --- a/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js +++ b/lighthouse-cli/test/smokehouse/offline-local/offline-expectations.js @@ -36,9 +36,6 @@ module.exports = [ 'no-document-write': { score: 1, }, - 'no-mutation-events': { - score: 1, - }, 'no-websql': { score: 1, }, diff --git a/lighthouse-core/audits/dobetterweb/no-mutation-events.js b/lighthouse-core/audits/dobetterweb/no-mutation-events.js deleted file mode 100644 index 6e5ee5501169..000000000000 --- a/lighthouse-core/audits/dobetterweb/no-mutation-events.js +++ /dev/null @@ -1,92 +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. - */ - -/** - * @fileoverview Audit a page to see if it is using Mutation Events (and suggest - * MutationObservers instead). - */ - -'use strict'; - -const URL = require('../../lib/url-shim'); -const Audit = require('../audit'); -const EventHelpers = require('../../lib/event-helpers'); - -class NoMutationEventsAudit extends Audit { - static get MUTATION_EVENTS() { - return [ - 'DOMAttrModified', - 'DOMAttributeNameChanged', - 'DOMCharacterDataModified', - 'DOMElementNameChanged', - 'DOMNodeInserted', - 'DOMNodeInsertedIntoDocument', - 'DOMNodeRemoved', - 'DOMNodeRemovedFromDocument', - 'DOMSubtreeModified', - ]; - } - - /** - * @return {LH.Audit.Meta} - */ - static get meta() { - return { - name: 'no-mutation-events', - description: 'Avoids Mutation Events in its own scripts', - failureDescription: 'Uses Mutation Events in its own scripts', - helpText: 'Mutation Events are deprecated and harm performance. Consider using Mutation ' + - 'Observers instead. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/mutation-events).', - requiredArtifacts: ['URL', 'EventListeners'], - }; - } - - /** - * @param {LH.Artifacts} artifacts - * @return {LH.Audit.Product} - */ - static audit(artifacts) { - /** @type {string[]} */ - const warnings = []; - const listeners = artifacts.EventListeners; - - const results = listeners.filter(loc => { - const isMutationEvent = this.MUTATION_EVENTS.includes(loc.type); - let sameHost = URL.hostsMatch(artifacts.URL.finalUrl, loc.url); - - if (!URL.isValid(loc.url) && isMutationEvent) { - sameHost = true; - warnings.push(URL.INVALID_URL_DEBUG_STRING); - } - - return sameHost && isMutationEvent; - }).map(EventHelpers.addFormattedCodeSnippet); - - const groupedResults = EventHelpers.groupCodeSnippetsByLocation(results); - - const headings = [ - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'type', itemType: 'code', text: 'Event'}, - {key: 'line', itemType: 'text', text: 'Line'}, - {key: 'col', itemType: 'text', text: 'Col'}, - {key: 'pre', itemType: 'code', text: 'Snippet'}, - ]; - const details = NoMutationEventsAudit.makeTableDetails(headings, groupedResults); - - return { - rawValue: groupedResults.length === 0, - extendedInfo: { - value: { - results: groupedResults, - }, - }, - details, - warnings, - }; - } -} - -module.exports = NoMutationEventsAudit; diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js index b9fcaa44c2cf..ec27a2a17040 100644 --- a/lighthouse-core/config/default-config.js +++ b/lighthouse-core/config/default-config.js @@ -29,7 +29,6 @@ module.exports = { 'chrome-console-messages', 'image-usage', 'accessibility', - 'dobetterweb/all-event-listeners', 'dobetterweb/anchors-with-no-rel-noopener', 'dobetterweb/appcache', 'dobetterweb/domstats', @@ -166,7 +165,6 @@ module.exports = { 'dobetterweb/external-anchors-use-rel-noopener', 'dobetterweb/geolocation-on-start', 'dobetterweb/no-document-write', - 'dobetterweb/no-mutation-events', 'dobetterweb/no-vulnerable-libraries', 'dobetterweb/no-websql', 'dobetterweb/notification-on-start', @@ -371,7 +369,6 @@ module.exports = { {id: 'is-on-https', weight: 1}, {id: 'uses-http2', weight: 1}, {id: 'uses-passive-event-listeners', weight: 1}, - {id: 'no-mutation-events', weight: 1}, {id: 'no-document-write', weight: 1}, {id: 'external-anchors-use-rel-noopener', weight: 1}, {id: 'geolocation-on-start', weight: 1}, diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index 5f5ebed34a6d..4a43dcc0127a 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -1077,38 +1077,6 @@ class Driver { await this.evaluteScriptOnNewDocument(scriptStr); } - /** - * Keeps track of calls to a JS function and returns a list of {url, line, col} - * of the usage. Should be called before page load (in beforePass). - * @param {string} funcName The function name to track ('Date.now', 'console.time'). - * @return {function(): Promise>} - * Call this method when you want results. - */ - captureFunctionCallSites(funcName) { - const globalVarToPopulate = `window['__${funcName}StackTraces']`; - const collectUsage = () => { - return this.evaluateAsync( - `Array.from(${globalVarToPopulate}).map(item => JSON.parse(item))`) - .then(result => { - if (!Array.isArray(result)) { - throw new Error( - 'Driver failure: Expected evaluateAsync results to be an array ' + - `but got "${JSON.stringify(result)}" instead.`); - } - // Filter out usage from extension content scripts. - return result.filter(item => !item.isExtension); - }); - }; - - const funcBody = pageFunctions.captureJSCallUsage.toString(); - - this.evaluteScriptOnNewDocument(` - ${globalVarToPopulate} = new Set(); - (${funcName} = ${funcBody}(${funcName}, ${globalVarToPopulate}))`); - - return collectUsage; - } - /** * @param {Array} urls URL patterns to block. Wildcards ('*') are allowed. * @return {Promise} diff --git a/lighthouse-core/gather/gatherers/dobetterweb/all-event-listeners.js b/lighthouse-core/gather/gatherers/dobetterweb/all-event-listeners.js deleted file mode 100644 index 556a5ea2f862..000000000000 --- a/lighthouse-core/gather/gatherers/dobetterweb/all-event-listeners.js +++ /dev/null @@ -1,154 +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. - */ - -/** - * @fileoverview Tests whether the page is using passive event listeners. - */ - -'use strict'; - -const Gatherer = require('../gatherer'); -const Driver = require('../../driver.js'); // eslint-disable-line no-unused-vars -const Element = require('../../../lib/element.js'); // eslint-disable-line no-unused-vars - -class EventListeners extends Gatherer { - /** - * @param {Driver} driver - */ - async listenForScriptParsedEvents(driver) { - /** @type {Map} */ - const parsedScripts = new Map(); - /** @param {LH.Crdp.Debugger.ScriptParsedEvent} script */ - const scriptListener = script => { - parsedScripts.set(script.scriptId, script); - }; - - // Enable and disable Debugger domain, triggering flood of parsed scripts. - driver.on('Debugger.scriptParsed', scriptListener); - await driver.sendCommand('Debugger.enable'); - await driver.sendCommand('Debugger.disable'); - driver.off('Debugger.scriptParsed', scriptListener); - - return parsedScripts; - } - - /** - * @param {Driver} driver - * @param {number|string} nodeIdOrObject The node id of the element or the - * string of an object ('document', 'window'). - * @return {Promise<{listeners: Array, tagName: string}>} - * @private - */ - _listEventListeners(driver, nodeIdOrObject) { - let promise; - - if (typeof nodeIdOrObject === 'string') { - promise = driver.sendCommand('Runtime.evaluate', { - expression: nodeIdOrObject, - objectGroup: 'event-listeners-gatherer', // populates event handler info. - }).then(result => result.result); - } else { - promise = driver.sendCommand('DOM.resolveNode', { - nodeId: nodeIdOrObject, - objectGroup: 'event-listeners-gatherer', // populates event handler info. - }) - .then(result => result.object) - .catch(() => { - return { - objectId: null, - description: '', - }; - }); - } - - return promise.then(obj => { - const objectId = obj.objectId; - const description = obj.description; - if (!objectId || !description) { - return {listeners: [], tagName: ''}; - } - - return driver.sendCommand('DOMDebugger.getEventListeners', { - objectId, - }).then(results => { - return {listeners: results.listeners, tagName: description}; - }); - }); - } - - /** - * Collects the event listeners attached to an object and formats the results. - * listenForScriptParsedEvents should be called before this method to ensure - * the page's parsed scripts are collected at page load. - * @param {Driver} driver - * @param {Map} parsedScripts - * @param {string|number} nodeId The node to look for attached event listeners. - * @return {Promise} List of event listeners attached to - * the node. - */ - getEventListeners(driver, parsedScripts, nodeId) { - /** @type {LH.Artifacts['EventListeners']} */ - const matchedListeners = []; - - return this._listEventListeners(driver, nodeId).then(results => { - results.listeners.forEach(listener => { - // Slim down the list of parsed scripts to match the found event - // listeners that have the same script id. - const script = parsedScripts.get(listener.scriptId); - if (script) { - // Combine the EventListener object and the result of the - // Debugger.scriptParsed event so we get .url and other - // needed properties. - matchedListeners.push({ - url: script.url, - type: listener.type, - handler: listener.handler, - objectName: results.tagName, - // Note: line/col numbers are zero-index. Add one to each so we have - // actual file line/col numbers. - line: listener.lineNumber + 1, - col: listener.columnNumber + 1, - }); - } - }); - - return matchedListeners; - }); - } - - /** - * Aggregates the event listeners used on each element into a single list. - * @param {Driver} driver - * @param {Map} parsedScripts - * @param {Array} nodeIds List of objects or nodeIds to fetch event listeners for. - * @return {Promise} Resolves to a list of all the event - * listeners found across the elements. - */ - collectListeners(driver, parsedScripts, nodeIds) { - // Gather event listeners from each node in parallel. - return Promise.all(nodeIds.map(node => this.getEventListeners(driver, parsedScripts, node))) - .then(nestedListeners => nestedListeners.reduce((prev, curr) => prev.concat(curr))); - } - - /** - * @param {LH.Gatherer.PassContext} passContext - * @return {Promise} - */ - async afterPass(passContext) { - const driver = passContext.driver; - await passContext.driver.sendCommand('DOM.enable'); - const parsedScripts = await this.listenForScriptParsedEvents(driver); - - const elements = await passContext.driver.getElementsInDocument(); - const elementIds = [...elements.map(el => el.getNodeId()), 'document', 'window']; - - const listeners = await this.collectListeners(driver, parsedScripts, elementIds); - await passContext.driver.sendCommand('DOM.disable'); - return listeners; - } -} - -module.exports = EventListeners; diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js index 38708ac6f25f..8480ee29503c 100644 --- a/lighthouse-core/lib/page-functions.js +++ b/lighthouse-core/lib/page-functions.js @@ -6,74 +6,11 @@ // @ts-nocheck 'use strict'; -/** - * Helper functions that are passed by `toString()` by Driver to be evaluated in target page. - */ +/* global window */ /** - * Tracks function call usage. Used by captureJSCalls to inject code into the page. - * @param {function(...*): *} funcRef The function call to track. - * @param {!Set} set An empty set to populate with stack traces. Should be - * on the global object. - * @return {function(...*): *} A wrapper around the original function. + * Helper functions that are passed by `toString()` by Driver to be evaluated in target page. */ -/* istanbul ignore next */ -function captureJSCallUsage(funcRef, set) { - /* global window */ - const __nativeError = window.__nativeError || Error; - const originalFunc = funcRef; - const originalPrepareStackTrace = __nativeError.prepareStackTrace; - - return function(...args) { - // Note: this function runs in the context of the page that is being audited. - - // See v8's Stack Trace API https://github.com/v8/v8/wiki/Stack-Trace-API#customizing-stack-traces - __nativeError.prepareStackTrace = function(error, structStackTrace) { - // First frame is the function we injected (the one that just threw). - // Second, is the actual callsite of the funcRef we're after. - const callFrame = structStackTrace[1]; - let url = callFrame.getFileName() || callFrame.getEvalOrigin(); - const line = callFrame.getLineNumber(); - const col = callFrame.getColumnNumber(); - const isEval = callFrame.isEval(); - let isExtension = false; - const stackTrace = structStackTrace.slice(1).map(callsite => callsite.toString()); - - // If we don't have an URL, (e.g. eval'd code), use the 2nd entry in the - // stack trace. First is eval context: eval()::. - // Second is the callsite where eval was called. - // See https://crbug.com/646849. - if (isEval) { - url = stackTrace[1]; - } - - // Chrome extension content scripts can produce an empty .url and - // ":line:col" for the first entry in the stack trace. - if (stackTrace[0].startsWith('')) { - // Note: Although captureFunctionCallSites filters out crx usage, - // filling url here provides context. We may want to keep those results - // some day. - url = stackTrace[0]; - isExtension = true; - } - - // TODO: add back when we want stack traces. - // Stack traces were removed from the return object in - // https://github.com/GoogleChrome/lighthouse/issues/957 so callsites - // would be unique. - return {url, args, line, col, isEval, isExtension}; // return value is e.stack - }; - const e = new __nativeError(`__called ${funcRef.name}__`); - set.add(JSON.stringify(e.stack)); - - // Restore prepareStackTrace so future errors use v8's formatter and not - // our custom one. - __nativeError.prepareStackTrace = originalPrepareStackTrace; - - // eslint-disable-next-line no-invalid-this - return originalFunc.apply(this, args); - }; -} /** * The `exceptionDetails` provided by the debugger protocol does not contain the useful @@ -142,7 +79,6 @@ function checkTimeSinceLastLongTask() { } module.exports = { - captureJSCallUsage, wrapRuntimeEvalErrorInBrowser, registerPerformanceObserverInPage, checkTimeSinceLastLongTask, diff --git a/lighthouse-core/test/audits/dobetterweb/no-mutation-events-test.js b/lighthouse-core/test/audits/dobetterweb/no-mutation-events-test.js deleted file mode 100644 index 5fcae3bc53f8..000000000000 --- a/lighthouse-core/test/audits/dobetterweb/no-mutation-events-test.js +++ /dev/null @@ -1,70 +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 NoMutationEventsAudit = require('../../../audits/dobetterweb/no-mutation-events.js'); -const assert = require('assert'); - -const fixtureData = require('../../fixtures/page-level-event-listeners.json'); - -const URL = 'https://example.com'; - -/* eslint-env mocha */ - -describe('Page does not use mutation events', () => { - it('passes when mutation events are not used', () => { - const auditResult = NoMutationEventsAudit.audit({ - EventListeners: [], - URL: {finalUrl: URL}, - }); - assert.equal(auditResult.rawValue, true); - assert.equal(auditResult.details.items.length, 0); - }); - - it('fails when mutation events are used on the origin', () => { - const auditResult = NoMutationEventsAudit.audit({ - EventListeners: fixtureData, - URL: {finalUrl: URL}, - }); - assert.equal(auditResult.rawValue, false); - assert.equal(auditResult.details.items.length, 4); - - const itemHeaders = auditResult.details.headings; - assert.deepEqual(Object.keys(itemHeaders).map(key => itemHeaders[key].text), - ['URL', 'Event', 'Line', 'Col', 'Snippet'], - 'table headings are correct and in order'); - }); - - it('fails when listener is missing a url property', () => { - const auditResult = NoMutationEventsAudit.audit({ - EventListeners: fixtureData, - URL: {finalUrl: URL}, - }); - assert.equal(auditResult.rawValue, false); - assert.ok(auditResult.details.items[1].url === undefined); - assert.equal(auditResult.details.items.length, 4); - }); - - it('fails when listener has a bad url property', () => { - const auditResult = NoMutationEventsAudit.audit({ - EventListeners: [ - { - objectName: 'Window', - type: 'DOMNodeInserted', - useCapture: false, - passive: false, - url: 'eval():54:21', - line: 54, - col: 21, - }, - ], - URL: {finalUrl: URL}, - }); - assert.equal(auditResult.rawValue, false); - assert.equal(auditResult.details.items[0].url, 'eval():54:21'); - assert.equal(auditResult.details.items.length, 1); - }); -}); diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index 05e2bfe2eb2a..d456b36b43d4 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -2405,89 +2405,6 @@ ] } }, - "no-mutation-events": { - "id": "no-mutation-events", - "title": "Uses Mutation Events in its own scripts", - "description": "Mutation Events are deprecated and harm performance. Consider using Mutation Observers instead. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/mutation-events).", - "score": 0, - "scoreDisplayMode": "binary", - "rawValue": false, - "warnings": [], - "details": { - "type": "table", - "headings": [ - { - "key": "url", - "itemType": "url", - "text": "URL" - }, - { - "key": "type", - "itemType": "code", - "text": "Event" - }, - { - "key": "line", - "itemType": "text", - "text": "Line" - }, - { - "key": "col", - "itemType": "text", - "text": "Col" - }, - { - "key": "pre", - "itemType": "code", - "text": "Snippet" - } - ], - "items": [ - { - "line": 206, - "col": 50, - "url": "http://localhost:10200/dobetterweb/dbw_tester.html", - "type": "DOMNodeInserted", - "pre": "section#touchmove-section.addEventListener('DOMNodeInserted', function(e) {\n console.log('DOMNodeInserted on element');\n })\n\n" - }, - { - "line": 200, - "col": 61, - "url": "http://localhost:10200/dobetterweb/dbw_tester.html", - "type": "DOMNodeInserted", - "pre": "body.addEventListener('DOMNodeInserted', function(e) {\n console.log('DOMNodeInserted');\n })\n\n" - }, - { - "line": 20, - "col": 56, - "url": "http://localhost:10200/dobetterweb/dbw_tester.js", - "type": "DOMNodeInserted", - "pre": "document.addEventListener('DOMNodeInserted', function(e) {\n console.log('DOMNodeInserted');\n })\n\n" - }, - { - "line": 190, - "col": 56, - "url": "http://localhost:10200/dobetterweb/dbw_tester.html", - "type": "DOMNodeInserted", - "pre": "document.addEventListener('DOMNodeInserted', function(e) {\n console.log('DOMNodeInserted');\n })\n\n" - }, - { - "line": 195, - "col": 55, - "url": "http://localhost:10200/dobetterweb/dbw_tester.html", - "type": "DOMNodeRemoved", - "pre": "document.addEventListener('DOMNodeRemoved', function(e) {\n console.log('DOMNodeRemoved');\n })\n\n" - }, - { - "line": 211, - "col": 54, - "url": "http://localhost:10200/dobetterweb/dbw_tester.html", - "type": "DOMNodeInserted", - "pre": "window.addEventListener('DOMNodeInserted', function(e) {\n console.log('DOMNodeInserted');\n })\n\n" - } - ] - } - }, "no-vulnerable-libraries": { "id": "no-vulnerable-libraries", "title": "Includes front-end JavaScript libraries with known security vulnerabilities", @@ -3362,10 +3279,6 @@ "id": "uses-passive-event-listeners", "weight": 1 }, - { - "id": "no-mutation-events", - "weight": 1 - }, { "id": "no-document-write", "weight": 1