Skip to content

Commit

Permalink
Consolidate viewer functionality into main report (#1594)
Browse files Browse the repository at this point in the history
  • Loading branch information
ebidel authored and brendankenny committed Feb 9, 2017
1 parent e7c6c50 commit 9ac57cb
Show file tree
Hide file tree
Showing 20 changed files with 372 additions and 295 deletions.
4 changes: 2 additions & 2 deletions lighthouse-cli/bin.ts
Expand Up @@ -22,6 +22,7 @@ const _RUNTIME_ERROR_CODE = 1;
const _PROTOCOL_TIMEOUT_EXIT_CODE = 67;

const assetSaver = require('../lighthouse-core/lib/asset-saver.js');
const getFilenamePrefix = require('../lighthouse-core/lib/file-namer.js').getFilenamePrefix;
import {ChromeLauncher} from './chrome-launcher';
import * as Commands from './commands/commands';
const lighthouse = require('../lighthouse-core');
Expand Down Expand Up @@ -257,8 +258,7 @@ function saveResults(results: Results,
// Use the output path as the prefix for all generated files.
// If no output path is set, generate a file prefix using the URL and date.
const configuredPath = !flags.outputPath || flags.outputPath === 'stdout' ?
assetSaver.getFilenamePrefix(results) :
flags.outputPath.replace(/\.\w{2,4}$/, '');
getFilenamePrefix(results) : flags.outputPath.replace(/\.\w{2,4}$/, '');
const resolvedPath = path.resolve(cwd, configuredPath);

if (flags.saveArtifacts) {
Expand Down
Expand Up @@ -20,7 +20,7 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const assetSaver = require('../../../lighthouse-core/lib/asset-saver');
const getFilenamePrefix = require('../../../lighthouse-core/lib/file-namer').getFilenamePrefix;

class ExperimentDatabase {
constructor() {
Expand All @@ -42,7 +42,7 @@ class ExperimentDatabase {
* @param {!Object} lhResults
*/
saveData(lhFlags, lhResults) {
const id = assetSaver.getFilenamePrefix(lhResults);
const id = getFilenamePrefix(lhResults);
this._timeStamps[id] = lhResults.generatedTime;

const dirPath = path.join(this._fsRoot, id);
Expand Down
25 changes: 0 additions & 25 deletions lighthouse-core/lib/asset-saver.js
Expand Up @@ -19,32 +19,8 @@
const fs = require('fs');
const log = require('../../lighthouse-core/lib/log.js');
const stringifySafe = require('json-stringify-safe');
const URL = require('./url-shim');
const Metrics = require('./traces/pwmetrics-events');

/**
* 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
* @returns string
*/
function getFilenamePrefix(results) {
const hostname = new URL(results.url).hostname;
const date = (results.generatedTime && new Date(results.generatedTime)) || new Date();

const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
const dateParts = date.toLocaleDateString('en-US', {
year: 'numeric', month: '2-digit', day: '2-digit'
}).split('/');
dateParts.unshift(dateParts.pop());
const dateStr = dateParts.join('-');

const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
// replace characters that are unfriendly to filenames
return filenamePrefix.replace(/[\/\?<>\\:\*\|":]/g, '-');
}

/**
* Generate basic HTML page of screenshot filmstrip
* @param {!Array<{timestamp: number, datauri: string}>} screenshots
Expand Down Expand Up @@ -163,6 +139,5 @@ function saveAssets(artifacts, audits, pathWithBasename) {
module.exports = {
saveArtifacts,
saveAssets,
getFilenamePrefix,
prepareAssets
};
50 changes: 50 additions & 0 deletions lighthouse-core/lib/file-namer.js
@@ -0,0 +1,50 @@
/**
* @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';

/**
* 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
* @returns string
*/
function getFilenamePrefix(results) {
// eslint-disable-next-line no-undef
const hostname = new (URLConstructor || URL)(results.url).hostname;
const date = (results.generatedTime && new Date(results.generatedTime)) || new Date();

const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
const dateParts = date.toLocaleDateString('en-US', {
year: 'numeric', month: '2-digit', day: '2-digit'
}).split('/');
dateParts.unshift(dateParts.pop());
const dateStr = dateParts.join('-');

const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
// replace characters that are unfriendly to filenames
return filenamePrefix.replace(/[\/\?<>\\:\*\|":]/g, '-');
}

let URLConstructor;
if (typeof module !== 'undefined' && module.exports) {
URLConstructor = require('./url-shim');

module.exports = {
getFilenamePrefix
};
}
6 changes: 5 additions & 1 deletion lighthouse-core/report/report-generator.js
Expand Up @@ -238,7 +238,11 @@ class ReportGenerator {
if (reportContext === 'devtools') {
return [];
} else {
return [fs.readFileSync(path.join(__dirname, './scripts/lighthouse-report.js'), 'utf8')];
return [
fs.readFileSync(path.join(__dirname, './scripts/logger.js'), 'utf8'),
fs.readFileSync(path.join(__dirname, '../lib/file-namer.js'), 'utf8'),
fs.readFileSync(path.join(__dirname, './scripts/lighthouse-report.js'), 'utf8')
];
}
}

Expand Down
6 changes: 6 additions & 0 deletions lighthouse-core/report/scripts/.eslintrc.js
@@ -0,0 +1,6 @@
module.exports = {
"extends": "../../../.eslintrc.js",
"env": {
"browser": true
}
}
173 changes: 159 additions & 14 deletions lighthouse-core/report/scripts/lighthouse-report.js
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

/* global window, document */
/* global ga, logger */

'use strict';

Expand All @@ -26,27 +26,147 @@ class LighthouseReport {
*/
constructor(lhresults) {
this.json = lhresults || null;
this._copyAttempt = false;

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._addEventListeners();
}

_addEventListeners() {
this._setupExpandDetailsWhenPrinting();

const printButton = document.querySelector('.js-print');
if (printButton) {
printButton.addEventListener('click', _ => window.print());
}

const openButton = document.querySelector('.js-open');
if (openButton) {
openButton.addEventListener('click', this.sendJSONReport.bind(this));
}

const headerContainer = document.querySelector('.js-header-container');
if (headerContainer) {
const toggleButton = headerContainer.querySelector('.js-header-toggle');
toggleButton.addEventListener('click', () => headerContainer.classList.toggle('expanded'));
}

this.exportButton = document.querySelector('.js-export');
if (this.exportButton) {
this.exportButton.addEventListener('click', this.onExportButtonClick);
const dropdown = document.querySelector('.export-dropdown');
dropdown.addEventListener('click', this.onExport);

document.addEventListener('copy', this.onCopy);
}
}

/**
* Handler copy events.
*/
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));
logger.log('Report JSON copied to clipboard');
}

this._copyAttempt = false;
}

/**
* Copies the report JSON to the clipboard (if supported by the browser).
*/
onCopyButtonClick() {
if (window.ga) {
ga('send', 'event', 'report', 'copy');
}

try {
if (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.
const successful = document.execCommand('copy');
if (!successful) {
this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
logger.warn('Your browser does not support copy to clipboard.');
}
}
} catch (err) {
this._copyAttempt = false;
logger.log(err.message);
}
}

closeExportDropdown() {
this.exportButton.classList.remove('active');
}

/**
* Click handler for export button.
*/
onExportButtonClick(e) {
e.preventDefault();
e.target.classList.toggle('active');
document.addEventListener('keydown', this.onKeyDown);
}

/**
* Handler for "export as" button.
*/
onExport(e) {
e.preventDefault();

if (!e.target.dataset.action) {
return;
}

switch (e.target.dataset.action) {
case 'copy':
this.onCopyButtonClick();
break;
case 'open-viewer':
this.sendJSONReport();
break;
case 'print':
window.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': {
let htmlStr = '';

// Since Viewer generates its page HTML dynamically from report JSON,
// run the ReportGenerator. For everything else, the page's HTML is
// already the final product.
if (e.target.dataset.context !== 'viewer') {
htmlStr = document.documentElement.outerHTML;
} else {
const reportGenerator = new ReportGenerator();
htmlStr = reportGenerator.generateHTML(this.json, 'cli');
}

try {
this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
} catch (err) {
logger.error('Could not export as HTML. ' + err.message);
}
break;
}
}

this.closeExportDropdown();
document.removeEventListener('keydown', this.onKeyDown);
}

/**
* Keydown handler for the document.
*/
onKeyDown(e) {
if (e.keyCode === 27) { // ESC
this.closeExportDropdown();
}
}

/**
Expand Down Expand Up @@ -99,10 +219,35 @@ class LighthouseReport {
});
}
}

/**
* Downloads a file (blob) using a[download].
* @param {Blob|File} blob The file to save.
*/
_saveFile(blob) {
const filename = window.getFilenamePrefix({
url: this.json.url,
generatedTime: this.json.generatedTime
});

const ext = blob.type.match('json') ? '.json' : '.html';

const a = document.createElement('a');
a.download = `${filename}${ext}`;
a.href = URL.createObjectURL(blob);
document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
a.click();

// cleanup.
document.body.removeChild(a);
setTimeout(_ => URL.revokeObjectURL(a.href), 500);
}
}

// If in Node, allow others to require us. By default, browser code can juse
// use the LighthouseReport class.
if (typeof module !== 'undefined') {
// Exports for Node usage (Viewer browserifies).
let ReportGenerator;
if (typeof module !== 'undefined' && module.exports) {
module.exports = LighthouseReport;
ReportGenerator = require('../../../lighthouse-core/report/report-generator');
window.getFilenamePrefix = require('../../../lighthouse-core/lib/file-namer').getFilenamePrefix;
}
Expand Up @@ -67,4 +67,6 @@ class Logger {
}
}

module.exports = new Logger('#log'); // singleton
if (typeof module !== 'undefined' && module.exports) {
module.exports = Logger;
}

0 comments on commit 9ac57cb

Please sign in to comment.