diff --git a/lighthouse-core/report/v2/renderer/details-renderer.js b/lighthouse-core/report/v2/renderer/details-renderer.js index 129538dd2878..ebf15a4650d5 100644 --- a/lighthouse-core/report/v2/renderer/details-renderer.js +++ b/lighthouse-core/report/v2/renderer/details-renderer.js @@ -15,6 +15,8 @@ */ 'use strict'; +/* globals self */ + class DetailsRenderer { /** * @param {!DOM} dom @@ -34,7 +36,7 @@ class DetailsRenderer { case 'block': return this._renderBlock(details); case 'cards': - return this._renderCards(details); + return this._renderCards(/** @type {!DetailsRenderer.CardsDetailsJSON} */ (details)); case 'list': return this._renderList(details); default: @@ -43,7 +45,7 @@ class DetailsRenderer { } /** - * @param {!DetailsJSON} text + * @param {!DetailsRenderer.DetailsJSON} text * @return {!Element} */ _renderText(text) { @@ -53,19 +55,20 @@ class DetailsRenderer { } /** - * @param {!DetailsJSON} block + * @param {!DetailsRenderer.DetailsJSON} block * @return {!Element} */ _renderBlock(block) { const element = this._dom.createElement('div', 'lh-block'); - for (const item of block.items) { + const items = block.items || []; + for (const item of items) { element.appendChild(this.render(item)); } return element; } /** - * @param {!DetailsJSON} list + * @param {!DetailsRenderer.DetailsJSON} list * @return {!Element} */ _renderList(list) { @@ -76,16 +79,17 @@ class DetailsRenderer { element.appendChild(summary); } - const items = this._dom.createElement('div', 'lh-list__items'); - for (const item of list.items) { - items.appendChild(this.render(item)); + const itemsElem = this._dom.createElement('div', 'lh-list__items'); + const items = list.items || []; + for (const item of items) { + itemsElem.appendChild(this.render(item)); } - element.appendChild(items); + element.appendChild(itemsElem); return element; } /** - * @param {!CardsDetailsJSON} details + * @param {!DetailsRenderer.CardsDetailsJSON} details * @return {!Element} */ _renderCards(details) { @@ -117,11 +121,25 @@ class DetailsRenderer { if (typeof module !== 'undefined' && module.exports) { module.exports = DetailsRenderer; +} else { + self.DetailsRenderer = DetailsRenderer; } -/** @typedef {{type: string, text: string|undefined, header: DetailsJSON|undefined, items: Array|undefined}} */ +/** + * @typedef {{ + * type: string, + * text: (string|undefined), + * header: (!DetailsRenderer.DetailsJSON|undefined), + * items: (!Array|undefined) + * }} + */ DetailsRenderer.DetailsJSON; // eslint-disable-line no-unused-expressions - -/** @typedef {{type: string, text: string, header: DetailsJSON, items: Array<{title: string, value: string, snippet: string|undefined, target: string}>}} */ +/** @typedef {{ + * type: string, + * text: string, + * header: !DetailsRenderer.DetailsJSON, + * items: !Array<{title: string, value: string, snippet: (string|undefined), target: string}> + * }} + */ DetailsRenderer.CardsDetailsJSON; // eslint-disable-line no-unused-expressions diff --git a/lighthouse-core/report/v2/renderer/dom.js b/lighthouse-core/report/v2/renderer/dom.js index 4323f6708fe9..0b3ea063a4bb 100644 --- a/lighthouse-core/report/v2/renderer/dom.js +++ b/lighthouse-core/report/v2/renderer/dom.js @@ -15,7 +15,7 @@ */ 'use strict'; -/* globals URL */ +/* globals URL self */ class DOM { /** @@ -33,14 +33,17 @@ class DOM { * set the attribute on the node. * @return {!Element} */ - createElement(name, className, attrs = {}) { + createElement(name, className, attrs) { + // TODO(all): adopt `attrs` default arg when https://codereview.chromium.org/2821773002/ lands + attrs = attrs || {}; const element = this._document.createElement(name); if (className) { element.className = className; } Object.keys(attrs).forEach(key => { - if (attrs[key] !== undefined) { - element.setAttribute(key, attrs[key]); + const value = attrs[key]; + if (typeof value !== 'undefined') { + element.setAttribute(key, value); } }); return element; @@ -48,20 +51,21 @@ class DOM { /** * @param {string} selector + * @param {!Document|!Element} context * @return {!DocumentFragment} A clone of the template content. * @throws {Error} */ - cloneTemplate(selector) { - const template = this._document.querySelector(selector); + cloneTemplate(selector, context) { + const template = context.querySelector(selector); if (!template) { throw new Error(`Template not found: template${selector}`); } - return this._document.importNode(template.content, true); + return /** @type {!DocumentFragment} */ (this._document.importNode(template.content, true)); } /** * @param {string} text - * @return {!HTMLSpanElement} + * @return {!Element} */ createSpanFromMarkdown(text) { const element = this.createElement('span'); @@ -87,8 +91,17 @@ class DOM { return element; } + + /** + * @return {!Document} + */ + document() { + return this._document; + } } if (typeof module !== 'undefined' && module.exports) { module.exports = DOM; +} else { + self.DOM = DOM; } diff --git a/lighthouse-core/report/v2/renderer/report-renderer.js b/lighthouse-core/report/v2/renderer/report-renderer.js index 58a87d413236..a6d1507fd058 100644 --- a/lighthouse-core/report/v2/renderer/report-renderer.js +++ b/lighthouse-core/report/v2/renderer/report-renderer.js @@ -22,7 +22,7 @@ * Dummy text for ensuring report robustness: pre$`post %%LIGHTHOUSE_JSON%% */ -/* globals DOM, DetailsRenderer */ +/* globals self */ const RATINGS = { PASS: {label: 'pass', minScore: 75}, @@ -56,15 +56,18 @@ function formatNumber(number) { class ReportRenderer { /** - * @param {!Document} document + * @param {!DOM} dom + * @param {!DetailsRenderer} detailsRenderer */ - constructor(document) { - this._dom = new DOM(document); - this._detailsRenderer = new DetailsRenderer(this._dom); + constructor(dom, detailsRenderer) { + this._dom = dom; + this._detailsRenderer = detailsRenderer; + + this._templateContext = this._dom.document(); } /** - * @param {!ReportJSON} report + * @param {!ReportRenderer.ReportJSON} report * @return {!Element} */ renderReport(report) { @@ -94,15 +97,24 @@ class ReportRenderer { element.querySelector('.lh-score__description') .appendChild(this._dom.createSpanFromMarkdown(description)); - return element; + return /** @type {!Element} **/ (element); + } + + /** + * Define a custom element for to be extracted from. For example: + * this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html')) + * @param {!Document|!Element} context + */ + setTemplateContext(context) { + this._templateContext = context; } /** - * @param {!AuditJSON} audit + * @param {!ReportRenderer.AuditJSON} audit * @return {!Element} */ _renderAuditScore(audit) { - const tmpl = this._dom.cloneTemplate('#tmpl-lh-audit-score'); + const tmpl = this._dom.cloneTemplate('#tmpl-lh-audit-score', this._templateContext); const scoringMode = audit.result.scoringMode; const description = audit.result.helpText; @@ -126,11 +138,11 @@ class ReportRenderer { } /** - * @param {!CategoryJSON} category + * @param {!ReportRenderer.CategoryJSON} category * @return {!Element} */ _renderCategoryScore(category) { - const tmpl = this._dom.cloneTemplate('#tmpl-lh-category-score'); + const tmpl = this._dom.cloneTemplate('#tmpl-lh-category-score', this._templateContext); const score = Math.round(category.score); return this._populateScore(tmpl, score, 'numeric', category.name, category.description); } @@ -146,7 +158,7 @@ class ReportRenderer { } /** - * @param {!ReportJSON} report + * @param {!ReportRenderer.ReportJSON} report * @return {!Element} */ _renderReport(report) { @@ -158,7 +170,7 @@ class ReportRenderer { } /** - * @param {!CategoryJSON} category + * @param {!ReportRenderer.CategoryJSON} category * @return {!Element} */ _renderCategory(category) { @@ -185,7 +197,7 @@ class ReportRenderer { } /** - * @param {!AuditJSON} audit + * @param {!ReportRenderer.AuditJSON} audit * @return {!Element} */ _renderAudit(audit) { @@ -197,13 +209,45 @@ class ReportRenderer { if (typeof module !== 'undefined' && module.exports) { module.exports = ReportRenderer; +} else { + self.ReportRenderer = ReportRenderer; } -/** @typedef {{id: string, weight: number, score: number, result: {description: string, displayValue: string, helpText: string, score: number|boolean, details: DetailsRenderer.DetailsJSON|DetailsRenderer.CardsDetailsJSON|undefined}}} */ -let AuditJSON; // eslint-disable-line no-unused-vars +/** + * @typedef {{ + * id: string, weight: + * number, score: number, + * result: { + * description: string, + * displayValue: string, + * helpText: string, + * score: (number|boolean), + * scoringMode: string, + * details: (!DetailsRenderer.DetailsJSON|!DetailsRenderer.CardsDetailsJSON|undefined) + * } + * }} + */ +ReportRenderer.AuditJSON; // eslint-disable-line no-unused-expressions -/** @typedef {{name: string, weight: number, score: number, description: string, audits: Array}} */ -let CategoryJSON; // eslint-disable-line no-unused-vars +/** + * @typedef {{ + * name: string, + * weight: number, + * score: number, + * description: string, + * audits: !Array + * }} + */ +ReportRenderer.CategoryJSON; // eslint-disable-line no-unused-expressions -/** @typedef {{reportCategories: Array}} */ -let ReportJSON; // eslint-disable-line no-unused-vars +/** + * @typedef {{ + * lighthouseVersion: !string, + * generatedTime: !string, + * initialUrl: !string, + * url: !string, + * audits: ?Object, + * reportCategories: !Array + * }} + */ +ReportRenderer.ReportJSON; // eslint-disable-line no-unused-expressions diff --git a/lighthouse-core/report/v2/report-template.html b/lighthouse-core/report/v2/report-template.html index 019c68ff6090..627c4047f251 100644 --- a/lighthouse-core/report/v2/report-template.html +++ b/lighthouse-core/report/v2/report-template.html @@ -30,8 +30,11 @@ diff --git a/lighthouse-core/test/report/v2/renderer/dom-test.js b/lighthouse-core/test/report/v2/renderer/dom-test.js index b1c711a20139..625efadf7ee0 100644 --- a/lighthouse-core/test/report/v2/renderer/dom-test.js +++ b/lighthouse-core/test/report/v2/renderer/dom-test.js @@ -58,12 +58,16 @@ describe('DOM', () => { describe('cloneTemplate', () => { it('should clone a template', () => { - const clone = dom.cloneTemplate('#tmpl-lh-audit-score'); + const clone = dom.cloneTemplate('#tmpl-lh-audit-score', dom.document()); assert.ok(clone.querySelector('.lh-score')); }); it('fails when template cannot be found', () => { - assert.throws(() => dom.cloneTemplate('#unknown-selector')); + assert.throws(() => dom.cloneTemplate('#unknown-selector', dom.document())); + }); + + it('fails when a template context isn\'t provided', () => { + assert.throws(() => dom.cloneTemplate('#tmpl-lh-audit-score')); }); }); diff --git a/lighthouse-core/test/report/v2/renderer/report-renderer-test.js b/lighthouse-core/test/report/v2/renderer/report-renderer-test.js index e6ae76038113..1575a5100f88 100644 --- a/lighthouse-core/test/report/v2/renderer/report-renderer-test.js +++ b/lighthouse-core/test/report/v2/renderer/report-renderer-test.js @@ -33,16 +33,14 @@ describe('ReportRenderer V2', () => { before(() => { global.URL = URL; - global.DOM = DOM; - global.DetailsRenderer = DetailsRenderer; const document = jsdom.jsdom(TEMPLATE_FILE); - renderer = new ReportRenderer(document); + const dom = new DOM(document); + const detailsRenderer = new DetailsRenderer(dom); + renderer = new ReportRenderer(dom, detailsRenderer); }); after(() => { global.URL = undefined; - global.DOM = undefined; - global.DetailsRenderer = undefined; }); describe('renderReport', () => { @@ -96,4 +94,12 @@ describe('ReportRenderer V2', () => { assert.equal(audits.length, category.audits.length, 'renders correct number of audits'); }); }); + + it('can set a custom templateContext', () => { + assert.equal(renderer._templateContext, renderer._dom.document()); + + const otherDocument = jsdom.jsdom(TEMPLATE_FILE); + renderer.setTemplateContext(otherDocument); + assert.equal(renderer._templateContext, otherDocument); + }); });