Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

core(tsc): add type checking to remaining (dbw) gatherers #5005

Merged
merged 3 commits into from
Apr 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 74 additions & 57 deletions lighthouse-core/gather/gatherers/dobetterweb/all-event-listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,63 @@
'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 {
listenForScriptParsedEvents() {
this._listener = script => {
this._parsedScripts.set(script.scriptId, script);
/**
* @param {Driver} driver
*/
async listenForScriptParsedEvents(driver) {
/** @type {Map<string, LH.Crdp.Debugger.ScriptParsedEvent>} */
const parsedScripts = new Map();
/** @param {LH.Crdp.Debugger.ScriptParsedEvent} script */
const scriptListener = script => {
parsedScripts.set(script.scriptId, script);
};
this.driver.on('Debugger.scriptParsed', this._listener);
return this.driver.sendCommand('Debugger.enable');
}

unlistenForScriptParsedEvents() {
this.driver.off('Debugger.scriptParsed', this._listener);
return this.driver.sendCommand('Debugger.disable');
// 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 and object ('document', 'window').
* @return {!Promise<!Array<{listeners: !Array, tagName: string}>>}
* string of an object ('document', 'window').
* @return {Promise<{listeners: Array<LH.Crdp.DOMDebugger.EventListener>, tagName: string}>}
* @private
*/
_listEventListeners(nodeIdOrObject) {
_listEventListeners(driver, nodeIdOrObject) {
let promise;

if (typeof nodeIdOrObject === 'string') {
promise = this.driver.sendCommand('Runtime.evaluate', {
promise = driver.sendCommand('Runtime.evaluate', {
expression: nodeIdOrObject,
objectGroup: 'event-listeners-gatherer', // populates event handler info.
});
}).then(result => result.result);
} else {
promise = this.driver.sendCommand('DOM.resolveNode', {
promise = driver.sendCommand('DOM.resolveNode', {
nodeId: nodeIdOrObject,
objectGroup: 'event-listeners-gatherer', // populates event handler info.
});
}).then(result => result.object);
}

return promise.then(result => {
const obj = result.object || result.result;
return this.driver.sendCommand('DOMDebugger.getEventListeners', {
objectId: obj.objectId,
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: obj.description};
return {listeners: results.listeners, tagName: description};
});
});
}
Expand All @@ -61,31 +76,35 @@ class EventListeners extends Gatherer {
* 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 {string} nodeId The node to look for attached event listeners.
* @return {!Promise<!Array<!Object>>} List of event listeners attached to
* @param {Driver} driver
* @param {Map<string, LH.Crdp.Debugger.ScriptParsedEvent>} parsedScripts
* @param {string|number} nodeId The node to look for attached event listeners.
* @return {Promise<LH.Artifacts['EventListeners']>} List of event listeners attached to
* the node.
*/
getEventListeners(nodeId) {
getEventListeners(driver, parsedScripts, nodeId) {
/** @type {LH.Artifacts['EventListeners']} */
const matchedListeners = [];

return this._listEventListeners(nodeId).then(results => {
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 = this._parsedScripts.get(listener.scriptId);
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.
const combo = Object.assign(listener, script);
combo.objectName = results.tagName;

// Note: line/col numbers are zero-index. Add one to each so we have
// actual file line/col numbers.
combo.line = combo.lineNumber + 1;
combo.col = combo.columnNumber + 1;

matchedListeners.push(combo);
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,
});
}
});

Expand All @@ -95,35 +114,33 @@ class EventListeners extends Gatherer {

/**
* Aggregates the event listeners used on each element into a single list.
* @param {!Array<!Element>} nodes List of elements to fetch event listeners for.
* @return {!Promise<!Array<!Object>>} Resolves to a list of all the event
* @param {Driver} driver
* @param {Map<string, LH.Crdp.Debugger.ScriptParsedEvent>} parsedScripts
* @param {Array<string|number>} nodeIds List of objects or nodeIds to fetch event listeners for.
* @return {Promise<LH.Artifacts['EventListeners']>} Resolves to a list of all the event
* listeners found across the elements.
*/
collectListeners(nodes) {
collectListeners(driver, parsedScripts, nodeIds) {
// Gather event listeners from each node in parallel.
return Promise.all(nodes.map(node => {
return this.getEventListeners(node.element ? node.element.nodeId : node);
})).then(nestedListeners => [].concat(...nestedListeners));
return Promise.all(nodeIds.map(node => this.getEventListeners(driver, parsedScripts, node)))
.then(nestedListeners => nestedListeners.reduce((prev, curr) => prev.concat(curr)));
}

/**
* @param {!Object} options
* @return {!Promise<!Array<!Object>>}
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['EventListeners']>}
*/
afterPass(options) {
this.driver = options.driver;
this._parsedScripts = new Map();
return options.driver.sendCommand('DOM.enable')
.then(() => this.listenForScriptParsedEvents())
.then(() => this.unlistenForScriptParsedEvents())
.then(() => options.driver.getElementsInDocument())
.then(nodes => {
nodes.push('document', 'window');
return this.collectListeners(nodes);
}).then(listeners => {
return options.driver.sendCommand('DOM.disable')
.then(() => listeners);
});
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const DOMHelpers = require('../../../lib/dom-helpers.js');

class AnchorsWithNoRelNoopener extends Gatherer {
/**
* @param {!Object} options
* @return {!Promise<!Array<{href: string, rel: string, target: string}>>}
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['AnchorsWithNoRelNoopener']>}
*/
afterPass(options) {
afterPass(passContext) {
const expression = `(function() {
${DOMHelpers.getElementsInDocumentFnString}; // define function on page
const selector = 'a[target="_blank"]:not([rel~="noopener"]):not([rel~="noreferrer"])';
Expand All @@ -25,7 +25,7 @@ class AnchorsWithNoRelNoopener extends Gatherer {
}));
})()`;

return options.driver.evaluateAsync(expression);
return passContext.driver.evaluateAsync(expression);
}
}

Expand Down
8 changes: 4 additions & 4 deletions lighthouse-core/gather/gatherers/dobetterweb/appcache.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ class AppCacheManifest extends Gatherer {
/**
* Retrurns the value of the html element's manifest attribute or null if it
* is not defined.
* @param {!Object} options
* @return {!Promise<?string>}
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['AppCacheManifest']>}
*/
afterPass(options) {
const driver = options.driver;
afterPass(passContext) {
const driver = passContext.driver;

return driver.querySelector('html')
.then(node => node && node.getAttribute('manifest'));
Expand Down
41 changes: 23 additions & 18 deletions lighthouse-core/gather/gatherers/dobetterweb/domstats.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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.
*/

// @ts-nocheck
/**
* @fileoverview Gathers stats about the max height and width of the DOM tree
* and total number of nodes used on the page.
Expand All @@ -17,20 +17,21 @@ const Gatherer = require('../gatherer');

/**
* Gets the opening tag text of the given node.
* @param {!Node}
* @return {string}
* @param {Element} element
* @return {?string}
*/
function getOuterHTMLSnippet(node) {
/* istanbul ignore next */
function getOuterHTMLSnippet(element) {
const reOpeningTag = /^.*?>/;
const match = node.outerHTML.match(reOpeningTag);
const match = element.outerHTML.match(reOpeningTag);
return match && match[0];
}

/**
* Constructs a pretty label from element's selectors. For example, given
* <div id="myid" class="myclass">, returns 'div#myid.myclass'.
* @param {!HTMLElement} element
* @return {!string}
* @param {Element} element
* @return {string}
*/
/* istanbul ignore next */
function createSelectorsLabel(element) {
Expand All @@ -54,8 +55,8 @@ function createSelectorsLabel(element) {
}

/**
* @param {!HTMLElement} element
* @return {!Array<string>}
* @param {Node} element
* @return {Array<string>}
*/
/* istanbul ignore next */
function elementPathInDOM(element) {
Expand Down Expand Up @@ -89,9 +90,9 @@ function elementPathInDOM(element) {

/**
* Calculates the maximum tree depth of the DOM.
* @param {!HTMLElement} element Root of the tree to look in.
* @param {HTMLElement} element Root of the tree to look in.
* @param {boolean=} deep True to include shadow roots. Defaults to true.
* @return {!number}
* @return {LH.Artifacts.DOMStats}
*/
/* istanbul ignore next */
function getDOMStats(element, deep=true) {
Expand All @@ -100,6 +101,10 @@ function getDOMStats(element, deep=true) {
let maxWidth = 0;
let parentWithMostChildren = null;

/**
* @param {Element} element
* @param {number} depth
*/
const _calcDOMWidthAndHeight = function(element, depth=1) {
if (depth > maxDepth) {
deepestNode = element;
Expand Down Expand Up @@ -141,21 +146,21 @@ function getDOMStats(element, deep=true) {

class DOMStats extends Gatherer {
/**
* @param {!Object} options
* @return {!Promise<!Array<!Object>>}
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['DOMStats']>}
*/
afterPass(options) {
afterPass(passContext) {
const expression = `(function() {
${getOuterHTMLSnippet.toString()};
${createSelectorsLabel.toString()};
${elementPathInDOM.toString()};
return (${getDOMStats.toString()}(document.documentElement));
})()`;
return options.driver.sendCommand('DOM.enable')
.then(() => options.driver.evaluateAsync(expression, {useIsolation: true}))
.then(results => options.driver.getElementsInDocument().then(allNodes => {
return passContext.driver.sendCommand('DOM.enable')
.then(() => passContext.driver.evaluateAsync(expression, {useIsolation: true}))
.then(results => passContext.driver.getElementsInDocument().then(allNodes => {
results.totalDOMNodes = allNodes.length;
return options.driver.sendCommand('DOM.disable').then(() => results);
return passContext.driver.sendCommand('DOM.disable').then(() => results);
}));
}
}
Expand Down
13 changes: 6 additions & 7 deletions lighthouse-core/gather/gatherers/dobetterweb/js-libraries.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ const libDetectorSource = fs.readFileSync(

/**
* Obtains a list of detected JS libraries and their versions.
* @return {!Array<!{name: string, version: string, npmPkgName: string}>}
*/
/* eslint-disable camelcase */
/* istanbul ignore next */
function detectLibraries() {
/** @type {LH.Artifacts['JSLibraries']} */
const libraries = [];

// d41d8cd98f00b204e9800998ecf8427e_ is a consistent prefix used by the detect libraries
// see https://github.com/HTTPArchive/httparchive/issues/77#issuecomment-291320900
// @ts-ignore - injected libDetectorSource var
Object.entries(d41d8cd98f00b204e9800998ecf8427e_LibraryDetectorTests).forEach(([name, lib]) => {
try {
const result = lib.test(window);
Expand All @@ -44,20 +44,19 @@ function detectLibraries() {

return libraries;
}
/* eslint-enable camelcase */

class JSLibraries extends Gatherer {
/**
* @param {!Object} options
* @return {!Promise<!Array<!Object>>}
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Artifacts['JSLibraries']>}
*/
afterPass(options) {
afterPass(passContext) {
const expression = `(function () {
${libDetectorSource};
return (${detectLibraries.toString()}());
})()`;

return options.driver.evaluateAsync(expression);
return passContext.driver.evaluateAsync(expression);
}
}

Expand Down
Loading