Commit a745fe44 authored by Paul Irish's avatar Paul Irish Committed by Commit Bot

DevTools: [Audits] Roll Lighthouse to v3.0-beta

Bug: 772558, 846211
Change-Id: I4f74ea0d4a84908d2e7c4ccfe7035ee421bdf023
Reviewed-on: https://chromium-review.googlesource.com/1070532
Commit-Queue: Paul Irish <paulirish@chromium.org>
Reviewed-by: default avatarPavel Feldman <pfeldman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#561412}
parent cb0ab8d4
...@@ -2343,6 +2343,9 @@ crbug.com/467477 virtual/disable-spv175/fast/multicol/vertical-rl/nested-columns ...@@ -2343,6 +2343,9 @@ crbug.com/467477 virtual/disable-spv175/fast/multicol/vertical-rl/nested-columns
crbug.com/674225 [ Mac ] fast/replaced/input-radio-height-inside-auto-container.html [ Failure ] crbug.com/674225 [ Mac ] fast/replaced/input-radio-height-inside-auto-container.html [ Failure ]
crbug.com/846211 [ Linux ] http/tests/devtools/audits2/audits2-limited-run.js [ Skip ]
crbug.com/846211 [ Linux ] http/tests/devtools/audits2/audits2-successful-run.js [ Skip ]
crbug.com/400841 media/video-canvas-draw.html [ Failure ] crbug.com/400841 media/video-canvas-draw.html [ Failure ]
crbug.com/400841 virtual/video-surface-layer/media/video-canvas-draw.html [ Failure ] crbug.com/400841 virtual/video-surface-layer/media/video-canvas-draw.html [ Failure ]
crbug.com/400829 media/video-object-fit.html [ Failure ] crbug.com/400829 media/video-object-fit.html [ Failure ]
......
...@@ -12,20 +12,23 @@ Run audits: enabled visible ...@@ -12,20 +12,23 @@ Run audits: enabled visible
=============== Lighthouse Results =============== =============== Lighthouse Results ===============
bootup-time: true bootup-time: true
consistently-interactive: false
critical-request-chains: true critical-request-chains: true
dom-size: true dom-size: true
estimated-input-latency: false efficient-animated-content: false
first-interactive: false estimated-input-latency: true
first-meaningful-paint: false first-contentful-paint: true
first-cpu-idle: true
first-meaningful-paint: true
font-display: true font-display: true
link-blocking-first-paint: false interactive: true
mainthread-work-breakdown: true mainthread-work-breakdown: true
metrics: false
network-requests: true
offscreen-images: false offscreen-images: false
redirects: false redirects: false
render-blocking-resources: false
screenshot-thumbnails: false screenshot-thumbnails: false
script-blocking-first-paint: false speed-index: false
speed-index-metric: false
time-to-first-byte: true time-to-first-byte: true
total-byte-weight: true total-byte-weight: true
unminified-css: false unminified-css: false
...@@ -34,8 +37,9 @@ unused-css-rules: false ...@@ -34,8 +37,9 @@ unused-css-rules: false
user-timings: true user-timings: true
uses-long-cache-ttl: false uses-long-cache-ttl: false
uses-optimized-images: false uses-optimized-images: false
uses-rel-preconnect: false
uses-rel-preload: false uses-rel-preload: false
uses-request-compression: false
uses-responsive-images: false uses-responsive-images: false
uses-text-compression: false
uses-webp-images: false uses-webp-images: false
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
Object.keys(results.audits).sort().forEach(auditName => { Object.keys(results.audits).sort().forEach(auditName => {
var audit = results.audits[auditName]; var audit = results.audits[auditName];
TestRunner.addResult(`${audit.name}: ${Boolean(audit.rawValue)}`); TestRunner.addResult(`${audit.id}: ${Boolean(audit.rawValue)}`);
}); });
TestRunner.completeTest(); TestRunner.completeTest();
......
...@@ -16,7 +16,6 @@ Initializing… ...@@ -16,7 +16,6 @@ Initializing…
Loading page & waiting for onload Loading page & waiting for onload
Retrieving trace Retrieving trace
Retrieving devtoolsLog and network records Retrieving devtoolsLog and network records
Retrieving: URL
Retrieving: Scripts Retrieving: Scripts
Retrieving: CSSUsage Retrieving: CSSUsage
Retrieving: Viewport Retrieving: Viewport
...@@ -44,6 +43,7 @@ Retrieving: MetaRobots ...@@ -44,6 +43,7 @@ Retrieving: MetaRobots
Retrieving: Hreflang Retrieving: Hreflang
Retrieving: EmbeddedContent Retrieving: EmbeddedContent
Retrieving: Canonical Retrieving: Canonical
Retrieving: RobotsTxt
Retrieving: Fonts Retrieving: Fonts
Loading page & waiting for onload Loading page & waiting for onload
Retrieving devtoolsLog and network records Retrieving devtoolsLog and network records
...@@ -62,18 +62,19 @@ Evaluating: Registers a service worker ...@@ -62,18 +62,19 @@ Evaluating: Registers a service worker
Evaluating: Responds with a 200 when offline Evaluating: Responds with a 200 when offline
Evaluating: Has a `<meta name="viewport">` tag with `width` or `initial-scale` Evaluating: Has a `<meta name="viewport">` tag with `width` or `initial-scale`
Evaluating: Contains some content when JavaScript is not available Evaluating: Contains some content when JavaScript is not available
Evaluating: First meaningful paint Evaluating: First Contentful Paint
Evaluating: First Meaningful Paint
Evaluating: Page load is fast enough on 3G Evaluating: Page load is fast enough on 3G
Evaluating: Perceptual Speed Index Evaluating: Speed Index
Evaluating: Screenshot Thumbnails Evaluating: Screenshot Thumbnails
Evaluating: Estimated Input Latency Evaluating: Estimated Input Latency
Evaluating: No browser errors logged to the console Evaluating: No browser errors logged to the console
Evaluating: Keep server response times low (TTFB) Evaluating: Keep server response times low (TTFB)
Evaluating: First Interactive (beta) Evaluating: First CPU Idle
Evaluating: Consistently Interactive (beta) Evaluating: Time to Interactive
Evaluating: User Timing marks and measures Evaluating: User Timing marks and measures
Evaluating: Critical Request Chains Evaluating: Critical Request Chains
Evaluating: Avoids page redirects Evaluating: Avoid multiple page redirects
Evaluating: User can be prompted to Install the Web App Evaluating: User can be prompted to Install the Web App
Evaluating: Configured for a custom splash screen Evaluating: Configured for a custom splash screen
Evaluating: Address bar matches brand colors Evaluating: Address bar matches brand colors
...@@ -81,10 +82,13 @@ Evaluating: Manifest's `short_name` won't be truncated when displayed on homescr ...@@ -81,10 +82,13 @@ Evaluating: Manifest's `short_name` won't be truncated when displayed on homescr
Evaluating: Content is sized correctly for the viewport Evaluating: Content is sized correctly for the viewport
Evaluating: Displays images with correct aspect ratio Evaluating: Displays images with correct aspect ratio
Evaluating: Avoids deprecated APIs Evaluating: Avoids deprecated APIs
Evaluating: Main thread work breakdown Evaluating: Minimizes main thread work
Evaluating: JavaScript boot-up time Evaluating: JavaScript boot-up time
Evaluating: Preload key requests Evaluating: Preload key requests
Evaluating: Avoid multiple, costly round trips to any origin
Evaluating: All text remains visible during webfont loads Evaluating: All text remains visible during webfont loads
Evaluating: Network Requests
Evaluating: Metrics
Evaluating: Site works cross-browser Evaluating: Site works cross-browser
Evaluating: Page transitions don't feel like they block on the network Evaluating: Page transitions don't feel like they block on the network
Evaluating: Each page has a URL Evaluating: Each page has a URL
...@@ -135,26 +139,26 @@ Evaluating: HTML5 landmark elements are used to improve navigation ...@@ -135,26 +139,26 @@ Evaluating: HTML5 landmark elements are used to improve navigation
Evaluating: Visual order on the page follows DOM order Evaluating: Visual order on the page follows DOM order
Evaluating: Uses efficient cache policy on static assets Evaluating: Uses efficient cache policy on static assets
Evaluating: Avoids enormous network payloads Evaluating: Avoids enormous network payloads
Evaluating: Offscreen images Evaluating: Defer offscreen images
Evaluating: Eliminate render-blocking resources
Evaluating: Minify CSS Evaluating: Minify CSS
Evaluating: Minify JavaScript Evaluating: Minify JavaScript
Evaluating: Unused CSS rules Evaluating: Defer unused CSS
Evaluating: Serve images in next-gen formats Evaluating: Serve images in next-gen formats
Evaluating: Optimize images Evaluating: Efficiently encode images
Evaluating: Enable text compression Evaluating: Enable text compression
Evaluating: Properly size images Evaluating: Properly size images
Evaluating: Use video formats for animated content
Evaluating: Avoids Application Cache Evaluating: Avoids Application Cache
Evaluating: Avoids an excessive DOM size Evaluating: Avoids an excessive DOM size
Evaluating: Opens external anchors using `rel="noopener"` Evaluating: Links to cross-origin destinations are safe
Evaluating: Avoids requesting the geolocation permission on page load Evaluating: Avoids requesting the geolocation permission on page load
Evaluating: Reduce render-blocking stylesheets
Evaluating: Avoids `document.write()` Evaluating: Avoids `document.write()`
Evaluating: Avoids Mutation Events in its own scripts Evaluating: Avoids Mutation Events in its own scripts
Evaluating: Avoids front-end JavaScript libraries with known security vulnerabilities Evaluating: Avoids front-end JavaScript libraries with known security vulnerabilities
Evaluating: Avoids WebSQL DB Evaluating: Avoids WebSQL DB
Evaluating: Avoids requesting the notification permission on page load Evaluating: Avoids requesting the notification permission on page load
Evaluating: Allows users to paste into password fields Evaluating: Allows users to paste into password fields
Evaluating: Reduce render-blocking scripts
Evaluating: Uses HTTP/2 for its own resources Evaluating: Uses HTTP/2 for its own resources
Evaluating: Uses passive listeners to improve scrolling performance Evaluating: Uses passive listeners to improve scrolling performance
Evaluating: Document has a meta description Evaluating: Document has a meta description
...@@ -162,6 +166,7 @@ Evaluating: Page has successful HTTP status code ...@@ -162,6 +166,7 @@ Evaluating: Page has successful HTTP status code
Evaluating: Document uses legible font sizes Evaluating: Document uses legible font sizes
Evaluating: Links have descriptive text Evaluating: Links have descriptive text
Evaluating: Page isn’t blocked from indexing Evaluating: Page isn’t blocked from indexing
Evaluating: robots.txt is valid
Evaluating: Document has a valid `hreflang` Evaluating: Document has a valid `hreflang`
Evaluating: Document avoids plugins Evaluating: Document avoids plugins
Evaluating: Document has a valid `rel=canonical` Evaluating: Document has a valid `rel=canonical`
...@@ -171,78 +176,81 @@ Generating results... ...@@ -171,78 +176,81 @@ Generating results...
=============== Lighthouse Results =============== =============== Lighthouse Results ===============
URL: http://127.0.0.1:8000/devtools/resources/inspected-page.html URL: http://127.0.0.1:8000/devtools/resources/inspected-page.html
Version: 2.9.1 Version: 3.0.0-beta.0
accesskeys: false accesskeys: true
appcache-manifest: true appcache-manifest: true
aria-allowed-attr: false aria-allowed-attr: true
aria-required-attr: false aria-required-attr: true
aria-required-children: false aria-required-children: true
aria-required-parent: false aria-required-parent: true
aria-roles: false aria-roles: true
aria-valid-attr: false aria-valid-attr: true
aria-valid-attr-value: false aria-valid-attr-value: true
audio-caption: false audio-caption: true
bootup-time: true bootup-time: true
button-name: false button-name: true
bypass: false bypass: true
canonical: true canonical: true
color-contrast: false color-contrast: true
consistently-interactive: false
content-width: false content-width: false
critical-request-chains: true critical-request-chains: true
custom-controls-labels: false custom-controls-labels: false
custom-controls-roles: false custom-controls-roles: false
definition-list: false definition-list: true
deprecations: true deprecations: true
dlitem: false dlitem: true
document-title: false document-title: false
dom-size: true dom-size: true
duplicate-id: false duplicate-id: true
efficient-animated-content: false
errors-in-console: false errors-in-console: false
estimated-input-latency: false estimated-input-latency: true
external-anchors-use-rel-noopener: true external-anchors-use-rel-noopener: true
first-interactive: false first-contentful-paint: true
first-meaningful-paint: false first-cpu-idle: true
first-meaningful-paint: true
focus-traps: false focus-traps: false
focusable-controls: false focusable-controls: false
font-display: true font-display: true
font-size: false font-size: false
frame-title: false frame-title: true
geolocation-on-start: true geolocation-on-start: true
heading-levels: false heading-levels: false
hreflang: true hreflang: true
html-has-lang: false html-has-lang: false
html-lang-valid: false html-lang-valid: true
http-status-code: true http-status-code: true
image-alt: false image-alt: true
image-aspect-ratio: true image-aspect-ratio: true
input-image-alt: false input-image-alt: true
interactive: true
is-crawlable: true is-crawlable: true
is-on-https: true is-on-https: true
label: false label: true
layout-table: false layout-table: true
link-blocking-first-paint: false link-name: true
link-name: false
link-text: true link-text: true
list: false list: true
listitem: false listitem: true
load-fast-enough-for-pwa: false load-fast-enough-for-pwa: true
logical-tab-order: false logical-tab-order: false
mainthread-work-breakdown: true mainthread-work-breakdown: true
managed-focus: false managed-focus: false
manifest-short-name-length: false manifest-short-name-length: false
meta-description: false meta-description: false
meta-refresh: false meta-refresh: true
meta-viewport: false meta-viewport: true
metrics: false
mobile-friendly: false mobile-friendly: false
network-requests: true
no-document-write: true no-document-write: true
no-mutation-events: true no-mutation-events: true
no-vulnerable-libraries: true no-vulnerable-libraries: true
no-websql: true no-websql: true
notification-on-start: true notification-on-start: true
object-alt: false object-alt: true
offscreen-content-hidden: false offscreen-content-hidden: false
offscreen-images: false offscreen-images: false
password-inputs-can-be-pasted-into: true password-inputs-can-be-pasted-into: true
...@@ -252,15 +260,16 @@ pwa-each-page-has-url: false ...@@ -252,15 +260,16 @@ pwa-each-page-has-url: false
pwa-page-transitions: false pwa-page-transitions: false
redirects: false redirects: false
redirects-http: false redirects-http: false
render-blocking-resources: false
robots-txt: true
screenshot-thumbnails: false screenshot-thumbnails: false
script-blocking-first-paint: false
service-worker: false service-worker: false
speed-index-metric: false speed-index: false
splash-screen: false splash-screen: false
structured-data: false structured-data: false
tabindex: false tabindex: true
td-headers-attr: false td-headers-attr: true
th-has-data-cells: false th-has-data-cells: true
themed-omnibox: false themed-omnibox: false
time-to-first-byte: true time-to-first-byte: true
total-byte-weight: true total-byte-weight: true
...@@ -273,18 +282,19 @@ uses-http2: false ...@@ -273,18 +282,19 @@ uses-http2: false
uses-long-cache-ttl: false uses-long-cache-ttl: false
uses-optimized-images: false uses-optimized-images: false
uses-passive-event-listeners: true uses-passive-event-listeners: true
uses-rel-preconnect: false
uses-rel-preload: false uses-rel-preload: false
uses-request-compression: false
uses-responsive-images: false uses-responsive-images: false
uses-text-compression: false
uses-webp-images: false uses-webp-images: false
valid-lang: false valid-lang: true
video-caption: false video-caption: true
video-description: false video-description: true
viewport: false viewport: false
visual-order-follows-dom: false visual-order-follows-dom: false
webapp-install-banner: false webapp-install-banner: false
without-javascript: false without-javascript: false
works-offline: false works-offline: false
# of .lh-audit divs: 109 # of .lh-audit divs: 110
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
var results = await Audits2TestRunner.waitForResults(); var results = await Audits2TestRunner.waitForResults();
TestRunner.addResult(`\n=============== Lighthouse Results ===============`); TestRunner.addResult(`\n=============== Lighthouse Results ===============`);
TestRunner.addResult(`URL: ${results.url}`); TestRunner.addResult(`URL: ${results.finalUrl}`);
TestRunner.addResult(`Version: ${results.lighthouseVersion}`); TestRunner.addResult(`Version: ${results.lighthouseVersion}`);
TestRunner.addResult('\n'); TestRunner.addResult('\n');
Object.keys(results.audits).sort().forEach(auditName => { Object.keys(results.audits).sort().forEach(auditName => {
var audit = results.audits[auditName]; var audit = results.audits[auditName];
TestRunner.addResult(`${audit.name}: ${Boolean(audit.rawValue)}`); TestRunner.addResult(`${audit.id}: ${Boolean(audit.rawValue)}`);
}); });
const resultsElement = Audits2TestRunner.getResultsElement(); const resultsElement = Audits2TestRunner.getResultsElement();
......
...@@ -54,6 +54,7 @@ all_devtools_files = [ ...@@ -54,6 +54,7 @@ all_devtools_files = [
"front_end/audits2/lighthouse/renderer/dom.js", "front_end/audits2/lighthouse/renderer/dom.js",
"front_end/audits2/lighthouse/renderer/details-renderer.js", "front_end/audits2/lighthouse/renderer/details-renderer.js",
"front_end/audits2/lighthouse/renderer/category-renderer.js", "front_end/audits2/lighthouse/renderer/category-renderer.js",
"front_end/audits2/lighthouse/renderer/performance-category-renderer.js",
"front_end/audits2/lighthouse/renderer/crc-details-renderer.js", "front_end/audits2/lighthouse/renderer/crc-details-renderer.js",
"front_end/audits2/lighthouse/renderer/report-renderer.js", "front_end/audits2/lighthouse/renderer/report-renderer.js",
"front_end/audits2/lighthouse/renderer/util.js", "front_end/audits2/lighthouse/renderer/util.js",
......
...@@ -254,29 +254,50 @@ Audits2.RuntimeSettings = [ ...@@ -254,29 +254,50 @@ Audits2.RuntimeSettings = [
}, },
{ {
setting: Common.settings.createSetting('audits2.throttling', 'default'), setting: Common.settings.createSetting('audits2.throttling', 'default'),
description: ls`Apply network and CPU throttling during performance auditing`,
setFlags: (flags, value) => { setFlags: (flags, value) => {
flags.disableNetworkThrottling = value === 'off'; switch (value) {
flags.disableCpuThrottling = value === 'off'; case 'devtools':
flags.throttlingMethod = 'devtools';
break;
case 'off':
flags.throttlingMethod = 'provided';
break;
default:
flags.throttlingMethod = 'simulate';
}
}, },
options: [ options: [
{label: ls`Fast 3G with 4x CPU Slowdown`, value: 'default'}, {
{label: ls`No throttling`, value: 'off'}, label: ls`Simulated Fast 3G, 4x CPU Slowdown`,
value: 'default',
title: 'Throttling is simulated, resulting in faster audit runs with similar measurement accuracy'
},
{
label: ls`Applied Fast 3G, 4x CPU Slowdown`,
value: 'devtools',
title: 'Typical DevTools throttling, with actual traffic shaping and CPU slowdown applied'
},
{
label: ls`No throttling`,
value: 'off',
title: 'No network or CPU throttling used. (Useful when not evaluating performance)'
},
], ],
}, },
{ {
setting: Common.settings.createSetting('audits2.clear_storage', true), setting: Common.settings.createSetting('audits2.clear_storage', true),
title: ls`Clear storage`, title: ls`Clear storage`,
description: ls`Reset storage (localStorage, IndexedDB, etc) to a clean baseline before auditing`, description: ls
`Reset storage (localStorage, IndexedDB, etc) before auditing. (Recommended for performance & PWA testing)`,
setFlags: (flags, value) => { setFlags: (flags, value) => {
flags.disableStorageReset = !value; flags.disableStorageReset = !value;
}, },
}, },
]; ];
Audits2.Events = { Audits2.Events = {
PageAuditabilityChanged: Symbol('PageAuditabilityChanged'), PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
AuditProgressChanged: Symbol('AuditProgressChanged'), AuditProgressChanged: Symbol('AuditProgressChanged'),
RequestAuditStart: Symbol('RequestAuditStart'), RequestAuditStart: Symbol('RequestAuditStart'),
RequestAuditCancel: Symbol('RequestAuditCancel'), RequestAuditCancel: Symbol('RequestAuditCancel'),
}; };
\ No newline at end of file
...@@ -116,8 +116,9 @@ Audits2.Audits2Panel = class extends UI.Panel { ...@@ -116,8 +116,9 @@ Audits2.Audits2Panel = class extends UI.Panel {
/** /**
* @param {!ReportRenderer.ReportJSON} lighthouseResult * @param {!ReportRenderer.ReportJSON} lighthouseResult
* @param {!ReportRenderer.RunnerResultArtifacts=} artifacts
*/ */
_renderReport(lighthouseResult) { _renderReport(lighthouseResult, artifacts) {
this.contentElement.classList.toggle('in-progress', false); this.contentElement.classList.toggle('in-progress', false);
this._startView.hideWidget(); this._startView.hideWidget();
this._statusView.hide(); this._statusView.hide();
...@@ -135,8 +136,7 @@ Audits2.Audits2Panel = class extends UI.Panel { ...@@ -135,8 +136,7 @@ Audits2.Audits2Panel = class extends UI.Panel {
const dom = new DOM(/** @type {!Document} */ (this._auditResultsElement.ownerDocument)); const dom = new DOM(/** @type {!Document} */ (this._auditResultsElement.ownerDocument));
const detailsRenderer = new Audits2.DetailsRenderer(dom); const detailsRenderer = new Audits2.DetailsRenderer(dom);
const categoryRenderer = new Audits2.CategoryRenderer(dom, detailsRenderer); const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
categoryRenderer.setTraceArtifact(lighthouseResult);
const renderer = new Audits2.ReportRenderer(dom, categoryRenderer); const renderer = new Audits2.ReportRenderer(dom, categoryRenderer);
const templatesHTML = Runtime.cachedResources['audits2/lighthouse/templates.html']; const templatesHTML = Runtime.cachedResources['audits2/lighthouse/templates.html'];
...@@ -145,20 +145,22 @@ Audits2.Audits2Panel = class extends UI.Panel { ...@@ -145,20 +145,22 @@ Audits2.Audits2Panel = class extends UI.Panel {
return; return;
renderer.setTemplateContext(templatesDOM); renderer.setTemplateContext(templatesDOM);
renderer.renderReport(lighthouseResult, reportContainer); const el = renderer.renderReport(lighthouseResult, reportContainer);
Audits2.ReportRenderer.addViewTraceButton(el, artifacts);
this._cachedRenderedReports.set(lighthouseResult, reportContainer); this._cachedRenderedReports.set(lighthouseResult, reportContainer);
} }
/** /**
* @param {!ReportRenderer.ReportJSON} lighthouseResult * @param {!ReportRenderer.ReportJSON} lighthouseResult
* @param {!ReportRenderer.RunnerResultArtifacts=} artifacts
*/ */
_buildReportUI(lighthouseResult) { _buildReportUI(lighthouseResult, artifacts) {
if (lighthouseResult === null) if (lighthouseResult === null)
return; return;
const optionElement = new Audits2.ReportSelector.Item( const optionElement = new Audits2.ReportSelector.Item(
lighthouseResult, () => this._renderReport(lighthouseResult), this._renderStartView.bind(this)); lighthouseResult, () => this._renderReport(lighthouseResult, artifacts), this._renderStartView.bind(this));
this._reportSelector.prepend(optionElement); this._reportSelector.prepend(optionElement);
this._refreshToolbarUI(); this._refreshToolbarUI();
this._renderReport(lighthouseResult); this._renderReport(lighthouseResult);
...@@ -206,21 +208,21 @@ Audits2.Audits2Panel = class extends UI.Panel { ...@@ -206,21 +208,21 @@ Audits2.Audits2Panel = class extends UI.Panel {
this._renderStatusView(inspectedURL); this._renderStatusView(inspectedURL);
const lighthouseResult = await this._protocolService.startLighthouse(inspectedURL, categoryIDs, flags); const lighthouseResponse = await this._protocolService.startLighthouse(inspectedURL, categoryIDs, flags);
if (lighthouseResult && lighthouseResult.fatal) { if (lighthouseResponse && lighthouseResponse.fatal) {
const error = new Error(lighthouseResult.message); const error = new Error(lighthouseResponse.message);
error.stack = lighthouseResult.stack; error.stack = lighthouseResponse.stack;
throw error; throw error;
} }
if (!lighthouseResult) if (!lighthouseResponse)
throw new Error('Auditing failed to produce a result'); throw new Error('Auditing failed to produce a result');
Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Finished); Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Finished);
await this._resetEmulationAndProtocolConnection(); await this._resetEmulationAndProtocolConnection();
this._buildReportUI(lighthouseResult); this._buildReportUI(lighthouseResponse.lhr, lighthouseResponse.artifacts);
} catch (err) { } catch (err) {
if (err instanceof Error) if (err instanceof Error)
this._statusView.renderBugReport(err); this._statusView.renderBugReport(err);
......
...@@ -28,7 +28,7 @@ Audits2.ProtocolService = class extends Common.Object { ...@@ -28,7 +28,7 @@ Audits2.ProtocolService = class extends Common.Object {
* @param {string} auditURL * @param {string} auditURL
* @param {!Array<string>} categoryIDs * @param {!Array<string>} categoryIDs
* @param {!Object} flags * @param {!Object} flags
* @return {!Promise<!ReportRenderer.ReportJSON>} * @return {!Promise<!ReportRenderer.RunnerResult>}
*/ */
startLighthouse(auditURL, categoryIDs, flags) { startLighthouse(auditURL, categoryIDs, flags) {
return this._send('start', {url: auditURL, categoryIDs, flags}); return this._send('start', {url: auditURL, categoryIDs, flags});
...@@ -80,7 +80,7 @@ Audits2.ProtocolService = class extends Common.Object { ...@@ -80,7 +80,7 @@ Audits2.ProtocolService = class extends Common.Object {
/** /**
* @param {string} method * @param {string} method
* @param {!Object=} params * @param {!Object=} params
* @return {!Promise<!ReportRenderer.ReportJSON>} * @return {!Promise<!ReportRenderer.RunnerResult>}
*/ */
_send(method, params) { _send(method, params) {
if (!this._backendPromise) if (!this._backendPromise)
......
...@@ -7,21 +7,22 @@ ...@@ -7,21 +7,22 @@
*/ */
Audits2.ReportRenderer = class extends ReportRenderer { Audits2.ReportRenderer = class extends ReportRenderer {
/** /**
* Provides empty element for left nav * @param {!Element} el Parent element to render the report into.
* @override * @param {!ReportRenderer.RunnerResultArtifacts=} artifacts
* @returns {!DocumentFragment}
*/ */
_renderReportNav() { static addViewTraceButton(el, artifacts) {
return createDocumentFragment(); if (!artifacts || !artifacts.traces || !artifacts.traces.defaultPass)
} return;
/** const defaultPassTrace = artifacts.traces.defaultPass;
* @param {!ReportRenderer.ReportJSON} report const timelineButton = UI.createTextButton(Common.UIString('View Trace'), onViewTraceClick, 'view-trace');
* @override el.querySelector('.lh-metric-column').appendChild(timelineButton);
* @return {!DocumentFragment} return el;
*/
_renderReportHeader(report) { async function onViewTraceClick() {
return createDocumentFragment(); await UI.inspectorView.showPanel('timeline');
Timeline.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
}
} }
}; };
...@@ -33,48 +34,6 @@ class ReportUIFeatures { ...@@ -33,48 +34,6 @@ class ReportUIFeatures {
} }
} }
Audits2.CategoryRenderer = class extends CategoryRenderer {
/**
* @override
* @param {!DOM} dom
* @param {!DetailsRenderer} detailsRenderer
*/
constructor(dom, detailsRenderer) {
super(dom, detailsRenderer);
this._defaultPassTrace = null;
}
/**
* @param {!ReportRenderer.ReportJSON} lhr
*/
setTraceArtifact(lhr) {
if (!lhr.artifacts || !lhr.artifacts.traces || !lhr.artifacts.traces.defaultPass)
return;
this._defaultPassTrace = lhr.artifacts.traces.defaultPass;
}
/**
* @override
* @param {!ReportRenderer.CategoryJSON} category
* @param {!Object<string, !ReportRenderer.GroupJSON>} groups
* @return {!Element}
*/
renderPerformanceCategory(category, groups) {
const defaultPassTrace = this._defaultPassTrace;
const element = super.renderPerformanceCategory(category, groups);
if (!defaultPassTrace)
return element;
const timelineButton = UI.createTextButton(Common.UIString('View Trace'), onViewTraceClick, 'view-trace');
element.querySelector('.lh-audit-group').prepend(timelineButton);
return element;
async function onViewTraceClick() {
await UI.inspectorView.showPanel('timeline');
Timeline.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
}
}
};
Audits2.DetailsRenderer = class extends DetailsRenderer { Audits2.DetailsRenderer = class extends DetailsRenderer {
/** /**
......
...@@ -111,8 +111,8 @@ Audits2.ReportSelector.Item = class { ...@@ -111,8 +111,8 @@ Audits2.ReportSelector.Item = class {
this._renderReport = renderReport; this._renderReport = renderReport;
this._showLandingCallback = showLandingCallback; this._showLandingCallback = showLandingCallback;
const url = new Common.ParsedURL(lighthouseResult.url); const url = new Common.ParsedURL(lighthouseResult.finalUrl);
const timestamp = lighthouseResult.generatedTime; const timestamp = lighthouseResult.fetchTime;
this._element = createElement('option'); this._element = createElement('option');
this._element.label = `${new Date(timestamp).toLocaleTimeString()} - ${url.domain()}`; this._element.label = `${new Date(timestamp).toLocaleTimeString()} - ${url.domain()}`;
} }
...@@ -135,8 +135,8 @@ Audits2.ReportSelector.Item = class { ...@@ -135,8 +135,8 @@ Audits2.ReportSelector.Item = class {
} }
download() { download() {
const url = new Common.ParsedURL(this._lighthouseResult.url).domain(); const url = new Common.ParsedURL(this._lighthouseResult.finalUrl).domain();
const timestamp = this._lighthouseResult.generatedTime; const timestamp = this._lighthouseResult.fetchTime;
const fileName = `${url}-${new Date(timestamp).toISO8601Compact()}.json`; const fileName = `${url}-${new Date(timestamp).toISO8601Compact()}.json`;
Workspace.fileManager.save(fileName, JSON.stringify(this._lighthouseResult), true); Workspace.fileManager.save(fileName, JSON.stringify(this._lighthouseResult), true);
} }
......
...@@ -16,13 +16,15 @@ Audits2.RadioSetting = class { ...@@ -16,13 +16,15 @@ Audits2.RadioSetting = class {
this._radioElements = []; this._radioElements = [];
for (const option of this._options) { for (const option of this._options) {
const fragment = UI.Fragment.build` const fragment = UI.Fragment.build`
<label class="audits2-radio"> <label $="label" class="audits2-radio">
<input $="input" type="radio" value=${option.value} name=${setting.name}> <input $="input" type="radio" value=${option.value} name=${setting.name}>
${option.label} ${option.label}
</label> </label>
`; `;
this.element.appendChild(fragment.element()); this.element.appendChild(fragment.element());
if (option.title)
UI.Tooltip.install(fragment.$('label'), option.title);
const radioElement = fragment.$('input'); const radioElement = fragment.$('input');
radioElement.addEventListener('change', this._valueChanged.bind(this)); radioElement.addEventListener('change', this._valueChanged.bind(this));
this._radioElements.push(radioElement); this._radioElements.push(radioElement);
......
...@@ -40,8 +40,7 @@ ...@@ -40,8 +40,7 @@
position: relative; position: relative;
} }
button.view-trace { button.view-trace {
position: absolute; margin: 10px;
right: 0;
} }
.audits2-results-container { .audits2-results-container {
......
...@@ -12,11 +12,13 @@ ...@@ -12,11 +12,13 @@
/* globals self Util */ /* globals self Util */
/** @typedef {import('./dom.js')} DOM */
class CriticalRequestChainRenderer { class CriticalRequestChainRenderer {
/** /**
* Create render context for critical-request-chain tree display. * Create render context for critical-request-chain tree display.
* @param {!Object<string, !CriticalRequestChainRenderer.CRCNode>} tree * @param {LH.Audit.SimpleCriticalRequestNode} tree
* @return {{tree: !Object<string, !CriticalRequestChainRenderer.CRCNode>, startTime: number, transferSize: number}} * @return {{tree: LH.Audit.SimpleCriticalRequestNode, startTime: number, transferSize: number}}
*/ */
static initTree(tree) { static initTree(tree) {
let startTime = 0; let startTime = 0;
...@@ -34,13 +36,13 @@ class CriticalRequestChainRenderer { ...@@ -34,13 +36,13 @@ class CriticalRequestChainRenderer {
* parent. Calculates if this node is the last child, whether it has any * parent. Calculates if this node is the last child, whether it has any
* children itself and what the tree looks like all the way back up to the root, * children itself and what the tree looks like all the way back up to the root,
* so the tree markers can be drawn correctly. * so the tree markers can be drawn correctly.
* @param {!Object<string, !CriticalRequestChainRenderer.CRCNode>} parent * @param {LH.Audit.SimpleCriticalRequestNode} parent
* @param {string} id * @param {string} id
* @param {number} startTime * @param {number} startTime
* @param {number} transferSize * @param {number} transferSize
* @param {!Array<boolean>=} treeMarkers * @param {Array<boolean>=} treeMarkers
* @param {boolean=} parentIsLastChild * @param {boolean=} parentIsLastChild
* @return {!CriticalRequestChainRenderer.CRCSegment} * @return {CRCSegment}
*/ */
static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) { static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) {
const node = parent[id]; const node = parent[id];
...@@ -68,10 +70,10 @@ class CriticalRequestChainRenderer { ...@@ -68,10 +70,10 @@ class CriticalRequestChainRenderer {
/** /**
* Creates the DOM for a tree segment. * Creates the DOM for a tree segment.
* @param {!DOM} dom * @param {DOM} dom
* @param {!DocumentFragment} tmpl * @param {DocumentFragment} tmpl
* @param {!CriticalRequestChainRenderer.CRCSegment} segment * @param {CRCSegment} segment
* @return {!Node} * @return {Node}
*/ */
static createChainNode(dom, tmpl, segment) { static createChainNode(dom, tmpl, segment) {
const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl); const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl);
...@@ -110,14 +112,14 @@ class CriticalRequestChainRenderer { ...@@ -110,14 +112,14 @@ class CriticalRequestChainRenderer {
const {file, hostname} = Util.parseURL(segment.node.request.url); const {file, hostname} = Util.parseURL(segment.node.request.url);
const treevalEl = dom.find('.crc-node__tree-value', chainsEl); const treevalEl = dom.find('.crc-node__tree-value', chainsEl);
dom.find('.crc-node__tree-file', treevalEl).textContent = `${file}`; dom.find('.crc-node__tree-file', treevalEl).textContent = `${file}`;
dom.find('.crc-node__tree-hostname', treevalEl).textContent = `(${hostname})`; dom.find('.crc-node__tree-hostname', treevalEl).textContent = hostname ? `(${hostname})` : '';
if (!segment.hasChildren) { if (!segment.hasChildren) {
const span = dom.createElement('span', 'crc-node__chain-duration'); const span = dom.createElement('span', 'crc-node__chain-duration');
span.textContent = ' - ' + Util.chainDuration( span.textContent = ' - ' + Util.chainDuration(
segment.node.request.startTime, segment.node.request.endTime) + 'ms, '; segment.node.request.startTime, segment.node.request.endTime) + 'ms, ';
const span2 = dom.createElement('span', 'crc-node__chain-duration'); const span2 = dom.createElement('span', 'crc-node__chain-duration');
span2.textContent = Util.formatBytesToKB(segment.node.request.transferSize); span2.textContent = Util.formatBytesToKB(segment.node.request.transferSize, 0.01);
treevalEl.appendChild(span); treevalEl.appendChild(span);
treevalEl.appendChild(span2); treevalEl.appendChild(span2);
...@@ -128,52 +130,48 @@ class CriticalRequestChainRenderer { ...@@ -128,52 +130,48 @@ class CriticalRequestChainRenderer {
/** /**
* Recursively builds a tree from segments. * Recursively builds a tree from segments.
* @param {!DOM} dom * @param {DOM} dom
* @param {!DocumentFragment} tmpl * @param {DocumentFragment} tmpl
* @param {!CriticalRequestChainRenderer.CRCSegment} segment * @param {CRCSegment} segment
* @param {!Element} detailsEl Parent details element. * @param {Element} elem Parent element.
* @param {!CriticalRequestChainRenderer.CRCDetailsJSON} details * @param {CRCDetailsJSON} details
*/ */
static buildTree(dom, tmpl, segment, detailsEl, details) { static buildTree(dom, tmpl, segment, elem, details) {
detailsEl.appendChild(CriticalRequestChainRenderer.createChainNode(dom, tmpl, segment)); elem.appendChild(CriticalRequestChainRenderer.createChainNode(dom, tmpl, segment));
for (const key of Object.keys(segment.node.children)) { for (const key of Object.keys(segment.node.children)) {
const childSegment = CriticalRequestChainRenderer.createSegment(segment.node.children, key, const childSegment = CriticalRequestChainRenderer.createSegment(segment.node.children, key,
segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild); segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild);
CriticalRequestChainRenderer.buildTree(dom, tmpl, childSegment, detailsEl, details); CriticalRequestChainRenderer.buildTree(dom, tmpl, childSegment, elem, details);
} }
} }
/** /**
* @param {!DOM} dom * @param {DOM} dom
* @param {!Node} templateContext * @param {ParentNode} templateContext
* @param {!CriticalRequestChainRenderer.CRCDetailsJSON} details * @param {CRCDetailsJSON} details
* @return {!Node} * @return {Element}
*/ */
static render(dom, templateContext, details) { static render(dom, templateContext, details) {
const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext); const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext);
const containerEl = dom.find('.lh-crc', tmpl);
// Fill in top summary. // Fill in top summary.
dom.find('.lh-crc__longest_duration', tmpl).textContent = dom.find('.lh-crc__longest_duration', tmpl).textContent =
Util.formatNumber(details.longestChain.duration) + 'ms'; Util.formatNumber(details.longestChain.duration) + 'ms';
dom.find('.lh-crc__longest_length', tmpl).textContent = details.longestChain.length; dom.find('.lh-crc__longest_length', tmpl).textContent = details.longestChain.length.toString();
dom.find('.lh-crc__longest_transfersize', tmpl).textContent = dom.find('.lh-crc__longest_transfersize', tmpl).textContent =
Util.formatBytesToKB(details.longestChain.transferSize); Util.formatBytesToKB(details.longestChain.transferSize);
const detailsEl = dom.find('.lh-details', tmpl);
detailsEl.open = true;
dom.find('.lh-details > summary', tmpl).textContent = details.header.text;
// Construct visual tree. // Construct visual tree.
const root = CriticalRequestChainRenderer.initTree(details.chains); const root = CriticalRequestChainRenderer.initTree(details.chains);
for (const key of Object.keys(root.tree)) { for (const key of Object.keys(root.tree)) {
const segment = CriticalRequestChainRenderer.createSegment(root.tree, key, const segment = CriticalRequestChainRenderer.createSegment(root.tree, key,
root.startTime, root.transferSize); root.startTime, root.transferSize);
CriticalRequestChainRenderer.buildTree(dom, tmpl, segment, detailsEl, details); CriticalRequestChainRenderer.buildTree(dom, tmpl, segment, containerEl, details);
} }
return tmpl; return dom.find('.lh-crc-container', tmpl);
} }
} }
...@@ -185,44 +183,19 @@ if (typeof module !== 'undefined' && module.exports) { ...@@ -185,44 +183,19 @@ if (typeof module !== 'undefined' && module.exports) {
} }
/** @typedef {{ /** @typedef {{
* type: string, type: string,
* header: {text: string}, header: {text: string},
* longestChain: {duration: number, length: number, transferSize: number}, longestChain: {duration: number, length: number, transferSize: number},
* chains: !Object<string, !CriticalRequestChainRenderer.CRCNode> chains: LH.Audit.SimpleCriticalRequestNode
* }} }} CRCDetailsJSON
*/
CriticalRequestChainRenderer.CRCDetailsJSON; // eslint-disable-line no-unused-expressions
/** @typedef {{
* endTime: number,
* responseReceivedTime: number,
* startTime: number,
* transferSize: number,
* url: string
* }}
*/ */
CriticalRequestChainRenderer.CRCRequest; // eslint-disable-line no-unused-expressions
/**
* Record type so children can circularly have CRCNode values.
* @struct
* @record
*/
CriticalRequestChainRenderer.CRCNode = function() {};
/** @type {!Object<string, !CriticalRequestChainRenderer.CRCNode>} */
CriticalRequestChainRenderer.CRCNode.prototype.children; // eslint-disable-line no-unused-expressions
/** @type {!CriticalRequestChainRenderer.CRCRequest} */
CriticalRequestChainRenderer.CRCNode.prototype.request; // eslint-disable-line no-unused-expressions
/** @typedef {{ /** @typedef {{
* node: !CriticalRequestChainRenderer.CRCNode, node: LH.Audit.SimpleCriticalRequestNode[string],
* isLastChild: boolean, isLastChild: boolean,
* hasChildren: boolean, hasChildren: boolean,
* startTime: number, startTime: number,
* transferSize: number, transferSize: number,
* treeMarkers: !Array<boolean> treeMarkers: Array<boolean>
* }} }} CRCSegment
*/ */
CriticalRequestChainRenderer.CRCSegment; // eslint-disable-line no-unused-expressions
...@@ -9,20 +9,21 @@ ...@@ -9,20 +9,21 @@
class DOM { class DOM {
/** /**
* @param {!Document} document * @param {Document} document
*/ */
constructor(document) { constructor(document) {
/** @private {!Document} */ /** @type {Document} */
this._document = document; this._document = document;
} }
// TODO(bckenny): can pass along `createElement`'s inferred type
/** /**
* @param {string} name * @param {string} name
* @param {string=} className * @param {string=} className
* @param {!Object<string, (string|undefined)>=} attrs Attribute key/val pairs. * @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not * Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node. * set the attribute on the node.
* @return {!Element} * @return {Element}
*/ */
createElement(name, className, attrs = {}) { createElement(name, className, attrs = {}) {
const element = this._document.createElement(name); const element = this._document.createElement(name);
...@@ -39,13 +40,20 @@ class DOM { ...@@ -39,13 +40,20 @@ class DOM {
} }
/** /**
* @param {!Element} parentElem * @return {DocumentFragment}
*/
createFragment() {
return this._document.createDocumentFragment();
}
/**
* @param {Element} parentElem
* @param {string} elementName * @param {string} elementName
* @param {string=} className * @param {string=} className
* @param {!Object<string, (string|undefined)>=} attrs Attribute key/val pairs. * @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not * Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node. * set the attribute on the node.
* @return {!Element} * @return {Element}
*/ */
createChildOf(parentElem, elementName, className, attrs) { createChildOf(parentElem, elementName, className, attrs) {
const element = this.createElement(elementName, className, attrs); const element = this.createElement(elementName, className, attrs);
...@@ -55,8 +63,8 @@ class DOM { ...@@ -55,8 +63,8 @@ class DOM {
/** /**
* @param {string} selector * @param {string} selector
* @param {!Node} context * @param {ParentNode} context
* @return {!DocumentFragment} A clone of the template content. * @return {DocumentFragment} A clone of the template content.
* @throws {Error} * @throws {Error}
*/ */
cloneTemplate(selector, context) { cloneTemplate(selector, context) {
...@@ -65,15 +73,14 @@ class DOM { ...@@ -65,15 +73,14 @@ class DOM {
throw new Error(`Template not found: template${selector}`); throw new Error(`Template not found: template${selector}`);
} }
const clone = /** @type {!DocumentFragment} */ (this._document.importNode( const clone = this._document.importNode(template.content, true);
template.content, true));
// Prevent duplicate styles in the DOM. After a template has been stamped // Prevent duplicate styles in the DOM. After a template has been stamped
// for the first time, remove the clone's styles so they're not re-added. // for the first time, remove the clone's styles so they're not re-added.
if (template.hasAttribute('data-stamped')) { if (template.hasAttribute('data-stamped')) {
this.findAll('style', clone).forEach(style => style.remove()); this.findAll('style', clone).forEach(style => style.remove());
} }
template.setAttribute('data-stamped', true); template.setAttribute('data-stamped', 'true');
return clone; return clone;
} }
...@@ -89,7 +96,7 @@ class DOM { ...@@ -89,7 +96,7 @@ class DOM {
/** /**
* @param {string} text * @param {string} text
* @return {!Element} * @return {Element}
*/ */
convertMarkdownLinkSnippets(text) { convertMarkdownLinkSnippets(text) {
const element = this.createElement('span'); const element = this.createElement('span');
...@@ -104,7 +111,7 @@ class DOM { ...@@ -104,7 +111,7 @@ class DOM {
// Append link if there are any. // Append link if there are any.
if (linkText && linkHref) { if (linkText && linkHref) {
const a = /** @type {!HTMLAnchorElement} */ (this.createElement('a')); const a = /** @type {HTMLAnchorElement} */ (this.createElement('a'));
a.rel = 'noopener'; a.rel = 'noopener';
a.target = '_blank'; a.target = '_blank';
a.textContent = linkText; a.textContent = linkText;
...@@ -118,7 +125,7 @@ class DOM { ...@@ -118,7 +125,7 @@ class DOM {
/** /**
* @param {string} text * @param {string} text
* @return {!Element} * @return {Element}
*/ */
convertMarkdownCodeSnippets(text) { convertMarkdownCodeSnippets(text) {
const element = this.createElement('span'); const element = this.createElement('span');
...@@ -129,7 +136,7 @@ class DOM { ...@@ -129,7 +136,7 @@ class DOM {
const [preambleText, codeText] = parts.splice(0, 2); const [preambleText, codeText] = parts.splice(0, 2);
element.appendChild(this._document.createTextNode(preambleText)); element.appendChild(this._document.createTextNode(preambleText));
if (codeText) { if (codeText) {
const pre = /** @type {!HTMLPreElement} */ (this.createElement('code')); const pre = /** @type {HTMLPreElement} */ (this.createElement('code'));
pre.textContent = codeText; pre.textContent = codeText;
element.appendChild(pre); element.appendChild(pre);
} }
...@@ -139,20 +146,29 @@ class DOM { ...@@ -139,20 +146,29 @@ class DOM {
} }
/** /**
* @return {!Document} * @return {Document}
*/ */
document() { document() {
return this._document; return this._document;
} }
/**
* TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
* @return {boolean}
*/
isDevTools() {
return !!this._document.querySelector('.lh-devtools');
}
/** /**
* Guaranteed context.querySelector. Always returns an element or throws if * Guaranteed context.querySelector. Always returns an element or throws if
* nothing matches query. * nothing matches query.
* @param {string} query * @param {string} query
* @param {!Node} context * @param {ParentNode} context
* @return {!Element} * @return {HTMLElement}
*/ */
find(query, context) { find(query, context) {
/** @type {?HTMLElement} */
const result = context.querySelector(query); const result = context.querySelector(query);
if (result === null) { if (result === null) {
throw new Error(`query ${query} not found`); throw new Error(`query ${query} not found`);
...@@ -163,8 +179,8 @@ class DOM { ...@@ -163,8 +179,8 @@ class DOM {
/** /**
* Helper for context.querySelectorAll. Returns an Array instead of a NodeList. * Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
* @param {string} query * @param {string} query
* @param {!Node} context * @param {ParentNode} context
* @return {!Array<!Element>} * @return {Array<HTMLElement>}
*/ */
findAll(query, context) { findAll(query, context) {
return Array.from(context.querySelectorAll(query)); return Array.from(context.querySelectorAll(query));
......
/**
* @license Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
/* globals self, Util, CategoryRenderer */
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./report-renderer.js').CategoryJSON} CategoryJSON */
/** @typedef {import('./report-renderer.js').GroupJSON} GroupJSON */
/** @typedef {import('./report-renderer.js').AuditJSON} AuditJSON */
/** @typedef {import('./details-renderer.js').OpportunitySummary} OpportunitySummary */
/** @typedef {import('./details-renderer.js').FilmstripDetails} FilmstripDetails */
class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* @param {AuditJSON} audit
* @return {Element}
*/
_renderMetric(audit) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-metric', this.templateContext);
const element = this.dom.find('.lh-metric', tmpl);
element.id = audit.result.id;
const rating = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
element.classList.add(`lh-metric--${rating}`);
const titleEl = this.dom.find('.lh-metric__title', tmpl);
titleEl.textContent = audit.result.title;
const valueEl = this.dom.find('.lh-metric__value', tmpl);
valueEl.textContent = Util.formatDisplayValue(audit.result.displayValue);
const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
if (audit.result.scoreDisplayMode === 'error') {
descriptionEl.textContent = '';
valueEl.textContent = 'Error!';
const tooltip = this.dom.createChildOf(descriptionEl, 'span');
tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
}
return element;
}
/**
* @param {AuditJSON} audit
* @param {number} index
* @param {number} scale
* @return {Element}
*/
_renderOpportunity(audit, index, scale) {
const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
const element = this.populateAuditValues(audit, index, oppTmpl);
element.id = audit.result.id;
const details = audit.result.details;
if (!details) {
return element;
}
const summaryInfo = /** @type {OpportunitySummary} */ (details.summary);
if (!summaryInfo || !summaryInfo.wastedMs || audit.result.scoreDisplayMode === 'error') {
return element;
}
// Overwrite the displayValue with opportunity's wastedMs
const displayEl = this.dom.find('.lh-audit__display-text', element);
const sparklineWidthPct = `${summaryInfo.wastedMs / scale * 100}%`;
this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
displayEl.textContent = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
// Set [title] tooltips
if (audit.result.displayValue) {
const displayValue = Util.formatDisplayValue(audit.result.displayValue);
this.dom.find('.lh-load-opportunity__sparkline', element).title = displayValue;
displayEl.title = displayValue;
}
return element;
}
/**
* Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
* Opportunties with an error won't have a summary object, so MIN_VALUE is returned to keep any
* erroring opportunities last in sort order.
* @param {AuditJSON} audit
* @return {number}
*/
_getWastedMs(audit) {
if (
audit.result.details &&
audit.result.details.summary &&
typeof audit.result.details.summary.wastedMs === 'number'
) {
return audit.result.details.summary.wastedMs;
} else {
return Number.MIN_VALUE;
}
}
/**
* @param {CategoryJSON} category
* @param {Object<string, GroupJSON>} groups
* @return {Element}
* @override
*/
render(category, groups) {
const element = this.dom.createElement('div', 'lh-category');
this.createPermalinkSpan(element, category.id);
element.appendChild(this.renderCategoryHeader(category));
// Metrics
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
const metricAuditsEl = this.renderAuditGroup(groups['metrics'], {expandable: false});
const keyMetrics = metricAudits.filter(a => a.weight >= 3);
const otherMetrics = metricAudits.filter(a => a.weight < 3);
const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metric-container');
const metricsColumn1El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
const metricsColumn2El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
keyMetrics.forEach(item => {
metricsColumn1El.appendChild(this._renderMetric(item));
});
otherMetrics.forEach(item => {
metricsColumn2El.appendChild(this._renderMetric(item));
});
const estValuesEl = this.dom.createChildOf(metricsColumn2El, 'div',
'lh-metrics__disclaimer lh-metrics__disclaimer');
estValuesEl.textContent = 'Values are estimated and may vary.';
metricAuditsEl.classList.add('lh-audit-group--metrics');
element.appendChild(metricAuditsEl);
// Filmstrip
const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
const thumbnailAudit = category.auditRefs.find(audit => audit.id === 'screenshot-thumbnails');
const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
if (thumbnailResult && thumbnailResult.details) {
timelineEl.id = thumbnailResult.id;
const filmstripEl = this.detailsRenderer.render(thumbnailResult.details);
timelineEl.appendChild(filmstripEl);
}
// Opportunities
const opportunityAudits = category.auditRefs
.filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
.sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
if (opportunityAudits.length) {
// Scale the sparklines relative to savings, minimum 2s to not overstate small savings
const minimumScale = 2000;
const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
const maxWaste = Math.max(...wastedMsValues);
const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
const groupEl = this.renderAuditGroup(groups['load-opportunities'], {expandable: false});
const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
groupEl.appendChild(headerEl);
opportunityAudits.forEach((item, i) =>
groupEl.appendChild(this._renderOpportunity(item, i, scale)));
groupEl.classList.add('lh-audit-group--opportunities');
element.appendChild(groupEl);
}
// Diagnostics
const diagnosticAudits = category.auditRefs
.filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
.sort((a, b) => {
const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
return scoreA - scoreB;
});
if (diagnosticAudits.length) {
const groupEl = this.renderAuditGroup(groups['diagnostics'], {expandable: false});
diagnosticAudits.forEach((item, i) => groupEl.appendChild(this.renderAudit(item, i)));
groupEl.classList.add('lh-audit-group--diagnostics');
element.appendChild(groupEl);
}
// Passed audits
const passedElements = category.auditRefs
.filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
Util.showAsPassed(audit.result))
.map((audit, i) => this.renderAudit(audit, i));
if (!passedElements.length) return element;
const passedElem = this.renderPassedAuditsSection(passedElements);
element.appendChild(passedElem);
return element;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = PerformanceCategoryRenderer;
} else {
self.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
}
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"lighthouse/renderer/util.js", "lighthouse/renderer/util.js",
"lighthouse/renderer/dom.js", "lighthouse/renderer/dom.js",
"lighthouse/renderer/category-renderer.js", "lighthouse/renderer/category-renderer.js",
"lighthouse/renderer/performance-category-renderer.js",
"lighthouse/renderer/details-renderer.js", "lighthouse/renderer/details-renderer.js",
"lighthouse/renderer/crc-details-renderer.js", "lighthouse/renderer/crc-details-renderer.js",
"lighthouse/renderer/report-renderer.js", "lighthouse/renderer/report-renderer.js",
...@@ -40,5 +41,14 @@ ...@@ -40,5 +41,14 @@
"audits2Panel.css", "audits2Panel.css",
"lighthouse/report-styles.css", "lighthouse/report-styles.css",
"lighthouse/templates.html" "lighthouse/templates.html"
],
"skip_compilation": [
"lighthouse/renderer/util.js",
"lighthouse/renderer/dom.js",
"lighthouse/renderer/category-renderer.js",
"lighthouse/renderer/performance-category-renderer.js",
"lighthouse/renderer/details-renderer.js",
"lighthouse/renderer/crc-details-renderer.js",
"lighthouse/renderer/report-renderer.js"
] ]
} }
...@@ -37,7 +37,7 @@ var Audits2Service = class { // eslint-disable-line ...@@ -37,7 +37,7 @@ var Audits2Service = class { // eslint-disable-line
} }
/** /**
* @return {!Promise<!ReportRenderer.ReportJSON>} * @return {!Promise<!ReportRenderer.RunnerResult>}
*/ */
start(params) { start(params) {
if (Runtime.queryParam('isUnderTest')) if (Runtime.queryParam('isUnderTest'))
...@@ -49,7 +49,7 @@ var Audits2Service = class { // eslint-disable-line ...@@ -49,7 +49,7 @@ var Audits2Service = class { // eslint-disable-line
return Promise.resolve() return Promise.resolve()
.then(_ => self.runLighthouseInWorker(this, params.url, {flags: params.flags}, params.categoryIDs)) .then(_ => self.runLighthouseInWorker(this, params.url, {flags: params.flags}, params.categoryIDs))
.then(/** @type {!ReportRenderer.ReportJSON} */ result => { .then(/** @type {!ReportRenderer.RunnerResult} */ result => {
// Keep all artifacts on the result, no trimming // Keep all artifacts on the result, no trimming
return result; return result;
}) })
......
...@@ -143,7 +143,7 @@ Bindings.ChunkedFileReader = class { ...@@ -143,7 +143,7 @@ Bindings.ChunkedFileReader = class {
if (event.target.readyState !== FileReader.DONE) if (event.target.readyState !== FileReader.DONE)
return; return;
const buffer = event.target.result; const buffer = this._reader.result;
this._loadedSize += buffer.byteLength; this._loadedSize += buffer.byteLength;
const endOfFile = this._loadedSize === this._fileSize; const endOfFile = this._loadedSize === this._fileSize;
const decodedString = this._decoder.decode(buffer, {stream: !endOfFile}); const decodedString = this._decoder.decode(buffer, {stream: !endOfFile});
......
...@@ -836,3 +836,164 @@ const ls = function(strings, vararg) {}; ...@@ -836,3 +836,164 @@ const ls = function(strings, vararg) {};
* @param {function(!Array<*>)} callback * @param {function(!Array<*>)} callback
*/ */
const ResizeObserver = function(callback) {}; const ResizeObserver = function(callback) {};
// Lighthouse Report Renderer
/**
* @constructor
* @param {!Document} document
*/
const DOM = function(document) {};
/**
* @constructor
* @param {!DOM} dom
*/
const ReportRenderer = function(dom) {};
ReportRenderer.prototype = {
/**
* @param {!ReportRenderer.ReportJSON} report
* @param {!Element} container Parent element to render the report into.
*/
renderReport: function(report, container) {},
/**
* @param {!Document|!Element} context
*/
setTemplateContext: function(context) {},
};
/**
* @typedef {{
* rawValue: (number|boolean|undefined),
* id: string,
* title: string,
* description: string,
* explanation: (string|undefined),
* errorMessage: (string|undefined),
* displayValue: (string|Array<string|number>|undefined),
* scoreDisplayMode: string,
* error: boolean,
* score: (number|null),
* details: (!DetailsRenderer.DetailsJSON|undefined),
* }}
*/
ReportRenderer.AuditResultJSON;
/**
* @typedef {{
* id: string,
* score: (number|null),
* weight: number,
* group: (string|undefined),
* result: ReportRenderer.AuditResultJSON
* }}
*/
ReportRenderer.AuditJSON;
/**
* @typedef {{
* title: string,
* id: string,
* score: (number|null),
* description: (string|undefined),
* manualDescription: string,
* auditRefs: !Array<!ReportRenderer.AuditJSON>
* }}
*/
ReportRenderer.CategoryJSON;
/**
* @typedef {{
* title: string,
* description: (string|undefined),
* }}
*/
ReportRenderer.GroupJSON;
/**
* @typedef {{
* lighthouseVersion: string,
* userAgent: string,
* fetchTime: string,
* timing: {total: number},
* requestedUrl: string,
* finalUrl: string,
* runWarnings: (!Array<string>|undefined),
* artifacts: {traces: {defaultPass: {traceEvents: !Array}}},
* audits: !Object<string, !ReportRenderer.AuditResultJSON>,
* categories: !Object<string, !ReportRenderer.CategoryJSON>,
* categoryGroups: !Object<string, !ReportRenderer.GroupJSON>,
* }}
*/
ReportRenderer.ReportJSON;
/**
* @typedef {{
* traces: {defaultPass: {traceEvents: !Array}},
* }}
*/
ReportRenderer.RunnerResultArtifacts;
/**
* @typedef {{
* lhr: !ReportRenderer.ReportJSON,
* artifacts: ReportRenderer.RunnerResultArtifacts,
* report: string
* }}
*/
ReportRenderer.RunnerResult;
/**
* @constructor
* @param {!DOM} dom
* @param {!DetailsRenderer} detailsRenderer
*/
const CategoryRenderer = function(dom, detailsRenderer) {};
/**
* @constructor
* @param {!DOM} dom
*/
const DetailsRenderer = function(dom) {};
DetailsRenderer.prototype = {
/**
* @param {!DetailsRenderer.NodeDetailsJSON} item
* @return {!Element}
*/
renderNode: function(item) {},
};
/**
* @typedef {{
* type: string,
* value: (string|number|undefined),
* summary: (DetailsRenderer.OpportunitySummary|undefined),
* granularity: (number|undefined),
* displayUnit: (string|undefined)
* }}
*/
DetailsRenderer.DetailsJSON;
/**
* @typedef {{
* type: string,
* path: (string|undefined),
* selector: (string|undefined),
* snippet:(string|undefined)
* }}
*/
DetailsRenderer.NodeDetailsJSON;
/** @typedef {{
* wastedMs: (number|undefined),
* wastedBytes: (number|undefined),
* }}
*/
DetailsRenderer.OpportunitySummary;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment