Skip to content

Commit

Permalink
Viewer: Lighthouse 2.0 support (#2521)
Browse files Browse the repository at this point in the history
* basic viewer working

* most features working

* reenable remaining features

* cleanup

* fix filename

* logger, ga, and version check

* make closure happy

* pull report-ui-features out of report-renderer

disable save-as-gist based on providing a callback for saving

* gulp watch

* add back drag-and-drop unit test

* actually make closure happy

* review fixes

* review feedback
  • Loading branch information
brendankenny authored and ebidel committed Jun 22, 2017
1 parent e145124 commit 2b31a44
Show file tree
Hide file tree
Showing 23 changed files with 816 additions and 737 deletions.
2 changes: 2 additions & 0 deletions lighthouse-core/closure/closure-type-checking.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ gulp.task('compile-report', () => {
return gulp.src([
// externs
'closure/third_party/commonjs.js',
'closure/typedefs/viewer-externs.js',

'lib/file-namer.js',
'report/v2/renderer/*.js',
'../lighthouse-viewer/app/src/viewer-ui-features.js',
])

// Ignore `module.exports` and `self.ClassName = ClassName` statements.
Expand Down
23 changes: 23 additions & 0 deletions lighthouse-core/closure/typedefs/viewer-externs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @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';

/**
* Typing externs file needed for Viewer compilation.
* @externs
*/

/**
* @struct
* @constructor
*/
function ReportGenerator() {}

/**
* @param {!ReportRenderer.ReportJSON} reportJson
* @return {string}
*/
ReportGenerator.prototype.generateReportHtml = function(reportJson) {};
11 changes: 1 addition & 10 deletions lighthouse-core/report/v2/renderer/report-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@ class ReportRenderer {
/**
* @param {!DOM} dom
* @param {!CategoryRenderer} categoryRenderer
* @param {?ReportUIFeatures=} uiFeatures
*/
constructor(dom, categoryRenderer, uiFeatures = null) {
constructor(dom, categoryRenderer) {
/** @private {!DOM} */
this._dom = dom;
/** @private {!CategoryRenderer} */
this._categoryRenderer = categoryRenderer;
/** @private {!Document|!Element} */
this._templateContext = this._dom.document();
/** @private {ReportUIFeatures} */
this._uiFeatures = uiFeatures;
}

/**
Expand All @@ -39,12 +36,6 @@ class ReportRenderer {
container.textContent = ''; // Remove previous report.
const 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);
}

return /** @type {!Element} **/ (element);
}

Expand Down
66 changes: 60 additions & 6 deletions lighthouse-core/report/v2/renderer/report-ui-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* the report.
*/

/* globals self URL Blob CustomEvent getFilenamePrefix */
/* globals self URL Blob CustomEvent getFilenamePrefix window */

class ReportUIFeatures {

Expand All @@ -20,9 +20,9 @@ class ReportUIFeatures {
constructor(dom) {
/** @type {!ReportRenderer.ReportJSON} */
this.json; // eslint-disable-line no-unused-expressions
/** @private {!DOM} */
/** @protected {!DOM} */
this._dom = dom;
/** @private {!Document} */
/** @protected {!Document} */
this._document = this._dom.document();
/** @private {boolean} */
this._copyAttempt = false;
Expand Down Expand Up @@ -188,9 +188,7 @@ class ReportUIFeatures {
break;
}
case 'save-html': {
this._resetUIState();

const htmlStr = this._document.documentElement.outerHTML;
const htmlStr = this.getReportHtml();
try {
this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
} catch (/** @type {!Error} */ e) {
Expand All @@ -200,6 +198,14 @@ class ReportUIFeatures {
}
break;
}
case 'open-viewer': {
this.sendJsonReport();
break;
}
case 'save-gist': {
this.saveAsGist();
break;
}
}

this.closeExportDropdown();
Expand All @@ -216,6 +222,34 @@ class ReportUIFeatures {
}
}

/**
* Opens a new tab to the online viewer and sends the local page's JSON results
* to the online viewer using postMessage.
* @protected
*/
sendJsonReport() {
const VIEWER_ORIGIN = 'https://googlechrome.github.io';
const VIEWER_URL = `${VIEWER_ORIGIN}/lighthouse/viewer/`;

// Chrome doesn't allow us to immediately postMessage to a popup right
// after it's created. Normally, we could also listen for the popup window's
// load event, however it is cross-domain and won't fire. Instead, listen
// for a message from the target app saying "I'm open".
window.addEventListener('message', function msgHandler(/** @type {!Event} */ e) {
const messageEvent = /** @type {!MessageEvent<{opened: boolean}>} */ (e);
if (messageEvent.origin !== VIEWER_ORIGIN) {
return;
}

if (messageEvent.data.opened) {
popup.postMessage({lhresults: this.json}, VIEWER_ORIGIN);
window.removeEventListener('message', msgHandler);
}
}.bind(this));

const popup = /** @type {!Window} */ (window.open(VIEWER_URL, '_blank'));
}

/**
* Expands audit details when user prints via keyboard shortcut.
* @param {!Event} e
Expand Down Expand Up @@ -265,9 +299,29 @@ class ReportUIFeatures {
});
}
}

/**
* Returns the html that recreates this report.
* @return {string}
* @protected
*/
getReportHtml() {
this._resetUIState();
return this._document.documentElement.outerHTML;
}

/**
* Save json as a gist. Unimplemented in base UI features.
* @protected
*/
saveAsGist() {
throw new Error('Cannot save as gist from base report');
}

/**
* Downloads a file (blob) using a[download].
* @param {!Blob|!File} blob The file to save.
* @private
*/
_saveFile(blob) {
const filename = getFilenamePrefix({
Expand Down
21 changes: 21 additions & 0 deletions lighthouse-core/report/v2/report-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8');
const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8');

class ReportGeneratorV2 {
/**
* @return {string}
*/
static get reportJs() {
return REPORT_JAVASCRIPT;
}

/**
* @return {string}
*/
static get reportCss() {
return REPORT_CSS;
}

/**
* @return {string}
*/
static get reportTemplates() {
return REPORT_TEMPLATES;
}

/**
* Computes the weighted-average of the score of the list of items.
* @param {!Array<{score: number|undefined, weight: number|undefined}} items
Expand Down
8 changes: 6 additions & 2 deletions lighthouse-core/report/v2/report-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@
const dom = new DOM(document);
const detailsRenderer = new DetailsRenderer(dom);
const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
const features = new ReportUIFeatures(dom);
const renderer = new ReportRenderer(dom, categoryRenderer, features);
const renderer = new ReportRenderer(dom, categoryRenderer);

const container = document.querySelector('main');
renderer.renderReport(window.__LIGHTHOUSE_JSON__, container);

// Hook in JS features and page-level event listeners after the report
// is in the document.
const features = new ReportUIFeatures(dom);
features.initFeatures(window.__LIGHTHOUSE_JSON__);
});

document.addEventListener('lh-analytics', e => {
Expand Down
6 changes: 6 additions & 0 deletions lighthouse-core/report/v2/templates.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ <h1 class="leftnav__header__title">Lighthouse</h1>
background-size: 16px;
background-position: 9px 50%;
}
/* save-as-gist option hidden in report */
.lh-export--gist {
display: none;
}
.lh-config {
display: flex;
}
Expand Down Expand Up @@ -321,6 +325,8 @@ <h1 class="leftnav__header__title">Lighthouse</h1>
<a href="#" class="report-icon report-icon--copy" data-action="copy">Copy JSON</a>
<a href="#" class="report-icon report-icon--download" data-action="save-html">Save as HTML</a>
<a href="#" class="report-icon report-icon--download" data-action="save-json">Save as JSON</a>
<a href="#" class="report-icon report-icon--open lh-export--viewer" data-action="open-viewer">Open in Viewer</a>
<a href="#" class="report-icon report-icon--open lh-export--gist" data-action="save-gist">Save as Gist</a>
</div>
</div>
</div>
Expand Down
17 changes: 10 additions & 7 deletions lighthouse-viewer/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
-->

<!doctype html>
<html data-report-context="viewer">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
Expand All @@ -15,11 +15,12 @@
<meta name="theme-color" content="#304ffe">
<link rel="stylesheet" href="styles/viewer.css">
</head>
<body>
<body class="lh-root lh-vars">
<div hidden>%%LIGHTHOUSE_TEMPLATES%%</div>

<div class="drop_zone"></div>

<output></output>
<main><!-- report populated here --></main>

<div class="viewer-placeholder">
<div class="viewer-placeholder-inner">
Expand All @@ -36,11 +37,13 @@ <h1 class="viewer-placeholder__heading">Lighthouse Report Viewer</h1>
</div>
</div>

<div class="log-wrapper">
<div id="log"></div>
</div>
<div id="lh-log"></div>

<input id="hidden-file-input" type="file" hidden accept="application/json">

<script src="src/main.js" async></script>
<script src="https://www.gstatic.com/firebasejs/4.1.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.1.2/firebase-auth.js"></script>
<script src="src/viewer.js"></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
Expand Down
63 changes: 63 additions & 0 deletions lighthouse-viewer/app/src/drag-and-drop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @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';

/**
* Manages drag and drop file input for the page.
*/
class DragAndDrop {
/**
* @param {function(!File)} fileHandlerCallback Invoked when the user chooses a new file.
*/
constructor(fileHandlerCallback) {
this._dropZone = document.querySelector('.drop_zone');
this._fileHandlerCallback = fileHandlerCallback;
this._dragging = false;

this._addListeners();
}

_addListeners() {
// The mouseleave event is more reliable than dragleave when the user drops
// the file outside the window.
document.addEventListener('mouseleave', _ => {
if (!this._dragging) {
return;
}
this._resetDraggingUI();
});

document.addEventListener('dragover', e => {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy'; // Explicitly show as copy action.
});

document.addEventListener('dragenter', _ => {
this._dropZone.classList.add('dropping');
this._dragging = true;
});

document.addEventListener('drop', e => {
e.stopPropagation();
e.preventDefault();

this._resetDraggingUI();

// Note, this ignores multiple files in the drop, only taking the first.
this._fileHandlerCallback(e.dataTransfer.files[0]);
});
}

_resetDraggingUI() {
this._dropZone.classList.remove('dropping');
this._dragging = false;
}
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = DragAndDrop;
}
Loading

0 comments on commit 2b31a44

Please sign in to comment.