Commit 6cba4bab authored by Paul Irish's avatar Paul Irish Committed by Commit Bot

DevTools: [Audits] Roll to Lighthouse 3.0.3

Hash: d1cae24fda4182406e02d3f4df6309d48878fc50

Bug: 772558
Change-Id: Ia7db6d3088e573562aa72172f52fda7be58cc17d
Reviewed-on: https://chromium-review.googlesource.com/1141325Reviewed-by: default avatarPavel Feldman <pfeldman@chromium.org>
Commit-Queue: Paul Irish <paulirish@chromium.org>
Cr-Commit-Position: refs/heads/master@{#576087}
parent 214e286b
......@@ -26,9 +26,9 @@ Retrieving: RuntimeExceptions
Retrieving: ChromeConsoleMessages
Retrieving: ImageUsage
Retrieving: Accessibility
Retrieving: EventListeners
Retrieving: AnchorsWithNoRelNoopener
Retrieving: AppCacheManifest
Retrieving: Doctype
Retrieving: DOMStats
Retrieving: JSLibraries
Retrieving: OptimizedImages
......@@ -78,7 +78,7 @@ Evaluating: Avoid multiple page redirects
Evaluating: User can be prompted to Install the Web App
Evaluating: Configured for a custom splash screen
Evaluating: Address bar matches brand colors
Evaluating: Manifest's `short_name` won't be truncated when displayed on homescreen
Evaluating: The `short_name` won't be truncated on the homescreen
Evaluating: Content is sized correctly for the viewport
Evaluating: Displays images with correct aspect ratio
Evaluating: Avoids deprecated APIs
......@@ -150,11 +150,11 @@ Evaluating: Enable text compression
Evaluating: Properly size images
Evaluating: Use video formats for animated content
Evaluating: Avoids Application Cache
Evaluating: Page has the HTML doctype
Evaluating: Avoids an excessive DOM size
Evaluating: Links to cross-origin destinations are safe
Evaluating: Avoids requesting the geolocation permission on page load
Evaluating: Avoids `document.write()`
Evaluating: Avoids Mutation Events in its own scripts
Evaluating: Avoids front-end JavaScript libraries with known security vulnerabilities
Evaluating: Avoids WebSQL DB
Evaluating: Avoids requesting the notification permission on page load
......@@ -176,7 +176,7 @@ Generating results...
=============== Lighthouse Results ===============
URL: http://127.0.0.1:8000/devtools/resources/inspected-page.html
Version: 3.0.0-beta.0
Version: 3.0.3
accesskeys: not-applicable
......@@ -201,6 +201,7 @@ custom-controls-roles: manual
definition-list: not-applicable
deprecations: pass
dlitem: not-applicable
doctype: fail
document-title: fail
dom-size: numeric
duplicate-id: not-applicable
......@@ -238,7 +239,7 @@ load-fast-enough-for-pwa: flaky
logical-tab-order: manual
mainthread-work-breakdown: numeric
managed-focus: manual
manifest-short-name-length: fail
manifest-short-name-length: not-applicable
meta-description: fail
meta-refresh: not-applicable
meta-viewport: not-applicable
......@@ -246,7 +247,6 @@ metrics: flaky
mobile-friendly: manual
network-requests: informative
no-document-write: pass
no-mutation-events: pass
no-vulnerable-libraries: pass
no-websql: pass
notification-on-start: pass
......
......@@ -9,9 +9,6 @@
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./report-renderer.js')} ReportRenderer */
/** @typedef {import('./report-renderer.js').AuditJSON} AuditJSON */
/** @typedef {import('./report-renderer.js').CategoryJSON} CategoryJSON */
/** @typedef {import('./report-renderer.js').GroupJSON} GroupJSON */
/** @typedef {import('./details-renderer.js')} DetailsRenderer */
/** @typedef {import('./util.js')} Util */
......@@ -32,7 +29,7 @@ class CategoryRenderer {
}
/**
* @param {AuditJSON} audit
* @param {LH.ReportResult.AuditRef} audit
* @param {number} index
* @return {Element}
*/
......@@ -43,7 +40,7 @@ class CategoryRenderer {
/**
* Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity
* @param {AuditJSON} audit
* @param {LH.ReportResult.AuditRef} audit
* @param {number} index
* @param {DocumentFragment} tmpl
* @return {Element}
......@@ -105,7 +102,7 @@ class CategoryRenderer {
}
/**
* @return {!HTMLElement}
* @return {HTMLElement}
*/
_createChevron() {
const chevronTmpl = this.dom.cloneTemplate('#tmpl-lh-chevron', this.templateContext);
......@@ -114,7 +111,7 @@ class CategoryRenderer {
}
/**
* @param {!Element} element DOM node to populate with values.
* @param {Element} element DOM node to populate with values.
* @param {number|null} score
* @param {string} scoreDisplayMode
* @return {Element}
......@@ -126,7 +123,7 @@ class CategoryRenderer {
}
/**
* @param {CategoryJSON} category
* @param {LH.ReportResult.Category} category
* @return {Element}
*/
renderCategoryHeader(category) {
......@@ -149,7 +146,7 @@ class CategoryRenderer {
/**
* Renders the group container for a group of audits. Individual audit elements can be added
* directly to the returned element.
* @param {GroupJSON} group
* @param {LH.Result.ReportGroup} group
* @param {{expandable: boolean, itemCount?: number}} opts
* @return {Element}
*/
......@@ -235,8 +232,8 @@ class CategoryRenderer {
}
/**
* @param {Array<AuditJSON>} manualAudits
* @param {string} manualDescription
* @param {Array<LH.ReportResult.AuditRef>} manualAudits
* @param {string} [manualDescription]
* @return {Element}
*/
_renderManualAudits(manualAudits, manualDescription) {
......@@ -259,7 +256,7 @@ class CategoryRenderer {
}
/**
* @param {CategoryJSON} category
* @param {LH.ReportResult.Category} category
* @return {DocumentFragment}
*/
renderScoreGauge(category) {
......@@ -293,8 +290,8 @@ class CategoryRenderer {
}
/**
* @param {CategoryJSON} category
* @param {Object<string, GroupJSON>} groupDefinitions
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>} [groupDefinitions]
* @return {Element}
*/
render(category, groupDefinitions) {
......@@ -306,7 +303,7 @@ class CategoryRenderer {
const manualAudits = auditRefs.filter(audit => audit.result.scoreDisplayMode === 'manual');
const nonManualAudits = auditRefs.filter(audit => !manualAudits.includes(audit));
/** @type {Object<string, {passed: Array<AuditJSON>, failed: Array<AuditJSON>, notApplicable: Array<AuditJSON>}>} */
/** @type {Object<string, {passed: Array<LH.ReportResult.AuditRef>, failed: Array<LH.ReportResult.AuditRef>, notApplicable: Array<LH.ReportResult.AuditRef>}>} */
const auditsGroupedByGroup = {};
const auditsUngrouped = {passed: [], failed: [], notApplicable: []};
......@@ -339,14 +336,14 @@ class CategoryRenderer {
const passedElements = /** @type {Array<Element>} */ ([]);
const notApplicableElements = /** @type {Array<Element>} */ ([]);
auditsUngrouped.failed.forEach((/** @type {AuditJSON} */ audit, i) =>
failedElements.push(this.renderAudit(audit, i)));
auditsUngrouped.passed.forEach((/** @type {AuditJSON} */ audit, i) =>
passedElements.push(this.renderAudit(audit, i)));
auditsUngrouped.notApplicable.forEach((/** @type {AuditJSON} */ audit, i) =>
notApplicableElements.push(this.renderAudit(audit, i)));
auditsUngrouped.failed.forEach((audit, i) => failedElements.push(this.renderAudit(audit, i)));
auditsUngrouped.passed.forEach((audit, i) => passedElements.push(this.renderAudit(audit, i)));
auditsUngrouped.notApplicable.forEach((audit, i) => notApplicableElements.push(
this.renderAudit(audit, i)));
Object.keys(auditsGroupedByGroup).forEach(groupId => {
if (!groupDefinitions) return; // We never reach here if there aren't groups, but TSC needs convincing
const group = groupDefinitions[groupId];
const groups = auditsGroupedByGroup[groupId];
......
......@@ -9,6 +9,10 @@
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./crc-details-renderer.js')} CRCDetailsJSON */
/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
/** @type {Array<string>} */
const URL_PREFIXES = ['http://', 'https://', 'data:'];
class DetailsRenderer {
/**
......@@ -29,7 +33,7 @@ class DetailsRenderer {
}
/**
* @param {DetailsJSON} details
* @param {DetailsJSON|OpportunityDetails} details
* @return {Element}
*/
render(details) {
......@@ -55,13 +59,16 @@ class DetailsRenderer {
// @ts-ignore - TODO(bckenny): Fix type hierarchy
return this._renderTable(/** @type {TableDetailsJSON} */ (details));
case 'code':
return this._renderCode(details);
return this._renderCode(/** @type {DetailsJSON} */ (details));
case 'node':
return this.renderNode(/** @type {NodeDetailsJSON} */(details));
case 'criticalrequestchain':
return CriticalRequestChainRenderer.render(this._dom, this._templateContext,
// @ts-ignore - TODO(bckenny): Fix type hierarchy
/** @type {CRCDetailsJSON} */ (details));
case 'opportunity':
// @ts-ignore - TODO(bckenny): Fix type hierarchy
return this._renderOpportunityTable(details);
default: {
throw new Error(`Unknown type: ${details.type}`);
}
......@@ -69,17 +76,17 @@ class DetailsRenderer {
}
/**
* @param {NumericUnitDetailsJSON} details
* @param {{value: number, granularity?: number}} details
* @return {Element}
*/
_renderBytes(details) {
// TODO: handle displayUnit once we have something other than 'kb'
const value = Util.formatBytesToKB(details.value, details.granularity);
return this._renderText({type: 'text', value});
return this._renderText({value});
}
/**
* @param {NumericUnitDetailsJSON} details
* @param {{value: number, granularity?: number, displayUnit?: string}} details
* @return {Element}
*/
_renderMilliseconds(details) {
......@@ -88,11 +95,11 @@ class DetailsRenderer {
value = Util.formatDuration(details.value);
}
return this._renderText({type: 'text', value});
return this._renderText({value});
}
/**
* @param {StringDetailsJSON} text
* @param {{value: string}} text
* @return {HTMLElement}
*/
_renderTextURL(text) {
......@@ -107,7 +114,7 @@ class DetailsRenderer {
displayedHost = parsed.file === '/' ? '' : `(${parsed.hostname})`;
title = url;
} catch (/** @type {!Error} */ e) {
if (!(e instanceof TypeError)) {
if (!e.name.startsWith('TypeError')) {
throw e;
}
displayedPath = url;
......@@ -116,13 +123,11 @@ class DetailsRenderer {
const element = /** @type {HTMLElement} */ (this._dom.createElement('div', 'lh-text__url'));
element.appendChild(this._renderText({
value: displayedPath,
type: 'text',
}));
if (displayedHost) {
const hostElem = this._renderText({
value: displayedHost,
type: 'text',
});
hostElem.classList.add('lh-text__url-host');
element.appendChild(hostElem);
......@@ -142,7 +147,6 @@ class DetailsRenderer {
if (!allowedProtocols.includes(url.protocol)) {
// Fall back to just the link text if protocol not allowed.
return this._renderText({
type: 'text',
value: details.text,
});
}
......@@ -157,7 +161,7 @@ class DetailsRenderer {
}
/**
* @param {StringDetailsJSON} text
* @param {{value: string}} text
* @return {Element}
*/
_renderText(text) {
......@@ -169,13 +173,11 @@ class DetailsRenderer {
/**
* Create small thumbnail with scaled down image asset.
* If the supplied details doesn't have an image/* mimeType, then an empty span is returned.
* @param {ThumbnailDetails} details
* @param {{value: string}} details
* @return {Element}
*/
_renderThumbnail(details) {
const element = /** @type {HTMLImageElement}*/ (this._dom.createElement('img', 'lh-thumbnail'));
/** @type {string} */
// @ts-ignore - type should have a value if we get here.
const strValue = details.value;
element.src = strValue;
element.title = strValue;
......@@ -242,6 +244,82 @@ class DetailsRenderer {
return tableElem;
}
/**
* TODO(bckenny): migrate remaining table rendering to this function, then rename
* back to _renderTable and replace the original.
* @param {OpportunityDetails} details
* @return {Element}
*/
_renderOpportunityTable(details) {
if (!details.items.length) return this._dom.createElement('span');
const tableElem = this._dom.createElement('table', 'lh-table');
const theadElem = this._dom.createChildOf(tableElem, 'thead');
const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
for (const heading of details.headings) {
const valueType = heading.valueType || 'text';
const classes = `lh-table-column--${valueType}`;
const labelEl = this._dom.createElement('div', 'lh-text');
labelEl.textContent = heading.label;
this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(labelEl);
}
const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
for (const row of details.items) {
const rowElem = this._dom.createChildOf(tbodyElem, 'tr');
for (const heading of details.headings) {
const key = /** @type {keyof LH.Result.Audit.OpportunityDetailsItem} */ (heading.key);
const value = row[key];
if (typeof value === 'undefined' || value === null) {
this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
continue;
}
const valueType = heading.valueType;
let itemElement;
// TODO(bckenny): as we add more table types, split out into _renderTableItem fn.
switch (valueType) {
case 'url': {
// Fall back to <pre> rendering if not actually a URL.
const strValue = /** @type {string} */ (value);
if (URL_PREFIXES.some(prefix => strValue.startsWith(prefix))) {
itemElement = this._renderTextURL({value: strValue});
} else {
const codeValue = /** @type {(number|string|undefined)} */ (value);
itemElement = this._renderCode({value: codeValue});
}
break;
}
case 'timespanMs': {
const numValue = /** @type {number} */ (value);
itemElement = this._renderMilliseconds({value: numValue});
break;
}
case 'bytes': {
const numValue = /** @type {number} */ (value);
itemElement = this._renderBytes({value: numValue, granularity: 1});
break;
}
case 'thumbnail': {
const strValue = /** @type {string} */ (value);
itemElement = this._renderThumbnail({value: strValue});
break;
}
default: {
throw new Error(`Unknown valueType: ${valueType}`);
}
}
const classes = `lh-table-column--${valueType}`;
this._dom.createChildOf(rowElem, 'td', classes).appendChild(itemElement);
}
}
return tableElem;
}
/**
* @param {NodeDetailsJSON} item
* @return {Element}
......@@ -279,7 +357,7 @@ class DetailsRenderer {
}
/**
* @param {DetailsJSON} details
* @param {{value?: string|number}} details
* @return {Element}
*/
_renderCode(details) {
......@@ -300,7 +378,6 @@ if (typeof module !== 'undefined' && module.exports) {
* @typedef {{
type: string,
value: (string|number|undefined),
summary?: OpportunitySummary,
granularity?: number,
displayUnit?: string
}} DetailsJSON
......@@ -352,7 +429,7 @@ if (typeof module !== 'undefined' && module.exports) {
/** @typedef {{
type: string,
value?: string,
value: string,
}} ThumbnailDetails
*/
......@@ -369,10 +446,3 @@ if (typeof module !== 'undefined' && module.exports) {
items: Array<{timing: number, timestamp: number, data: string}>,
}} FilmstripDetails
*/
/** @typedef {{
wastedMs?: number,
wastedBytes?: number
}} OpportunitySummary
*/
......@@ -8,15 +8,12 @@
/* 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 */
/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* @param {AuditJSON} audit
* @param {LH.ReportResult.AuditRef} audit
* @return {Element}
*/
_renderMetric(audit) {
......@@ -46,7 +43,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
}
/**
* @param {AuditJSON} audit
* @param {LH.ReportResult.AuditRef} audit
* @param {number} index
* @param {number} scale
* @return {Element}
......@@ -56,20 +53,20 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
const element = this.populateAuditValues(audit, index, oppTmpl);
element.id = audit.result.id;
const details = audit.result.details;
if (!details) {
if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
return element;
}
const summaryInfo = /** @type {OpportunitySummary} */ (details.summary);
if (!summaryInfo || !summaryInfo.wastedMs || audit.result.scoreDisplayMode === 'error') {
// TODO(bckenny): remove cast when details is fully discriminated based on `type`.
const details = /** @type {OpportunityDetails} */ (audit.result.details);
if (details.type !== 'opportunity') {
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}%`;
const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
displayEl.textContent = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
displayEl.textContent = Util.formatSeconds(details.overallSavingsMs, 0.01);
// Set [title] tooltips
if (audit.result.displayValue) {
......@@ -83,26 +80,27 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* 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
* Opportunties with an error won't have a details object, so MIN_VALUE is returned to keep any
* erroring opportunities last in sort order.
* @param {AuditJSON} audit
* @param {LH.ReportResult.AuditRef} 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;
if (audit.result.details && audit.result.details.type === 'opportunity') {
// TODO(bckenny): remove cast when details is fully discriminated based on `type`.
const details = /** @type {OpportunityDetails} */ (audit.result.details);
if (typeof details.overallSavingsMs !== 'number') {
throw new Error('non-opportunity details passed to _getWastedMs');
}
return details.overallSavingsMs;
} else {
return Number.MIN_VALUE;
}
}
/**
* @param {CategoryJSON} category
* @param {Object<string, GroupJSON>} groups
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>} groups
* @return {Element}
* @override
*/
......
......@@ -29,12 +29,12 @@ class ReportRenderer {
}
/**
* @param {ReportJSON} report
* @param {LH.ReportResult} report
* @param {Element} container Parent element to render the report into.
*/
renderReport(report, container) {
// If any mutations happen to the report within the renderers, we want the original object untouched
const clone = /** @type {ReportJSON} */ (JSON.parse(JSON.stringify(report)));
const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(report)));
// TODO(phulce): we all agree this is technical debt we should fix
if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
......@@ -56,7 +56,7 @@ class ReportRenderer {
}
/**
* @param {ReportJSON} report
* @param {LH.ReportResult} report
* @return {DocumentFragment}
*/
_renderReportHeader(report) {
......@@ -90,7 +90,7 @@ class ReportRenderer {
/**
* @param {ReportJSON} report
* @param {LH.ReportResult} report
* @return {DocumentFragment}
*/
_renderReportFooter(report) {
......@@ -117,7 +117,7 @@ class ReportRenderer {
/**
* Returns a div with a list of top-level warnings, or an empty div if no warnings.
* @param {ReportJSON} report
* @param {LH.ReportResult} report
* @return {Node}
*/
_renderReportWarnings(report) {
......@@ -136,7 +136,7 @@ class ReportRenderer {
}
/**
* @param {ReportJSON} report
* @param {LH.ReportResult} report
* @return {DocumentFragment}
*/
_renderReport(report) {
......@@ -203,7 +203,7 @@ class ReportRenderer {
/**
* Place the AuditResult into the auditDfn (which has just weight & group)
* @param {Object<string, LH.Audit.Result>} audits
* @param {Array<CategoryJSON>} reportCategories
* @param {Array<LH.ReportResult.Category>} reportCategories
*/
static smooshAuditResultsIntoCategories(audits, reportCategories) {
for (const category of reportCategories) {
......@@ -220,49 +220,3 @@ if (typeof module !== 'undefined' && module.exports) {
} else {
self.ReportRenderer = ReportRenderer;
}
/**
* @typedef {{
id: string,
score: (number|null),
weight: number,
group?: string,
result: LH.Audit.Result
}} AuditJSON
*/
/**
* @typedef {{
title: string,
id: string,
score: (number|null),
description?: string,
manualDescription: string,
auditRefs: Array<AuditJSON>
}} CategoryJSON
*/
/**
* @typedef {{
title: string,
description?: string,
}} GroupJSON
*/
/**
* @typedef {{
lighthouseVersion: string,
userAgent: string,
fetchTime: string,
timing: {total: number},
requestedUrl: string,
finalUrl: string,
runWarnings?: Array<string>,
artifacts: {traces: {defaultPass: {traceEvents: Array}}},
audits: Object<string, LH.Audit.Result>,
categories: Object<string, CategoryJSON>,
reportCategories: Array<CategoryJSON>,
categoryGroups: Object<string, GroupJSON>,
configSettings: LH.Config.Settings,
}} ReportJSON
*/
......@@ -127,10 +127,10 @@
scroll-behavior: smooth;
}
.lh-root [data-keyboard-focus="true"]:focus {
.lh-root :focus {
outline: -webkit-focus-ring-color auto 3px;
}
.lh-root summary[data-keyboard-focus="true"]:focus {
.lh-root summary:focus {
outline: 1px solid hsl(217, 89%, 61%);
}
......@@ -864,6 +864,7 @@ summary.lh-passed-audits-summary {
.lh-table-column--text,
.lh-table-column--bytes,
.lh-table-column--timespanMs,
.lh-table-column--ms {
text-align: right;
}
......
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