diff --git a/lighthouse-core/closure/closure-type-checking.js b/lighthouse-core/closure/closure-type-checking.js index ffe6f7f6015f..c0e42afd0dfb 100755 --- a/lighthouse-core/closure/closure-type-checking.js +++ b/lighthouse-core/closure/closure-type-checking.js @@ -32,7 +32,7 @@ gulp.task('compile-report', () => { return gulp.src([ // externs 'closure/third_party/commonjs.js', - + 'lib/filer-namer.js', 'report/v2/renderer/*.js', ]) diff --git a/lighthouse-core/lib/file-namer.js b/lighthouse-core/lib/file-namer.js index 97ae75ebec7e..88313e0c7e1c 100644 --- a/lighthouse-core/lib/file-namer.js +++ b/lighthouse-core/lib/file-namer.js @@ -20,7 +20,7 @@ * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS * Date/time uses the local timezone, however Node has unreliable ICU * support, so we must construct a YYYY-MM-DD date format manually. :/ - * @param {!Object} results + * @param {!{url: string, generatedTime: string}} results * @returns string */ function getFilenamePrefix(results) { diff --git a/lighthouse-core/report/scripts/logger.js b/lighthouse-core/report/scripts/logger.js index acf04ba7c85a..2c978fe51b25 100644 --- a/lighthouse-core/report/scripts/logger.js +++ b/lighthouse-core/report/scripts/logger.js @@ -21,22 +21,20 @@ /** * Logs messages via a UI butter. - * @class */ class Logger { constructor(selector) { + /** @type {!Element} */ this.el = document.querySelector(selector); } /** * Shows a butter bar. * @param {!string} msg The message to show. - * @param {boolean=} optAutoHide True to hide the message after a duration. + * @param {boolean=} autoHide True to hide the message after a duration. * Default is true. */ - log(msg, optAutoHide) { - const autoHide = typeof optAutoHide === 'undefined' ? true : optAutoHide; - + log(msg, autoHide = true) { clearTimeout(this._id); this.el.textContent = msg; diff --git a/lighthouse-core/report/v2/renderer/category-renderer.js b/lighthouse-core/report/v2/renderer/category-renderer.js index eb5a75a740d2..fa662daf03ad 100644 --- a/lighthouse-core/report/v2/renderer/category-renderer.js +++ b/lighthouse-core/report/v2/renderer/category-renderer.js @@ -15,37 +15,7 @@ */ 'use strict'; -/* globals self */ - -const RATINGS = { - PASS: {label: 'pass', minScore: 75}, - AVERAGE: {label: 'average', minScore: 45}, - FAIL: {label: 'fail'} -}; - -/** - * Convert a score to a rating label. - * @param {number} score - * @return {string} - */ -function calculateRating(score) { - let rating = RATINGS.FAIL.label; - if (score >= RATINGS.PASS.minScore) { - rating = RATINGS.PASS.label; - } else if (score >= RATINGS.AVERAGE.minScore) { - rating = RATINGS.AVERAGE.label; - } - return rating; -} - -/** - * Format number. - * @param {number} number - * @return {string} - */ -function formatNumber(number) { - return number.toLocaleString(undefined, {maximumFractionDigits: 1}); -} +/* globals self, Util */ class CategoryRenderer { /** @@ -105,8 +75,8 @@ class CategoryRenderer { _populateScore(element, score, scoringMode, title, description) { // Fill in the blanks. const valueEl = this._dom.find('.lh-score__value', element); - valueEl.textContent = formatNumber(score); - valueEl.classList.add(`lh-score__value--${calculateRating(score)}`, + valueEl.textContent = Util.formatNumber(score); + valueEl.classList.add(`lh-score__value--${Util.calculateRating(score)}`, `lh-score__value--${scoringMode}`); this._dom.find('.lh-score__title', element).textContent = title; @@ -157,7 +127,7 @@ class CategoryRenderer { const gauge = this._dom.find('.lh-gauge', tmpl); gauge.setAttribute('data-progress', score); // .dataset not supported in jsdom. - gauge.classList.add(`lh-gauge--${calculateRating(score)}`); + gauge.classList.add(`lh-gauge--${Util.calculateRating(score)}`); this._dom.findAll('.lh-gauge__fill', gauge).forEach(el => { el.style.transform = `rotate(${fillRotation}deg)`; diff --git a/lighthouse-core/report/v2/renderer/dom.js b/lighthouse-core/report/v2/renderer/dom.js index f55210fea6ac..412087a94d5e 100644 --- a/lighthouse-core/report/v2/renderer/dom.js +++ b/lighthouse-core/report/v2/renderer/dom.js @@ -50,7 +50,7 @@ class DOM { /** * @param {string} selector - * @param {!Document|!Element} context + * @param {!Node} context * @return {!DocumentFragment} A clone of the template content. * @throws {Error} */ @@ -73,6 +73,15 @@ class DOM { return clone; } + /** + * Resets the "stamped" state of the templates. + */ + resetTemplates() { + this.findAll('template[data-stamped]', this._document).forEach(t => { + t.removeAttribute('data-stamped'); + }); + } + /** * @param {string} text * @return {!Element} @@ -113,7 +122,7 @@ class DOM { * Guaranteed context.querySelector. Always returns an element or throws if * nothing matches query. * @param {string} query - * @param {!DocumentFragment|!Element} context + * @param {!Node} context * @return {!Element} */ find(query, context) { @@ -127,8 +136,8 @@ class DOM { /** * Helper for context.querySelectorAll. Returns an Array instead of a NodeList. * @param {string} query - * @param {!DocumentFragment|!Element} context - * @return {!Array} + * @param {!Node} context + * @return {!Array} */ findAll(query, context) { return Array.from(context.querySelectorAll(query)); diff --git a/lighthouse-core/report/v2/renderer/logger.js b/lighthouse-core/report/v2/renderer/logger.js new file mode 100644 index 000000000000..48e543e47d6f --- /dev/null +++ b/lighthouse-core/report/v2/renderer/logger.js @@ -0,0 +1,77 @@ +/** + * @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'; + +/** + * Logs messages via a UI butter. + */ +class Logger { + /** + * @param {!Element} element + */ + constructor(element) { + /** @type {!Element} */ + this.el = element; + /** @private {?number} */ + this._id = null; + } + + /** + * Shows a butter bar. + * @param {!string} msg The message to show. + * @param {boolean=} autoHide True to hide the message after a duration. + * Default is true. + */ + log(msg, autoHide = true) { + clearTimeout(this._id); + + this.el.textContent = msg; + this.el.classList.add('show'); + if (autoHide) { + this._id = setTimeout(_ => { + this.el.classList.remove('show'); + }, 7000); + } + } + + /** + * @param {string} msg + */ + warn(msg) { + this.log('Warning: ' + msg); + } + + /** + * @param {string} msg + */ + error(msg) { + this.log(msg); + } + + /** + * Explicitly hides the butter bar. + */ + hide() { + clearTimeout(this._id); + this.el.classList.remove('show'); + } +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = Logger; +} diff --git a/lighthouse-core/report/v2/renderer/report-features.js b/lighthouse-core/report/v2/renderer/report-features.js new file mode 100644 index 000000000000..50f503e9de74 --- /dev/null +++ b/lighthouse-core/report/v2/renderer/report-features.js @@ -0,0 +1,292 @@ +/** + * 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'; + +/** + * @fileoverview Adds export button, print, and other dynamic functionality to + * the report. + */ + +/* globals self URL Blob CustomEvent */ + +class ReportUIFeatures { + + /** + * @param {!DOM} dom + */ + constructor(dom) { + /** @type {!ReportRenderer.ReportJSON} */ + this.json; // eslint-disable-line no-unused-expressions + /** @private {!DOM} */ + this._dom = dom; + /** @private {!Document} */ + this._document = this._dom.document(); + /** @private {boolean} */ + this._copyAttempt = false; + /** @type {!Element} **/ + this.exportButton; // eslint-disable-line no-unused-expressions + + this.onCopy = this.onCopy.bind(this); + this.onExportButtonClick = this.onExportButtonClick.bind(this); + this.onExport = this.onExport.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.printShortCutDetect = this.printShortCutDetect.bind(this); + } + + /** + * Adds export button, print, and other functionality to the report. The method + * should be called whenever the report needs to be re-rendered. + * @param {!ReportRenderer.ReportJSON} report + */ + initFeatures(report) { + this.json = report; + this._setupExportButton(); + this._setUpCollapseDetailsAfterPrinting(); + this._resetUIState(); + this._document.addEventListener('keydown', this.printShortCutDetect); + this._document.addEventListener('copy', this.onCopy); + } + + /** + * Fires a custom DOM event on target. + * @param {string} name Name of the event. + * @param {!Node=} target DOM node to fire the event on. + * @param {Object<{detail: Object}>=} detail Custom data to include. + */ + _fireEventOn(name, target = this._document, detail) { + const event = new CustomEvent(name, detail ? {detail} : null); + this._document.dispatchEvent(event); + } + + _setupExportButton() { + this.exportButton = this._dom.find('.lh-export__button', this._document); + this.exportButton.addEventListener('click', this.onExportButtonClick); + + const dropdown = this._dom.find('.lh-export__dropdown', this._document); + dropdown.addEventListener('click', this.onExport); + } + + /** + * Handler copy events. + * @param {!Event} e + */ + onCopy(e) { + // Only handle copy button presses (e.g. ignore the user copying page text). + if (this._copyAttempt) { + // We want to write our own data to the clipboard, not the user's text selection. + e.preventDefault(); + e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2)); + + this._fireEventOn('lh-log', this._document, { + cmd: 'log', msg: 'Report JSON copied to clipboard' + }); + } + + this._copyAttempt = false; + } + + /** + * Copies the report JSON to the clipboard (if supported by the browser). + * @suppress {reportUnknownTypes} + */ + onCopyButtonClick() { + this._fireEventOn('lh-analytics', this._document, { + cmd: 'send', + fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'} + }); + + try { + if (this._document.queryCommandSupported('copy')) { + this._copyAttempt = true; + + // Note: In Safari 10.0.1, execCommand('copy') returns true if there's + // a valid text selection on the page. See http://caniuse.com/#feat=clipboard. + if (!this._document.execCommand('copy')) { + this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt. + + this._fireEventOn('lh-log', this._document, { + cmd: 'warn', msg: 'Your browser does not support copy to clipboard.' + }); + } + } + } catch (/** @type {!Error} */ e) { + this._copyAttempt = false; + this._fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message}); + } + } + + closeExportDropdown() { + this.exportButton.classList.remove('active'); + } + + /** + * Click handler for export button. + * @param {!Event} e + */ + onExportButtonClick(e) { + e.preventDefault(); + const el = /** @type {!Element} */ (e.target); + el.classList.toggle('active'); + this._document.addEventListener('keydown', this.onKeyDown); + } + + /** + * Resets the state of page before capturing the page for export. + * When the user opens the exported HTML page, certain UI elements should + * be in their closed state (not opened) and the templates should be unstamped. + */ + _resetUIState() { + this.closeExportDropdown(); + this._dom.resetTemplates(); + } + + /** + * Handler for "export as" button. + * @param {!Event} e + */ + onExport(e) { + e.preventDefault(); + + const el = /** @type {!Element} */ (e.target); + + if (!el.hasAttribute('data-action')) { + return; + } + + switch (el.getAttribute('data-action')) { + case 'copy': + this.onCopyButtonClick(); + break; + case 'print': + this.expandAllDetails(); + self.print(); + break; + case 'save-json': { + const jsonStr = JSON.stringify(this.json, null, 2); + this._saveFile(new Blob([jsonStr], {type: 'application/json'})); + break; + } + case 'save-html': { + this._resetUIState(); + + const htmlStr = this._document.documentElement.outerHTML; + try { + this._saveFile(new Blob([htmlStr], {type: 'text/html'})); + } catch (/** @type {!Error} */ e) { + this._fireEventOn('lh-log', this._document, { + cmd: 'error', msg: 'Could not export as HTML. ' + e.message + }); + } + break; + } + } + + this.closeExportDropdown(); + this._document.removeEventListener('keydown', this.onKeyDown); + } + + /** + * Keydown handler for the document. + * @param {!Event} e + */ + onKeyDown(e) { + if (e.keyCode === 27) { // ESC + this.closeExportDropdown(); + } + } + + /** + * Expands audit details when user prints via keyboard shortcut. + * @param {!Event} e + */ + printShortCutDetect(e) { + if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) { // Ctrl+P + this.expandAllDetails(); + } + } + + /** + * Expands all audit `
`. + * Ideally, a print stylesheet could take care of this, but CSS has no way to + * open a `
` element. + */ + expandAllDetails() { + const details = this._dom.findAll('.lh-categories details', this._document); + details.map(detail => detail.open = true); + } + + /** + * Collapses all audit `
`. + * open a `
` element. + */ + collapseAllDetails() { + const details = this._dom.findAll('.lh-categories details', this._document); + details.map(detail => detail.open = false); + } + + /** + * Sets up listeners to collapse audit `
` when the user closes the + * print dialog, all `
` are collapsed. + */ + _setUpCollapseDetailsAfterPrinting() { + // FF and IE implement these old events. + if ('onbeforeprint' in self) { + self.addEventListener('afterprint', this.collapseAllDetails); + } else { + // Note: FF implements both window.onbeforeprint and media listeners. However, + // it doesn't matchMedia doesn't fire when matching 'print'. + self.matchMedia('print').addListener(mql => { + if (mql.matches) { + this.expandAllDetails(); + } else { + this.collapseAllDetails(); + } + }); + } + } + /** + * Downloads a file (blob) using a[download]. + * @param {!Blob|!File} blob The file to save. + */ + _saveFile(blob) { + const filename = self.getFilenamePrefix({ + url: this.json.url, + generatedTime: this.json.generatedTime + }); + + const ext = blob.type.match('json') ? '.json' : '.html'; + const href = URL.createObjectURL(blob); + + const a = /** @type {!HTMLAnchorElement} */ (this._dom.createElement('a')); + a.download = `${filename}${ext}`; + a.href = href; + this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM. + a.click(); + + // cleanup. + this._document.body.removeChild(a); + setTimeout(_ => URL.revokeObjectURL(href), 500); + } +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = ReportUIFeatures; +} else { + self.ReportUIFeatures = ReportUIFeatures; + + /** @type {function({url: string, generatedTime: string}): string} */ + self.getFilenamePrefix; // eslint-disable-line no-unused-expressions +} diff --git a/lighthouse-core/report/v2/renderer/report-renderer.js b/lighthouse-core/report/v2/renderer/report-renderer.js index 3d58109733bc..b7f63bd462a8 100644 --- a/lighthouse-core/report/v2/renderer/report-renderer.js +++ b/lighthouse-core/report/v2/renderer/report-renderer.js @@ -22,30 +22,47 @@ * Dummy text for ensuring report robustness: pre$`post %%LIGHTHOUSE_JSON%% */ -/* globals self */ +/* globals self, Util */ class ReportRenderer { /** * @param {!DOM} dom * @param {!CategoryRenderer} categoryRenderer + * @param {ReportUIFeatures=} uiFeatures */ - constructor(dom, categoryRenderer) { + constructor(dom, categoryRenderer, uiFeatures = null) { /** @private {!DOM} */ this._dom = dom; /** @private {!CategoryRenderer} */ this._categoryRenderer = categoryRenderer; + /** @private {!Document|!Element} */ + this._templateContext = this._dom.document(); + /** @private {ReportUIFeatures} */ + this._uiFeatures = uiFeatures; } /** * @param {!ReportRenderer.ReportJSON} report - * @return {!Element} + * @param {!Element} container Parent element to render the report into. */ - renderReport(report) { + renderReport(report, container) { + container.textContent = ''; // Remove previous report. + + let element; try { - return this._renderReport(report); + element = container.appendChild(this._renderReport(report)); + + // Hook in JS features and page-level event listeners after the report + // is in the document. + if (this._uiFeatures) { + this._uiFeatures.initFeatures(report); + } } catch (/** @type {!Error} */ e) { - return this._renderException(e); + container.textContent = ''; + element = container.appendChild(this._renderException(e)); } + + return /** @type {!Element} **/ (element); } /** @@ -54,6 +71,7 @@ class ReportRenderer { * @param {!Document|!Element} context */ setTemplateContext(context) { + this._templateContext = context; this._categoryRenderer.setTemplateContext(context); } @@ -67,19 +85,93 @@ class ReportRenderer { return element; } + /** + * @param {!ReportRenderer.ReportJSON} report + * @return {!DocumentFragment} + */ + _renderReportHeader(report) { + const header = this._dom.cloneTemplate('#tmpl-lh-heading', this._templateContext); + this._dom.find('.lh-config__timestamp', header).textContent = + Util.formatDateTime(report.generatedTime); + const url = this._dom.find('.lh-metadata__url', header); + url.href = report.url; + url.textContent = report.url; + + const env = this._dom.find('.lh-env__items', header); + report.runtimeConfig.environment.forEach(runtime => { + const item = this._dom.cloneTemplate('#tmpl-lh-env__items', env); + this._dom.find('.lh-env__name', item).textContent = runtime.name; + this._dom.find('.lh-env__description', item).textContent = runtime.description; + this._dom.find('.lh-env__enabled', item).textContent = + runtime.enabled ? 'Enabled' : 'Disabled'; + env.appendChild(item); + }); + + return header; + } + + /** + * @param {!ReportRenderer.ReportJSON} report + * @return {!DocumentFragment} + */ + _renderReportFooter(report) { + const footer = this._dom.cloneTemplate('#tmpl-lh-footer', this._templateContext); + this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion; + this._dom.find('.lh-footer__timestamp', footer).textContent = + Util.formatDateTime(report.generatedTime); + return footer; + } + + /** + * @param {!ReportRenderer.ReportJSON} report + * @return {!DocumentFragment} + */ + _renderReportNav(report) { + const leftNav = this._dom.cloneTemplate('#tmpl-lh-leftnav', this._templateContext); + + this._dom.find('.leftnav__header__version', leftNav).textContent = + `Version: ${report.lighthouseVersion}`; + + const nav = this._dom.find('.lh-leftnav', leftNav); + for (const category of report.reportCategories) { + const itemsTmpl = this._dom.cloneTemplate('#tmpl-lh-leftnav__items', leftNav); + + const navItem = this._dom.find('.lh-leftnav__item', itemsTmpl); + navItem.href = `#${category.id}`; + + this._dom.find('.leftnav-item__category', navItem).textContent = category.name; + const score = this._dom.find('.leftnav-item__score', navItem); + score.classList.add(`lh-score__value--${Util.calculateRating(category.score)}`); + score.textContent = Math.round(Util.formatNumber(category.score)); + nav.appendChild(navItem); + } + return leftNav; + } + /** * @param {!ReportRenderer.ReportJSON} report * @return {!Element} */ _renderReport(report) { - const element = this._dom.createElement('div', 'lh-report'); - const scoreHeader = element.appendChild( + const container = this._dom.createElement('div', 'lh-container'); + + container.appendChild(this._renderReportHeader(report)); // sticky header goes at the top. + container.appendChild(this._renderReportNav(report)); + + const reportSection = container.appendChild(this._dom.createElement('div', 'lh-report')); + + const scoreHeader = reportSection.appendChild( this._dom.createElement('div', 'lh-scores-header')); + + const categories = reportSection.appendChild(this._dom.createElement('div', 'lh-categories')); for (const category of report.reportCategories) { scoreHeader.appendChild(this._categoryRenderer.renderScoreGauge(category)); - element.appendChild(this._categoryRenderer.render(category)); + categories.appendChild(this._categoryRenderer.render(category)); } - return element; + + reportSection.appendChild(this._renderReportFooter(report)); + + return container; } } @@ -126,7 +218,11 @@ ReportRenderer.CategoryJSON; // eslint-disable-line no-unused-expressions * generatedTime: string, * initialUrl: string, * url: string, - * reportCategories: !Array + * reportCategories: !Array, + * runtimeConfig: { + * blockedUrlPatterns: !Array, + * environment: !Array<{description: string, enabled: boolean, name: string}> + * } * }} */ ReportRenderer.ReportJSON; // eslint-disable-line no-unused-expressions diff --git a/lighthouse-core/report/v2/renderer/util.js b/lighthouse-core/report/v2/renderer/util.js new file mode 100644 index 000000000000..d92c65c83161 --- /dev/null +++ b/lighthouse-core/report/v2/renderer/util.js @@ -0,0 +1,78 @@ +/** + * 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'; + +/* globals self */ + +const RATINGS = { + PASS: {label: 'pass', minScore: 75}, + AVERAGE: {label: 'average', minScore: 45}, + FAIL: {label: 'fail'} +}; + +class Util { + /** + * Convert a score to a rating label. + * @param {number} score + * @return {string} + */ + static calculateRating(score) { + let rating = RATINGS.FAIL.label; + if (score >= RATINGS.PASS.minScore) { + rating = RATINGS.PASS.label; + } else if (score >= RATINGS.AVERAGE.minScore) { + rating = RATINGS.AVERAGE.label; + } + return rating; + } + + /** + * Format number. + * @param {number} number + * @return {string} + */ + static formatNumber(number) { + return number.toLocaleString(undefined, {maximumFractionDigits: 1}); + } + + /** + * Format time. + * @param {string} date + * @return {string} + */ + static formatDateTime(date) { + const options = { + month: 'short', day: 'numeric', year: 'numeric', + hour: 'numeric', minute: 'numeric', timeZoneName: 'short' + }; + let formatter = new Intl.DateTimeFormat('en-US', options); + + // Force UTC if runtime timezone could not be detected. + // See https://github.com/GoogleChrome/lighthouse/issues/1056 + const tz = formatter.resolvedOptions().timeZone; + if (!tz || tz.toLowerCase() === 'etc/unknown') { + options.timeZone = 'UTC'; + formatter = new Intl.DateTimeFormat('en-US', options); + } + return formatter.format(new Date(date)); + } +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = Util; +} else { + self.Util = Util; +} diff --git a/lighthouse-core/report/v2/report-generator.js b/lighthouse-core/report/v2/report-generator.js index 8b960ac82f00..1124c423807e 100644 --- a/lighthouse-core/report/v2/report-generator.js +++ b/lighthouse-core/report/v2/report-generator.js @@ -20,8 +20,12 @@ const fs = require('fs'); const REPORT_TEMPLATE = fs.readFileSync(__dirname + '/report-template.html', 'utf8'); const REPORT_JAVASCRIPT = [ + fs.readFileSync(__dirname + '/renderer/util.js', 'utf8'), fs.readFileSync(__dirname + '/renderer/dom.js', 'utf8'), fs.readFileSync(__dirname + '/renderer/details-renderer.js', 'utf8'), + fs.readFileSync(__dirname + '/../../lib/file-namer.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/logger.js', 'utf8'), + fs.readFileSync(__dirname + '/renderer/report-features.js', 'utf8'), fs.readFileSync(__dirname + '/renderer/category-renderer.js', 'utf8'), fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'), ].join(';\n'); diff --git a/lighthouse-core/report/v2/report-styles.css b/lighthouse-core/report/v2/report-styles.css index 1bde530f6d6c..b73641881f01 100644 --- a/lighthouse-core/report/v2/report-styles.css +++ b/lighthouse-core/report/v2/report-styles.css @@ -17,6 +17,7 @@ :root { --text-font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif; --body-font-size: 13px; + --header-font-size: 16px; --body-line-height: 1.5; --default-padding: 16px; @@ -27,7 +28,11 @@ --average-color: #ef6c00; /* md orange 800 */ --warning-color: #757575; /* md grey 600 */ - --report-border-color: #ebebeb; + --report-border-color: #ccc; + --report-secondary-border-color: #ebebeb; + --report-width: 850px; + --report-menu-width: 280px; + --report-content-width: calc(var(--report-width) + var(--report-menu-width)); --lh-score-highlight-bg: #fafafa; --lh-score-icon-background-size: 17px; @@ -45,6 +50,7 @@ body { font-size: var(--body-font-size); margin: 0; line-height: var(--body-line-height); + background: #f5f5f5; scroll-behavior: smooth; } @@ -52,20 +58,58 @@ body { display: none !important; } -.lh-details { - font-size: smaller; - margin-top: var(--default-padding); +a { + color: #0c50c7; } -.lh-details summary { +summary { cursor: pointer; } +.lh-details { + font-size: smaller; + margin-top: var(--default-padding); +} + .lh-details[open] summary { margin-bottom: var(--default-padding); } +/* Report header */ + +.report-icon { + opacity: 0.7; +} +.report-icon:hover { + opacity: 1; +} +.report-icon[disabled] { + opacity: 0.3; + pointer-events: none; +} + +.report-icon--share { + background-image: url('data:image/svg+xml;utf8,'); +} +.report-icon--print { + background-image: url('data:image/svg+xml;utf8,'); +} +.report-icon--copy { + background-image: url('data:image/svg+xml;utf8,'); +} +.report-icon--open { + background-image: url('data:image/svg+xml;utf8,'); +} +.report-icon--download { + background-image: url('data:image/svg+xml;utf8,'); +} + /* List */ +.lh-list { + font-size: smaller; + margin-top: var(--default-padding); +} + .lh-list__items { padding-left: var(--default-padding); } @@ -202,26 +246,27 @@ body { margin-top: 2px; } -.lh-score__header[open] .lh-score__arrow { +.lh-score__header[open] .lh-toggle-arrow { transform: rotateZ(90deg); } -.lh-score__arrow { +.lh-toggle-arrow { background: url('data:image/svg+xml;utf8,') no-repeat 50% 50%; background-size: contain; background-color: transparent; width: 24px; height: 24px; flex: none; - margin: 0 8px 0 8px; + margin-left: calc(var(--default-padding) / 2); transition: transform 150ms ease-in-out; + cursor: pointer; + border: none; } .lh-score__snippet { display: flex; align-items: center; justify-content: space-between; - cursor: pointer; /*outline: none;*/ } @@ -245,6 +290,7 @@ body { .lh-audit > .lh-score { font-size: 16px; + font-size: var(--header-font-size); } .lh-debug { @@ -255,27 +301,37 @@ body { /* Report */ -.lh-exception { - font-size: large; +.lh-container { + display: flex; + max-width: var(--report-content-width); + margin: 0 auto; + /*border-right: 1px solid var(--report-border-color);*/ + /*border-left: 1px solid var(--report-border-color);*/ } .lh-report { - padding: var(--default-padding); + margin-left: var(--report-menu-width); + background-color: #fff; +} + +.lh-exception { + font-size: large; } .lh-scores-header { display: flex; - margin: var(--default-padding) 0 calc(var(--default-padding) * 2) 0; + margin: var(--report-header-height) 0 0 0; + padding: calc(var(--default-padding) * 2) var(--default-padding); + border-bottom: 1px solid var(--report-border-color); } .lh-category { - padding: 24px 0; + padding: 24px calc(var(--default-padding) * 2); border-top: 1px solid var(--report-border-color); } .lh-category:first-of-type { border: none; - padding-top: 0; } .lh-category > .lh-audit, @@ -302,16 +358,70 @@ body { } summary.lh-passed-audits-summary { - margin: 10px 5px; + margin: var(--default-padding); font-size: 15px; + display: flex; + align-items: center; } summary.lh-passed-audits-summary::-webkit-details-marker { - background: rgb(66, 175, 69); + background: var(--pass-color); color: white; - position:relative; + position: relative; content: ''; - padding: 3px; + padding: 3px 3px 3px 6px; + border-radius: 2px; +} + +.lh-passed-audits[open] summary.lh-passed-audits-summary::-webkit-details-marker { + padding: 3px 5px 3px 4px; +} + +#lh-log { + position: fixed; + background-color: #323232; + color: #fff; + min-height: 48px; + min-width: 288px; + padding: 16px 24px; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); + border-radius: 2px; + margin: 12px; + font-size: 14px; + cursor: default; + transition: transform 0.3s, opacity 0.3s; + transform: translateY(100px); + opacity: 0; + -webkit-font-smoothing: antialiased; + bottom: 0; + left: 0; + z-index: 3; +} + +#lh-log.show { + opacity: 1; + transform: translateY(0); +} + +@media screen and (max-width: 767px) { + .lh-report { + margin-left: 0; + } + .lh-category { + padding: 24px var(--default-padding); + } +} + +@media print { + body { + -webkit-print-color-adjust: exact; /* print background colors */ + } + .lh-report { + margin-left: 0; + } + .lh-categories { + margin-top: 0; + } } /*# sourceURL=report.styles.css */ diff --git a/lighthouse-core/report/v2/report-template.html b/lighthouse-core/report/v2/report-template.html index adf0b45f2e6e..400e6f35f080 100644 --- a/lighthouse-core/report/v2/report-template.html +++ b/lighthouse-core/report/v2/report-template.html @@ -27,15 +27,49 @@ + +
+ +
+ diff --git a/lighthouse-core/report/v2/templates.html b/lighthouse-core/report/v2/templates.html index 709bca895d35..f58a31242c2d 100644 --- a/lighthouse-core/report/v2/templates.html +++ b/lighthouse-core/report/v2/templates.html @@ -18,13 +18,315 @@
-
+
+ + + + + + + + +