diff --git a/.eslintrc.js b/.eslintrc.js index 020f8ff402c2..0f2def50d210 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,8 +44,6 @@ module.exports = { "valid-jsdoc": 0, "comma-dangle": 0, "arrow-parens": 0, - // Compat: support for rest params is behind a flag for node v5.x - "prefer-rest-params": 0, }, "parserOptions": { "ecmaVersion": 6, diff --git a/lighthouse-cli/test/global-mocha-hooks.js b/lighthouse-cli/test/global-mocha-hooks.js index 4f14eefeff1e..8cdd95ff568c 100644 --- a/lighthouse-cli/test/global-mocha-hooks.js +++ b/lighthouse-cli/test/global-mocha-hooks.js @@ -15,11 +15,11 @@ Object.keys(assert) .filter(key => typeof assert[key] === 'function') .forEach(key => { const _origFn = assert[key]; - assert[key] = function() { + assert[key] = function(...args) { if (currTest) { currTest._assertions++; } - return _origFn.apply(this, arguments); + return _origFn.apply(this, args); }; } ); diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js index 855a5cd95b9e..9465764e9a77 100644 --- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js +++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js @@ -121,7 +121,9 @@ module.exports = [ score: false, extendedInfo: { value: { - length: 5 + results: { + length: 5 + } } } }, @@ -151,7 +153,9 @@ module.exports = [ score: false, extendedInfo: { value: { - length: 2 + results: { + length: 2 + } } } }, diff --git a/lighthouse-core/audits/dobetterweb/uses-optimized-images.js b/lighthouse-core/audits/dobetterweb/uses-optimized-images.js index d20d31ce7669..110254229046 100644 --- a/lighthouse-core/audits/dobetterweb/uses-optimized-images.js +++ b/lighthouse-core/audits/dobetterweb/uses-optimized-images.js @@ -46,7 +46,7 @@ class UsesOptimizedImages extends Audit { 'The following images could have smaller file sizes when compressed with ' + '[WebP](https://developers.google.com/speed/webp/) or JPEG at 80 quality. ' + '[Learn more about image optimization](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/image-optimization).', - requiredArtifacts: ['OptimizedImages'] + requiredArtifacts: ['OptimizedImages', 'networkRecords'] }; } @@ -67,6 +67,18 @@ class UsesOptimizedImages extends Audit { * @return {!AuditResult} */ static audit(artifacts) { + const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS]; + return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => { + return UsesOptimizedImages.audit_(artifacts, networkThroughput); + }); + } + + /** + * @param {!Artifacts} artifacts + * @param {number} networkThroughput + * @return {!AuditResult} + */ + static audit_(artifacts, networkThroughput) { const images = artifacts.OptimizedImages; if (images.rawValue === -1) { @@ -114,7 +126,10 @@ class UsesOptimizedImages extends Audit { let displayValue = ''; if (totalWastedBytes > 1000) { - displayValue = `${Math.round(totalWastedBytes / KB_IN_BYTES)}KB potential savings`; + const totalWastedKb = Math.round(totalWastedBytes / KB_IN_BYTES); + // Only round to nearest 10ms since we're relatively hand-wavy + const totalWastedMs = Math.round(totalWastedBytes / networkThroughput * 100) * 10; + displayValue = `${totalWastedKb}KB (~${totalWastedMs}ms) potential savings`; } let debugString; @@ -134,8 +149,8 @@ class UsesOptimizedImages extends Audit { tableHeadings: { url: 'URL', total: 'Original (KB)', - webpSavings: 'WebP savings', - jpegSavings: 'JPEG savings' + webpSavings: 'WebP Savings (%)', + jpegSavings: 'JPEG Savings (%)', } } } diff --git a/lighthouse-core/audits/dobetterweb/uses-responsive-images.js b/lighthouse-core/audits/dobetterweb/uses-responsive-images.js index bb6804185321..284e5c121436 100644 --- a/lighthouse-core/audits/dobetterweb/uses-responsive-images.js +++ b/lighthouse-core/audits/dobetterweb/uses-responsive-images.js @@ -44,7 +44,7 @@ class UsesResponsiveImages extends Audit { 'Image sizes served should be based on the device display size to save network bytes. ' + 'Learn more about [responsive images](https://developers.google.com/web/fundamentals/design-and-ui/media/images) ' + 'and [client hints](https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints).', - requiredArtifacts: ['ImageUsage', 'ContentWidth'] + requiredArtifacts: ['ImageUsage', 'ContentWidth', 'networkRecords'] }; } @@ -68,20 +68,17 @@ class UsesResponsiveImages extends Audit { return null; } - // TODO(#1517): use an average transfer time for data URI images - const size = image.networkRecord.resourceSize; - const transferTimeInMs = 1000 * (image.networkRecord.endTime - - image.networkRecord.responseReceivedTime); - const wastedBytes = Math.round(size * wastedRatio); - const wastedTime = Math.round(transferTimeInMs * wastedRatio); - const percentSavings = Math.round(100 * wastedRatio); - const label = `${Math.round(size / KB_IN_BYTES)}KB total, ${percentSavings}% potential savings`; + const totalBytes = image.networkRecord.resourceSize; + const wastedBytes = Math.round(totalBytes * wastedRatio); return { wastedBytes, - wastedTime, isWasteful: wastedRatioFullDPR > WASTEFUL_THRESHOLD_AS_RATIO, - result: {url, label}, + result: { + url, + totalKb: Math.round(totalBytes / KB_IN_BYTES) + ' KB', + potentialSavings: Math.round(100 * wastedRatio) + '%' + }, }; } @@ -90,12 +87,23 @@ class UsesResponsiveImages extends Audit { * @return {!AuditResult} */ static audit(artifacts) { + const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS]; + return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => { + return UsesResponsiveImages.audit_(artifacts, networkThroughput); + }); + } + + /** + * @param {!Artifacts} artifacts + * @param {number} networkThroughput + * @return {!AuditResult} + */ + static audit_(artifacts, networkThroughput) { const images = artifacts.ImageUsage; const contentWidth = artifacts.ContentWidth; let debugString; let totalWastedBytes = 0; - let totalWastedTime = 0; let hasWastefulImage = false; const DPR = contentWidth.devicePixelRatio; const results = images.reduce((results, image) => { @@ -112,7 +120,6 @@ class UsesResponsiveImages extends Audit { } hasWastefulImage = hasWastefulImage || processed.isWasteful; - totalWastedTime += processed.wastedTime; totalWastedBytes += processed.wastedBytes; results.push(processed.result); return results; @@ -121,7 +128,9 @@ class UsesResponsiveImages extends Audit { let displayValue; if (results.length) { const totalWastedKB = Math.round(totalWastedBytes / KB_IN_BYTES); - displayValue = `${totalWastedKB}KB (~${totalWastedTime}ms) potential savings`; + // Only round to nearest 10ms since we're relatively hand-wavy + const totalWastedMs = Math.round(totalWastedBytes / networkThroughput * 100) * 10; + displayValue = `${totalWastedKB}KB (~${totalWastedMs}ms) potential savings`; } return UsesResponsiveImages.generateAuditResult({ @@ -129,8 +138,15 @@ class UsesResponsiveImages extends Audit { displayValue, rawValue: !hasWastefulImage, extendedInfo: { - formatter: Formatter.SUPPORTED_FORMATS.URLLIST, - value: results + formatter: Formatter.SUPPORTED_FORMATS.TABLE, + value: { + results, + tableHeadings: { + url: 'URL', + totalKb: 'Original (KB)', + potentialSavings: 'Potential Savings (%)' + } + } } }); } diff --git a/lighthouse-core/audits/unused-css-rules.js b/lighthouse-core/audits/unused-css-rules.js index bb8757c0de2c..9541de5448c2 100644 --- a/lighthouse-core/audits/unused-css-rules.js +++ b/lighthouse-core/audits/unused-css-rules.js @@ -18,7 +18,9 @@ const Audit = require('./audit'); const Formatter = require('../formatters/formatter'); +const URL = require('../lib/url-shim'); +const KB_IN_BYTES = 1024; const PREVIEW_LENGTH = 100; const ALLOWABLE_UNUSED_RULES_RATIO = 0.10; @@ -33,19 +35,27 @@ class UnusedCSSRules extends Audit { description: 'Site does not have more than 10% unused CSS', helpText: 'Remove unused rules from stylesheets to reduce unnecessary ' + 'bytes consumed by network activity. [Learn more](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery)', - requiredArtifacts: ['CSSUsage', 'Styles', 'URL'] + requiredArtifacts: ['CSSUsage', 'Styles', 'URL', 'networkRecords'] }; } /** * @param {!Array.<{header: {styleSheetId: string}}>} styles The output of the Styles gatherer. + * @param {!Array} networkRecords * @return {!Object} A map of styleSheetId to stylesheet information. */ - static indexStylesheetsById(styles) { + static indexStylesheetsById(styles, networkRecords) { + const indexedNetworkRecords = networkRecords + .filter(record => record._resourceType && record._resourceType._name === 'stylesheet') + .reduce((indexed, record) => { + indexed[record.url] = record; + return indexed; + }, {}); return styles.reduce((indexed, stylesheet) => { indexed[stylesheet.header.styleSheetId] = Object.assign({ used: [], unused: [], + networkRecord: indexedNetworkRecords[stylesheet.header.sourceURL], }, stylesheet); return indexed; }, {}); @@ -63,7 +73,7 @@ class UnusedCSSRules extends Audit { rules.forEach(rule => { const stylesheetInfo = indexedStylesheets[rule.styleSheetId]; - if (stylesheetInfo.isDuplicate) { + if (!stylesheetInfo || stylesheetInfo.isDuplicate) { return; } @@ -78,6 +88,42 @@ class UnusedCSSRules extends Audit { return unused; } + /** + * Trims stylesheet content down to the first rule-set definition. + * @param {string} content + * @return {string} + */ + static determineContentPreview(content) { + let preview = content + .slice(0, PREVIEW_LENGTH * 5) + .replace(/( {2,}|\t)+/g, ' ') // remove leading indentation if present + .replace(/\n\s+}/g, '\n}') // completely remove indentation of closing braces + .trim(); // trim the leading whitespace + + if (preview.length > PREVIEW_LENGTH) { + const firstRuleStart = preview.indexOf('{'); + const firstRuleEnd = preview.indexOf('}'); + + if (firstRuleStart === -1 || firstRuleEnd === -1 + || firstRuleStart > firstRuleEnd + || firstRuleStart > PREVIEW_LENGTH) { + // We couldn't determine the first rule-set or it's not within the preview + preview = preview.slice(0, PREVIEW_LENGTH) + '...'; + } else if (firstRuleEnd < PREVIEW_LENGTH) { + // The entire first rule-set fits within the preview + preview = preview.slice(0, firstRuleEnd + 1) + ' ...'; + } else { + // The first rule-set doesn't fit within the preview, just show as many as we can + const lastSemicolonIndex = preview.slice(0, PREVIEW_LENGTH).lastIndexOf(';'); + preview = lastSemicolonIndex < firstRuleStart ? + preview.slice(0, PREVIEW_LENGTH) + '... } ...' : + preview.slice(0, lastSemicolonIndex + 1) + ' ... } ...'; + } + } + + return preview; + } + /** * @param {!Object} stylesheetInfo The stylesheetInfo object. * @param {string} pageUrl The URL of the page, used to identify inline styles. @@ -91,36 +137,30 @@ class UnusedCSSRules extends Audit { return null; } - const percentUsed = Math.round(100 * numUsed / (numUsed + numUnused)); - - let contentPreview = stylesheetInfo.content; - if (contentPreview.length > PREVIEW_LENGTH) { - const firstRuleStart = contentPreview.indexOf('{'); - const firstRuleEnd = contentPreview.indexOf('}'); - if (firstRuleStart === -1 || firstRuleEnd === -1 - || firstRuleStart > firstRuleEnd - || firstRuleStart > PREVIEW_LENGTH) { - contentPreview = contentPreview.slice(0, PREVIEW_LENGTH) + '...'; - } else if (firstRuleEnd < PREVIEW_LENGTH) { - contentPreview = contentPreview.slice(0, firstRuleEnd + 1) + ' ...'; - } else { - const lastSemicolonIndex = contentPreview.slice(0, PREVIEW_LENGTH).lastIndexOf(';'); - contentPreview = lastSemicolonIndex < firstRuleStart ? - contentPreview.slice(0, PREVIEW_LENGTH) + '... } ...' : - contentPreview.slice(0, lastSemicolonIndex + 1) + ' ... } ...'; - } - } - - let code; let url = stylesheetInfo.header.sourceURL; - const label = `${percentUsed}% rules used`; - if (!url || url === pageUrl) { - url = 'inline'; - code = contentPreview.trim(); + const contentPreview = UnusedCSSRules.determineContentPreview(stylesheetInfo.content); + url = '*inline*```' + contentPreview + '```'; + } else { + url = URL.getDisplayName(url); } - return {url, code, label}; + // If we don't know for sure how many bytes this sheet used on the network, + // we can guess it was roughly the size of the content gzipped. + const totalBytes = stylesheetInfo.networkRecord ? + stylesheetInfo.networkRecord.transferSize : + Math.round(stylesheetInfo.content.length / 3); + + const percentUnused = numUnused / (numUsed + numUnused); + const wastedBytes = Math.round(percentUnused * totalBytes); + + return { + url, + numUnused, + wastedBytes, + totalKb: Math.round(totalBytes / KB_IN_BYTES) + ' KB', + potentialSavings: `${Math.round(percentUnused * 100)}%`, + }; } /** @@ -128,9 +168,22 @@ class UnusedCSSRules extends Audit { * @return {!AuditResult} */ static audit(artifacts) { + const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS]; + return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => { + return UnusedCSSRules.audit_(artifacts, networkThroughput); + }); + } + + /** + * @param {!Artifacts} artifacts + * @param {number} networkThroughput + * @return {!AuditResult} + */ + static audit_(artifacts, networkThroughput) { const styles = artifacts.Styles; const usage = artifacts.CSSUsage; const pageUrl = artifacts.URL.finalUrl; + const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS]; if (styles.rawValue === -1) { return UnusedCSSRules.generateAuditResult(styles); @@ -138,27 +191,36 @@ class UnusedCSSRules extends Audit { return UnusedCSSRules.generateAuditResult(usage); } - const indexedSheets = UnusedCSSRules.indexStylesheetsById(styles); + const indexedSheets = UnusedCSSRules.indexStylesheetsById(styles, networkRecords); const unused = UnusedCSSRules.countUnusedRules(usage, indexedSheets); const unusedRatio = (unused / usage.length) || 0; const results = Object.keys(indexedSheets).map(sheetId => { return UnusedCSSRules.mapSheetToResult(indexedSheets[sheetId], pageUrl); }).filter(Boolean); - + const wastedBytes = results.reduce((waste, result) => waste + result.wastedBytes, 0); let displayValue = ''; - if (unused > 1) { - displayValue = `${unused} CSS rules were unused`; - } else if (unused === 1) { - displayValue = `${unused} CSS rule was unused`; + if (unused > 0) { + const wastedKb = Math.round(wastedBytes / KB_IN_BYTES); + // Only round to nearest 10ms since we're relatively hand-wavy + const wastedMs = Math.round(wastedBytes / networkThroughput * 100) * 10; + displayValue = `${wastedKb}KB (~${wastedMs}ms) potential savings`; } return UnusedCSSRules.generateAuditResult({ displayValue, rawValue: unusedRatio < ALLOWABLE_UNUSED_RULES_RATIO, extendedInfo: { - formatter: Formatter.SUPPORTED_FORMATS.URLLIST, - value: results + formatter: Formatter.SUPPORTED_FORMATS.TABLE, + value: { + results, + tableHeadings: { + url: 'URL', + numUnused: 'Unused Rules', + totalKb: 'Original (KB)', + potentialSavings: 'Potential Savings (%)', + } + } } }); } diff --git a/lighthouse-core/formatters/partials/table.html b/lighthouse-core/formatters/partials/table.html index 39687efdaad9..db93b73497dd 100644 --- a/lighthouse-core/formatters/partials/table.html +++ b/lighthouse-core/formatters/partials/table.html @@ -26,10 +26,14 @@ .table_list tr:hover { background-color: #fafafa; } -.table_list code { +.table_list code, .table_list pre { + display: block; white-space: pre; font-family: monospace; } +.table_list em + code, .table_list em + pre { + margin-top: 10px; +} .table_list.multicolumn { display: flex; @@ -69,4 +73,4 @@ {{/if}} -{{/createTable}} \ No newline at end of file +{{/createTable}} diff --git a/lighthouse-core/formatters/table.js b/lighthouse-core/formatters/table.js index 1f20d9fc8f0d..0eef477f198e 100644 --- a/lighthouse-core/formatters/table.js +++ b/lighthouse-core/formatters/table.js @@ -78,13 +78,18 @@ class Table extends Formatter { const rows = results.map(result => { const cols = headingKeys.map(key => { + let value = result[key]; + if (typeof value === 'undefined') { + value = '--'; + } + switch (key) { case 'code': - return '`' + result[key].trim() + '`'; + return '`' + value.trim() + '`'; case 'lineCol': return `${result.line}:${result.col}`; default: - return result[key]; + return String(value); } }); diff --git a/lighthouse-core/gather/computed/network-throughput.js b/lighthouse-core/gather/computed/network-throughput.js new file mode 100644 index 000000000000..79aae1660b1f --- /dev/null +++ b/lighthouse-core/gather/computed/network-throughput.js @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2017 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 ComputedArtifact = require('./computed-artifact'); + +class NetworkThroughput extends ComputedArtifact { + get name() { + return 'NetworkThroughput'; + } + + /** + * Computes the average throughput for the given records in bytes/second. + * Excludes data URI, failed or otherwise incomplete, and cached requests. + * + * @param {?Array} networkRecords + * @return {number} + */ + compute_(networkRecords) { + if (!networkRecords || !networkRecords.length) { + return 0; + } + + let totalBytes = 0; + const timeBoundaries = networkRecords.reduce((boundaries, record) => { + const scheme = record.parsedURL && record.parsedURL.scheme; + if (scheme === 'data' || record.failed || !record.finished || + record.statusCode > 300 || !record.transferSize) { + return boundaries; + } + + totalBytes += record.transferSize; + boundaries.push({time: record.responseReceivedTime, isStart: true}); + boundaries.push({time: record.endTime, isStart: false}); + return boundaries; + }, []).sort((a, b) => a.time - b.time); + + let inflight = 0; + let currentStart = 0; + let totalDuration = 0; + timeBoundaries.forEach(boundary => { + if (boundary.isStart) { + if (inflight === 0) { + currentStart = boundary.time; + } + inflight++; + } else { + inflight--; + if (inflight === 0) { + totalDuration += boundary.time - currentStart; + } + } + }); + + return totalBytes / totalDuration; + } +} + +module.exports = NetworkThroughput; diff --git a/lighthouse-core/gather/connections/cri.js b/lighthouse-core/gather/connections/cri.js index 3019304d3f83..377e981d20b8 100644 --- a/lighthouse-core/gather/connections/cri.js +++ b/lighthouse-core/gather/connections/cri.js @@ -43,6 +43,7 @@ class CriConnection extends Connection { connect() { return this._runJsonCommand('new').then(response => { const url = response.webSocketDebuggerUrl; + this._pageId = response.id; return new Promise((resolve, reject) => { const ws = new WebSocket(url); @@ -74,10 +75,18 @@ class CriConnection extends Connection { }); response.on('end', _ => { if (response.statusCode === 200) { - resolve(JSON.parse(data)); - return; + try { + resolve(JSON.parse(data)); + return; + } catch (e) { + // In the case of 'close' Chromium returns a string rather than JSON: goo.gl/7v27xD + if (data === 'Target is closing') { + return resolve({message: data}); + } + return reject(e); + } } - reject(new Error(`Unable to fetch webSocketDebuggerUrl, status: ${response.statusCode}`)); + reject(new Error(`Protocol JSON API error (${command}), status: ${response.statusCode}`)); }); }); @@ -108,10 +117,12 @@ class CriConnection extends Connection { log.warn('CriConnection', 'disconnect() was called without an established connection.'); return Promise.resolve(); } - this._ws.removeAllListeners(); - this._ws.close(); - this._ws = null; - return Promise.resolve(); + return this._runJsonCommand(`close/${this._pageId}`).then(_ => { + this._ws.removeAllListeners(); + this._ws.close(); + this._ws = null; + this._pageId = null; + }); } /** diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index 57849a7ec9e8..639ee8eb1c79 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -801,11 +801,9 @@ function captureJSCallUsage(funcRef, set) { const originalFunc = funcRef; const originalPrepareStackTrace = __nativeError.prepareStackTrace; - return function() { + return function(...args) { // Note: this function runs in the context of the page that is being audited. - const args = [...arguments]; // callee's arguments. - // 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). @@ -850,7 +848,7 @@ function captureJSCallUsage(funcRef, set) { __nativeError.prepareStackTrace = originalPrepareStackTrace; // eslint-disable-next-line no-invalid-this - return originalFunc.apply(this, arguments); + return originalFunc.apply(this, args); }; } diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index 9730c010b909..5a7c9614c46d 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -104,7 +104,9 @@ class GatherRunner { // We dont need to hold up the reporting for the reload/disconnect, // so we will not return a promise in here. log.log('status', 'Disconnecting from browser...'); - driver.disconnect(); + driver.disconnect().catch(e => { + log.error('gather-runner disconnect', e); + }); } /** diff --git a/lighthouse-core/lib/console-quieter.js b/lighthouse-core/lib/console-quieter.js index 055428ddf589..f7ba967440dd 100644 --- a/lighthouse-core/lib/console-quieter.js +++ b/lighthouse-core/lib/console-quieter.js @@ -26,14 +26,14 @@ class ConsoleQuieter { static mute(opts) { ConsoleQuieter._logs = ConsoleQuieter._logs || []; - console.log = function() { - ConsoleQuieter._logs.push({type: 'log', args: arguments, prefix: opts.prefix}); + console.log = function(...args) { + ConsoleQuieter._logs.push({type: 'log', args, prefix: opts.prefix}); }; - console.warn = function() { - ConsoleQuieter._logs.push({type: 'warn', args: arguments, prefix: opts.prefix}); + console.warn = function(...args) { + ConsoleQuieter._logs.push({type: 'warn', args, prefix: opts.prefix}); }; - console.error = function() { - ConsoleQuieter._logs.push({type: 'error', args: arguments, prefix: opts.prefix}); + console.error = function(...args) { + ConsoleQuieter._logs.push({type: 'error', args, prefix: opts.prefix}); }; } diff --git a/lighthouse-core/lib/log.js b/lighthouse-core/lib/log.js index 62ba8bbd3876..979ff2280180 100644 --- a/lighthouse-core/lib/log.js +++ b/lighthouse-core/lib/log.js @@ -40,19 +40,22 @@ class Emitter extends EventEmitter { * Fires off all status updates. Listen with * `require('lib/log').events.addListener('status', callback)` * @param {string} title + * @param {!Array<*>} argsArray */ - issueStatus(title, args) { + issueStatus(title, argsArray) { if (title === 'status' || title === 'statusEnd') { - this.emit(title, args); + this.emit(title, [title, ...argsArray]); } } /** * Fires off all warnings. Listen with * `require('lib/log').events.addListener('warning', callback)` + * @param {string} title + * @param {!Array<*>} argsArray */ - issueWarning(args) { - this.emit('warning', args); + issueWarning(title, argsArray) { + this.emit('warning', [title, ...argsArray]); } } @@ -62,9 +65,8 @@ const loggingBufferColumns = 25; class Log { static _logToStdErr(title, argsArray) { - const args = [...argsArray]; const log = Log.loggerfn(title); - log(...args); + log(...argsArray); } static loggerfn(title) { @@ -113,23 +115,23 @@ class Log { Log._logToStdErr(`${prefix}:${level || ''}`, [data.method, snippet]); } - static log(title) { - Log.events.issueStatus(title, arguments); - return Log._logToStdErr(title, Array.from(arguments).slice(1)); + static log(title, ...args) { + Log.events.issueStatus(title, args); + return Log._logToStdErr(title, args); } - static warn(title) { - Log.events.issueWarning(arguments); - return Log._logToStdErr(`${title}:warn`, Array.from(arguments).slice(1)); + static warn(title, ...args) { + Log.events.issueWarning(title, args); + return Log._logToStdErr(`${title}:warn`, args); } - static error(title) { - return Log._logToStdErr(`${title}:error`, Array.from(arguments).slice(1)); + static error(title, ...args) { + return Log._logToStdErr(`${title}:error`, args); } - static verbose(title) { - Log.events.issueStatus(title); - return Log._logToStdErr(`${title}:verbose`, Array.from(arguments).slice(1)); + static verbose(title, ...args) { + Log.events.issueStatus(title, args); + return Log._logToStdErr(`${title}:verbose`, args); } /** diff --git a/lighthouse-core/report/report-generator.js b/lighthouse-core/report/report-generator.js index 1a174746f3b0..c54961f937bd 100644 --- a/lighthouse-core/report/report-generator.js +++ b/lighthouse-core/report/report-generator.js @@ -112,10 +112,10 @@ class ReportGenerator { }); // arg1 && arg2 && ... && argn - Handlebars.registerHelper('and', function() { + Handlebars.registerHelper('and', function(...args) { let arg = false; - for (let i = 0, n = arguments.length - 1; i < n; i++) { - arg = arguments[i]; + for (let i = 0, n = args.length - 1; i < n; i++) { + arg = args[i]; if (!arg) { break; } @@ -132,6 +132,7 @@ class ReportGenerator { // XSS, define a renderer that only transforms links and code snippets. // All other markdown ad HTML is ignored. const renderer = new marked.Renderer(); + renderer.em = str => `${str}`; renderer.link = (href, title, text) => { title = title || text; return `${text}`; diff --git a/lighthouse-core/report/templates/report-template.html b/lighthouse-core/report/templates/report-template.html index 973496f6e7cd..c5fef416a597 100644 --- a/lighthouse-core/report/templates/report-template.html +++ b/lighthouse-core/report/templates/report-template.html @@ -127,7 +127,7 @@

{{ aggregation.name }}

{{/if}} {{#if subItem.helpText }} - + {{ sanitize subItem.helpText }} @@ -159,8 +159,8 @@

{{ aggregation.name }}

{{/if}} - {{#if subItem.extendedInfo.value}} - {{> (lookup . 'name') subItem.extendedInfo.value }} + {{~#if subItem.extendedInfo.value~}} + {{~> (lookup . 'name') subItem.extendedInfo.value ~}} {{/if}} {{/each}} diff --git a/lighthouse-core/test/audits/dobetterweb/uses-optimized-images-test.js b/lighthouse-core/test/audits/dobetterweb/uses-optimized-images-test.js index 0a3e8694ec8d..224dc84e378c 100644 --- a/lighthouse-core/test/audits/dobetterweb/uses-optimized-images-test.js +++ b/lighthouse-core/test/audits/dobetterweb/uses-optimized-images-test.js @@ -39,7 +39,7 @@ function generateImage(type, originalSize, webpSize, jpegSize) { describe('Page uses optimized images', () => { it('fails when gatherer returns error', () => { const debugString = 'All image optimizations failed.'; - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: { rawValue: -1, debugString: debugString @@ -50,7 +50,7 @@ describe('Page uses optimized images', () => { }); it('fails when one jpeg image is unoptimized', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: [ generateImage('jpeg', 5000, 4000, 4500), ], @@ -60,12 +60,12 @@ describe('Page uses optimized images', () => { const headings = auditResult.extendedInfo.value.tableHeadings; assert.deepEqual(Object.keys(headings).map(key => headings[key]), - ['URL', 'Original (KB)', 'WebP savings', 'JPEG savings'], + ['URL', 'Original (KB)', 'WebP Savings (%)', 'JPEG Savings (%)'], 'table headings are correct and in order'); }); it('fails when one png image is highly unoptimized', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: [ generateImage('png', 100000, 40000), ], @@ -75,7 +75,7 @@ describe('Page uses optimized images', () => { }); it('fails when images are collectively unoptimized', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: [ generateImage('png', 50000, 30000), generateImage('jpeg', 50000, 30000, 40000), @@ -89,7 +89,7 @@ describe('Page uses optimized images', () => { }); it('passes when all images are sufficiently optimized', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: [ generateImage('png', 50000, 30000), generateImage('jpeg', 50000, 30000, 50001), @@ -104,7 +104,7 @@ describe('Page uses optimized images', () => { it('limits output of data URIs', () => { const image = generateImage('data:png', 50000, 30000); - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: [image], }); @@ -113,7 +113,7 @@ describe('Page uses optimized images', () => { }); it('warns when images have failed', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesOptimizedImagesAudit.audit_({ OptimizedImages: [{failed: true, url: 'http://localhost/image.jpg'}], }); diff --git a/lighthouse-core/test/audits/dobetterweb/uses-responsive-images-test.js b/lighthouse-core/test/audits/dobetterweb/uses-responsive-images-test.js index e8b7c0b8a466..4d8c8a2d0e75 100644 --- a/lighthouse-core/test/audits/dobetterweb/uses-responsive-images-test.js +++ b/lighthouse-core/test/audits/dobetterweb/uses-responsive-images-test.js @@ -15,7 +15,7 @@ */ 'use strict'; -const UsesOptimizedImagesAudit = require('../../../audits/dobetterweb/uses-responsive-images.js'); +const UsesResponsiveImagesAudit = require('../../../audits/dobetterweb/uses-responsive-images.js'); const assert = require('assert'); /* eslint-env mocha */ @@ -46,7 +46,7 @@ function generateImage(clientSize, naturalSize, networkRecord, src) { describe('Page uses responsive images', () => { it('fails when an image is much larger than displayed size', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesResponsiveImagesAudit.audit_({ ContentWidth: {devicePixelRatio: 1}, ImageUsage: [ generateImage( @@ -63,12 +63,12 @@ describe('Page uses responsive images', () => { }); assert.equal(auditResult.rawValue, false); - assert.equal(auditResult.extendedInfo.value.length, 1); + assert.equal(auditResult.extendedInfo.value.results.length, 1); assert.ok(/45KB/.test(auditResult.displayValue), 'computes total kb'); }); it('fails when an image is much larger than DPR displayed size', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesResponsiveImagesAudit.audit_({ ContentWidth: {devicePixelRatio: 2}, ImageUsage: [ generateImage( @@ -80,12 +80,12 @@ describe('Page uses responsive images', () => { }); assert.equal(auditResult.rawValue, false); - assert.equal(auditResult.extendedInfo.value.length, 1); + assert.equal(auditResult.extendedInfo.value.results.length, 1); assert.ok(/80KB/.test(auditResult.displayValue), 'compute total kb'); }); it('handles images without network record', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesResponsiveImagesAudit.audit_({ ContentWidth: {devicePixelRatio: 2}, ImageUsage: [ generateImage( @@ -97,11 +97,11 @@ describe('Page uses responsive images', () => { }); assert.equal(auditResult.rawValue, true); - assert.equal(auditResult.extendedInfo.value.length, 0); + assert.equal(auditResult.extendedInfo.value.results.length, 0); }); it('passes when all images are not wasteful', () => { - const auditResult = UsesOptimizedImagesAudit.audit({ + const auditResult = UsesResponsiveImagesAudit.audit_({ ContentWidth: {devicePixelRatio: 2}, ImageUsage: [ generateImage( @@ -124,6 +124,6 @@ describe('Page uses responsive images', () => { }); assert.equal(auditResult.rawValue, true); - assert.equal(auditResult.extendedInfo.value.length, 2); + assert.equal(auditResult.extendedInfo.value.results.length, 2); }); }); diff --git a/lighthouse-core/test/audits/unused-css-rules-test.js b/lighthouse-core/test/audits/unused-css-rules-test.js index 19c2b58589cf..36e406e2d85d 100644 --- a/lighthouse-core/test/audits/unused-css-rules-test.js +++ b/lighthouse-core/test/audits/unused-css-rules-test.js @@ -21,46 +21,22 @@ const assert = require('assert'); /* eslint-env mocha */ describe('Best Practices: unused css rules audit', () => { - describe('#mapSheetToResult', () => { - let baseSheet; - - function map(overrides, url) { - url = url || 'the_page'; - return UnusedCSSAudit.mapSheetToResult(Object.assign(baseSheet, overrides), url); + function generate(content, length) { + const arr = []; + for (let i = 0; i < length; i++) { + arr.push(content); } + return arr.join(''); + } - function generate(content, length) { - const arr = []; - for (let i = 0; i < length; i++) { - arr.push(content); - } - return arr.join(''); + describe('#determineContentPreview', () => { + function assertLinesContained(actual, expected) { + expected.split('\n').forEach(line => { + assert.ok(actual.indexOf(line.trim()) >= 0, `${line} is found in preview`); + }); } - beforeEach(() => { - baseSheet = { - header: {sourceURL: ''}, - content: 'dummy', - used: [{dummy: 1}], - unused: [], - }; - }); - - it('correctly computes percentUsed', () => { - assert.ok(/0%/.test(map({used: [], unused: [1, 2]}).label)); - assert.ok(/50%/.test(map({used: [1, 2], unused: [1, 2]}).label)); - assert.ok(/100%/.test(map({used: [1, 2], unused: []}).label)); - }); - - it('correctly computes url', () => { - assert.equal(map({header: {sourceURL: ''}}).url, 'inline'); - assert.equal(map({header: {sourceURL: 'page'}}, 'page').url, 'inline'); - assert.equal(map({header: {sourceURL: 'foobar'}}).url, 'foobar'); - }); - - it('does not give content preview when url is present', () => { - assert.ok(!map({header: {sourceURL: 'foobar'}}).code); - }); + const preview = UnusedCSSAudit.determineContentPreview; it('correctly computes short content preview', () => { const shortContent = ` @@ -69,7 +45,7 @@ describe('Best Practices: unused css rules audit', () => { } `.trim(); - assert.equal(map({content: shortContent}).code, shortContent); + assertLinesContained(preview(shortContent), shortContent); }); it('correctly computes long content preview', () => { @@ -83,7 +59,7 @@ describe('Best Practices: unused css rules audit', () => { } `.trim(); - assert.equal(map({content: longContent}).code, ` + assertLinesContained(preview(longContent), ` body { color: white; } ... @@ -99,7 +75,7 @@ describe('Best Practices: unused css rules audit', () => { } `.trim(); - assert.equal(map({content: longContent}).code, ` + assertLinesContained(preview(longContent), ` body { color: white; font-size: 20px; ... } ... @@ -113,13 +89,62 @@ describe('Best Practices: unused css rules audit', () => { */ `.trim(); - assert.ok(/aaa\.\.\.$/.test(map({content: longContent}).code)); + assert.ok(/aaa\.\.\./.test(preview(longContent))); + }); + }); + + describe('#mapSheetToResult', () => { + let baseSheet; + const baseUrl = 'http://g.co/'; + + function map(overrides, url) { + if (overrides.header && overrides.header.sourceURL) { + overrides.header.sourceURL = baseUrl + overrides.header.sourceURL; + } + url = url || baseUrl; + return UnusedCSSAudit.mapSheetToResult(Object.assign(baseSheet, overrides), url); + } + + beforeEach(() => { + baseSheet = { + header: {sourceURL: baseUrl}, + content: 'dummy', + used: [{dummy: 1}], + unused: [], + }; + }); + + it('correctly computes potentialSavings', () => { + assert.ok(map({used: [], unused: [1, 2]}).potentialSavings, '100%'); + assert.ok(map({used: [1, 2], unused: [1, 2]}).potentialSavings, '50%'); + assert.ok(map({used: [1, 2], unused: []}).potentialSavings, '0%'); + }); + + it('correctly computes url', () => { + assert.equal(map({header: {sourceURL: ''}}).url, '*inline*```dummy```'); + assert.equal(map({header: {sourceURL: 'a'}}, 'http://g.co/a').url, '*inline*```dummy```'); + assert.equal(map({header: {sourceURL: 'foobar'}}).url, '/foobar'); + }); + + it('does not give content preview when url is present', () => { + assert.ok(!/dummy/.test(map({header: {sourceURL: 'foobar'}}).url)); }); }); describe('#audit', () => { + const networkRecords = { + defaultPass: [ + { + url: 'file://a.css', + transferSize: 10 * 1024, + _resourceType: {_name: 'stylesheet'} + }, + ] + }; + it('fails when gatherers failed', () => { - const result = UnusedCSSAudit.audit({ + const result = UnusedCSSAudit.audit_({ + networkRecords, URL: {finalUrl: ''}, CSSUsage: {rawValue: -1, debugString: 'It errored'}, Styles: [] @@ -129,8 +154,20 @@ describe('Best Practices: unused css rules audit', () => { assert.equal(result.rawValue, -1); }); + it('ignores missing stylesheets', () => { + const result = UnusedCSSAudit.audit_({ + networkRecords, + URL: {finalUrl: ''}, + CSSUsage: [{styleSheetId: 'a', used: false}], + Styles: [] + }); + + assert.equal(result.rawValue, true); + }); + it('passes when rules are used', () => { - const result = UnusedCSSAudit.audit({ + const result = UnusedCSSAudit.audit_({ + networkRecords, URL: {finalUrl: ''}, CSSUsage: [ {styleSheetId: 'a', used: true}, @@ -139,11 +176,11 @@ describe('Best Practices: unused css rules audit', () => { ], Styles: [ { - header: {styleSheetId: 'a', sourceURL: 'a.css'}, + header: {styleSheetId: 'a', sourceURL: 'file://a.css'}, content: '.my.selector {color: #ccc;}\n a {color: #fff}' }, { - header: {styleSheetId: 'b', sourceURL: 'b.css'}, + header: {styleSheetId: 'b', sourceURL: 'file://b.css'}, content: '.my.favorite.selector { rule: content; }' } ] @@ -151,11 +188,16 @@ describe('Best Practices: unused css rules audit', () => { assert.ok(!result.displayValue); assert.equal(result.rawValue, true); - assert.equal(result.extendedInfo.value.length, 2); + assert.equal(result.extendedInfo.value.results.length, 2); + assert.equal(result.extendedInfo.value.results[0].totalKb, '10 KB'); + assert.equal(result.extendedInfo.value.results[1].totalKb, '0 KB'); + assert.equal(result.extendedInfo.value.results[0].potentialSavings, '0%'); + assert.equal(result.extendedInfo.value.results[1].potentialSavings, '0%'); }); it('fails when rules are unused', () => { - const result = UnusedCSSAudit.audit({ + const result = UnusedCSSAudit.audit_({ + networkRecords, URL: {finalUrl: ''}, CSSUsage: [ {styleSheetId: 'a', used: true}, @@ -167,12 +209,12 @@ describe('Best Practices: unused css rules audit', () => { ], Styles: [ { - header: {styleSheetId: 'a', sourceURL: 'a.css'}, + header: {styleSheetId: 'a', sourceURL: 'file://a.css'}, content: '.my.selector {color: #ccc;}\n a {color: #fff}' }, { - header: {styleSheetId: 'b', sourceURL: 'b.css'}, - content: '.my.favorite.selector { rule: content; }' + header: {styleSheetId: 'b', sourceURL: 'file://b.css'}, + content: `.my.favorite.selector { ${generate('rule: a; ', 1000)}; }` }, { header: {styleSheetId: 'c', sourceURL: ''}, @@ -183,11 +225,18 @@ describe('Best Practices: unused css rules audit', () => { assert.ok(result.displayValue); assert.equal(result.rawValue, false); - assert.equal(result.extendedInfo.value.length, 3); + assert.equal(result.extendedInfo.value.results.length, 3); + assert.equal(result.extendedInfo.value.results[0].totalKb, '10 KB'); + assert.equal(result.extendedInfo.value.results[1].totalKb, '3 KB'); + assert.equal(result.extendedInfo.value.results[2].totalKb, '0 KB'); + assert.equal(result.extendedInfo.value.results[0].potentialSavings, '67%'); + assert.equal(result.extendedInfo.value.results[1].potentialSavings, '50%'); + assert.equal(result.extendedInfo.value.results[2].potentialSavings, '100%'); }); it('does not include duplicate sheets', () => { - const result = UnusedCSSAudit.audit({ + const result = UnusedCSSAudit.audit_({ + networkRecords, URL: {finalUrl: ''}, CSSUsage: [ {styleSheetId: 'a', used: true}, @@ -196,12 +245,12 @@ describe('Best Practices: unused css rules audit', () => { ], Styles: [ { - header: {styleSheetId: 'a', sourceURL: 'a.css'}, + header: {styleSheetId: 'a', sourceURL: 'file://a.css'}, content: '.my.selector {color: #ccc;}\n a {color: #fff}' }, { isDuplicate: true, - header: {styleSheetId: 'b', sourceURL: 'b.css'}, + header: {styleSheetId: 'b', sourceURL: 'file://b.css'}, content: 'a.other {color: #fff}' }, ] @@ -209,11 +258,12 @@ describe('Best Practices: unused css rules audit', () => { assert.ok(!result.displayValue); assert.equal(result.rawValue, true); - assert.equal(result.extendedInfo.value.length, 1); + assert.equal(result.extendedInfo.value.results.length, 1); }); it('does not include empty sheets', () => { - const result = UnusedCSSAudit.audit({ + const result = UnusedCSSAudit.audit_({ + networkRecords, URL: {finalUrl: ''}, CSSUsage: [ {styleSheetId: 'a', used: true}, @@ -222,11 +272,11 @@ describe('Best Practices: unused css rules audit', () => { ], Styles: [ { - header: {styleSheetId: 'a', sourceURL: 'a.css'}, + header: {styleSheetId: 'a', sourceURL: 'file://a.css'}, content: '.my.selector {color: #ccc;}\n a {color: #fff}' }, { - header: {styleSheetId: 'b', sourceURL: 'b.css'}, + header: {styleSheetId: 'b', sourceURL: 'file://b.css'}, content: '.my.favorite.selector { rule: content; }' }, { @@ -246,7 +296,7 @@ describe('Best Practices: unused css rules audit', () => { assert.ok(!result.displayValue); assert.equal(result.rawValue, true); - assert.equal(result.extendedInfo.value.length, 2); + assert.equal(result.extendedInfo.value.results.length, 2); }); }); }); diff --git a/lighthouse-core/test/formatter/table-formatter-test.js b/lighthouse-core/test/formatter/table-formatter-test.js index 431774eea829..32f35f6d7c51 100644 --- a/lighthouse-core/test/formatter/table-formatter-test.js +++ b/lighthouse-core/test/formatter/table-formatter-test.js @@ -75,4 +75,34 @@ describe('TableFormatter', () => { const output2 = template(extendedInfoShort).split('\n').join(''); assert.ok(!output2.match('multicolumn"'), 'does not add multicolumn class for small tables'); }); + + it('handles missing values', () => { + const pretty = TableFormatter.getFormatter('pretty'); + assert.equal(pretty({ + tableHeadings: {name: 'Name', value: 'Value'}, + results: [ + {name: 'thing1', value: 'foo'}, + {name: 'thing2'}, + {value: 'bar'}, + ] + }), [ + ' thing1 foo \n', + ' thing2 -- \n', + ' -- bar \n', + ].join('')); + }); + + it('handles non-string values', () => { + const pretty = TableFormatter.getFormatter('pretty'); + assert.equal(pretty({ + tableHeadings: {name: 'Name', value: 'Value'}, + results: [ + {name: 'thing1', value: 5}, + {name: 'thing2', value: false}, + ] + }), [ + ' thing1 5 \n', + ' thing2 false \n', + ].join('')); + }); }); diff --git a/lighthouse-core/test/gather/computed/network-throughput-test.js b/lighthouse-core/test/gather/computed/network-throughput-test.js new file mode 100644 index 000000000000..8031d167a416 --- /dev/null +++ b/lighthouse-core/test/gather/computed/network-throughput-test.js @@ -0,0 +1,101 @@ +/** + * Copyright 2017 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 NetworkThroughput = require('../../../gather/computed/network-throughput'); +const assert = require('assert'); + +describe('NetworkThroughput', () => { + const compute = new NetworkThroughput().compute_; + function createRecord(responseReceivedTime, endTime, extras) { + return Object.assign({ + responseReceivedTime, + endTime, + transferSize: 1000, + finished: true, + failed: false, + statusCode: 200, + url: 'https://google.com/logo.png', + parsedURL: {isValid: true, scheme: 'https'} + }, extras); + } + + it('should compute correctly for a basic waterfall', () => { + const result = compute([ + createRecord(0, 1), + createRecord(1, 2), + createRecord(2, 6), + ]); + + assert.equal(result, 500); + }); + + it('should compute correctly for concurrent requests', () => { + const result = compute([ + createRecord(0, 1), + createRecord(0.5, 1), + ]); + + assert.equal(result, 2000); + }); + + it('should compute correctly for gaps', () => { + const result = compute([ + createRecord(0, 1), + createRecord(3, 4), + ]); + + assert.equal(result, 1000); + }); + + it('should compute correctly for partially overlapping requests', () => { + const result = compute([ + createRecord(0, 1), + createRecord(0.5, 1.5), + createRecord(1.25, 3), + createRecord(1.4, 4), + createRecord(5, 9) + ]); + + assert.equal(result, 625); + }); + + it('should exclude failed records', () => { + const extras = {failed: true}; + const result = compute([createRecord(0, 2), createRecord(3, 4, extras)]); + assert.equal(result, 500); + }); + + it('should exclude cached records', () => { + const extras = {statusCode: 304}; + const result = compute([createRecord(0, 2), createRecord(3, 4, extras)]); + assert.equal(result, 500); + }); + + it('should exclude unfinished records', () => { + const extras = {finished: false}; + const result = compute([createRecord(0, 2), createRecord(3, 4, extras)]); + assert.equal(result, 500); + }); + + it('should exclude data URIs', () => { + const extras = {parsedURL: {scheme: 'data'}}; + const result = compute([createRecord(0, 2), createRecord(3, 4, extras)]); + assert.equal(result, 500); + }); +}); diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js index 7801f28886a2..a4d05cb2afdd 100644 --- a/lighthouse-core/test/gather/fake-driver.js +++ b/lighthouse-core/test/gather/fake-driver.js @@ -22,7 +22,9 @@ module.exports = { connect() { return Promise.resolve(); }, - disconnect() {}, + disconnect() { + return Promise.resolve(); + }, gotoURL() { return Promise.resolve(); }, diff --git a/lighthouse-core/test/global-mocha-hooks.js b/lighthouse-core/test/global-mocha-hooks.js index 371fc60bfefc..ea8433af5ea0 100644 --- a/lighthouse-core/test/global-mocha-hooks.js +++ b/lighthouse-core/test/global-mocha-hooks.js @@ -16,11 +16,11 @@ Object.keys(assert) .filter(key => typeof assert[key] === 'function') .forEach(key => { const _origFn = assert[key]; - assert[key] = function() { + assert[key] = function(...args) { if (currTest) { currTest._assertions++; } - return _origFn.apply(this, arguments); + return _origFn.apply(this, args); }; } ); diff --git a/readme.md b/readme.md index 6031d3bd45c6..3e8fb84231d7 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ lighthouse --help ## Lighthouse Viewer -If you run Lighthouse with the `--output=json` flag, it will generate a json dump of the run. You can view this report online by visiting and dragging the file onto the app. Reports can also be shared by clicking the share icon in the top right corner and signing in to GitHub. +If you run Lighthouse with the `--output=json` flag, it will generate a json dump of the run. You can view this report online by visiting and dragging the file onto the app. Reports can also be shared by clicking the share icon in the top right corner and signing in to GitHub. Note: shared reports are stashed as a secret Gist in GitHub, under your account.