diff --git a/treemap/app/src/main.js b/treemap/app/src/main.js index 87626f0c7a84..a4176e59d830 100644 --- a/treemap/app/src/main.js +++ b/treemap/app/src/main.js @@ -24,6 +24,8 @@ if (!logEl) { throw new Error('logger element not found'); } const logger = new Logger(logEl); +// `getGistFileContentAsJson` expects logger to be defined globally. +window.logger = logger; /** @type {TreemapViewer} */ let treemapViewer; @@ -787,16 +789,40 @@ class LighthouseTreemap { } /** + * Coerce json into LH.Treemap.Options + * Accepts if json is an lhr, or {lhr: ...} or {lighthouseResult: ...} + * Throws error if json does not match expectations. * @param {any} json * @return {LH.Treemap.Options} */ - convertToOptions(json) { + coerceToOptions(json) { + /** @type {LH.Treemap.Options['lhr']|null} */ + let lhr = null; if (json && typeof json === 'object') { - if (json.audits) json = {lhr: json}; - if (json.lhr && json.lhr.audits && typeof json.lhr.audits === 'object') return json; + for (const maybeLhr of [json, json.lhr, json.lighthouseResult]) { + if (maybeLhr && maybeLhr.audits && typeof maybeLhr.audits === 'object') { + lhr = maybeLhr; + break; + } + } + } + + if (!lhr) { + throw new Error('provided json is not a Lighthouse result'); + } + + if (!lhr.audits['script-treemap-data']) { + throw new Error('provided Lighthouse result is missing audit: `script-treemap-data`'); } - throw new Error('unknown json'); + if (lhr === json.lhr) { + // Special case: file was {lhr: ...} and potentially has other properties. + // LH.Treemap.Options only has `lhr` currently, but may have more in the future. + return json; + } + + // json was exactly a LHR, or a PSI result object aka {lighthouseResult} + return {lhr}; } /** @@ -818,7 +844,7 @@ class LighthouseTreemap { const gistId = match[0]; history.pushState({}, '', `${LighthouseTreemap.APP_URL}?gist=${gistId}`); const json = await this._github.getGistFileContentAsJson(gistId); - const options = this.convertToOptions(json); + const options = this.coerceToOptions(json); this.init(options); } } catch (err) { @@ -834,7 +860,7 @@ class LighthouseTreemap { let options; try { json = JSON.parse(str); - options = this.convertToOptions(json); + options = this.coerceToOptions(json); } catch (e) { logger.error('Could not parse JSON file.'); } @@ -867,7 +893,7 @@ class LighthouseTreemap { // Try paste as json content. try { const json = JSON.parse(e.clipboardData.getData('text')); - const options = this.convertToOptions(json); + const options = this.coerceToOptions(json); this.init(options); if (window.ga) { @@ -898,47 +924,26 @@ async function main() { if (window.__treemapOptions) { // Prefer the hardcoded options from a saved HTML file above all. - app.init(window.__treemapOptions); + app.init(app.coerceToOptions(window.__treemapOptions)); } else if ('debug' in params) { const response = await fetch('debug.json'); - app.init(await response.json()); + const json = await response.json(); + const options = app.coerceToOptions(json); + app.init(options); } else if (params.lhr) { - const options = { - lhr: params.lhr, - }; + const options = app.coerceToOptions(params.lhr); app.init(options); } else if (params.gist) { - let json; - let options; - try { - json = await app._github.getGistFileContentAsJson(params.gist || ''); - options = app.convertToOptions(json); - } catch (err) { - logger.log(err); - } - if (options) app.init(options); - } else { - // TODO: remove for v8. - window.addEventListener('message', e => { - if (e.source !== self.opener) return; - - /** @type {LH.Treemap.Options} */ - const options = e.data; - const {lhr} = options; - if (!lhr) return logger.error('Error: Invalid options'); - - const documentUrl = lhr.requestedUrl; - if (!documentUrl) return logger.error('Error: Invalid options'); - - app.init(options); - }); - } - - // TODO: remove for v8. - // If the page was opened as a popup, tell the opening window we're ready. - if (self.opener && !self.opener.closed) { - self.opener.postMessage({opened: true}, '*'); + const json = await app._github.getGistFileContentAsJson(params.gist || ''); + const options = app.coerceToOptions(json); + app.init(options); } } -document.addEventListener('DOMContentLoaded', main); +document.addEventListener('DOMContentLoaded', async () => { + try { + await main(); + } catch (err) { + logger.error(err); + } +}); diff --git a/treemap/test/treemap-test-pptr.js b/treemap/test/treemap-test-pptr.js index dbf62b5563c0..875d2053d758 100644 --- a/treemap/test/treemap-test-pptr.js +++ b/treemap/test/treemap-test-pptr.js @@ -81,18 +81,16 @@ describe('Lighthouse Treemap', () => { expect(options.lhr.requestedUrl).toBe(debugOptions.lhr.requestedUrl); }); - // TODO: remove for v8 - async function loadFromPostMessage(options) { - const openerPage = await browser.newPage(); - await openerPage.evaluate((treemapUrl, options) => { - const popup = window.open(treemapUrl); - window.addEventListener('message', () => { - popup.postMessage(options, new URL(treemapUrl).origin); - }); - }, treemapUrl, options); - await new Promise(resolve => browser.on('targetcreated', resolve)); - const target = (await browser.targets()).find(target => target.url() === treemapUrl); - page = await target.page(); + /** + * @param {{options: any, usesGzip: boolean}} + */ + async function loadFromEncodedUrl({options, useGzip}) { + const json = JSON.stringify(options); + const encoded = await page.evaluate(` + ${getTextEncodingCode()} + TextEncoding.toBase64(${JSON.stringify(json)}, {gzip: ${useGzip}}); + `); + await page.goto(`${treemapUrl}?gzip=${useGzip ? '1' : '0'}#${encoded}`); await page.waitForFunction(() => { if (window.__treemapOptions) return true; @@ -101,53 +99,51 @@ describe('Lighthouse Treemap', () => { }); } - it('from window postMessage', async () => { - await loadFromPostMessage(debugOptions); - const optionsInPage = await page.evaluate(() => window.__treemapOptions); - expect(optionsInPage.lhr.requestedUrl).toBe(debugOptions.lhr.requestedUrl); - }); - - it('handles errors', async () => { - await loadFromPostMessage({}); - const optionsInPage = await page.evaluate(() => window.__treemapOptions); - expect(optionsInPage).toBeUndefined(); - const error = await page.evaluate(() => document.querySelector('#lh-log').textContent); - expect(error).toBe('Error: Invalid options'); - }); - - it('from encoded fragment (gzip)', async () => { + it('from encoded fragment (no gzip)', async () => { const options = JSON.parse(JSON.stringify(debugOptions)); options.lhr.requestedUrl += '😃😃😃'; - const json = JSON.stringify(options); - const encoded = await page.evaluate(` - ${getTextEncodingCode()} - TextEncoding.toBase64(${JSON.stringify(json)}, {gzip: true}); - `); - - await page.goto(`${treemapUrl}?gzip=1#${encoded}`); - await page.waitForFunction( - () => window.__treemapOptions || document.body.textContent.startsWith('Error')); + await loadFromEncodedUrl({options, usesGzip: false}); const optionsInPage = await page.evaluate(() => window.__treemapOptions); expect(optionsInPage.lhr.requestedUrl).toBe(options.lhr.requestedUrl); }); - it('from encoded fragment (no gzip)', async () => { + it('from encoded fragment (gzip)', async () => { const options = JSON.parse(JSON.stringify(debugOptions)); options.lhr.requestedUrl += '😃😃😃'; - const json = JSON.stringify(options); - const encoded = await page.evaluate(` - ${getTextEncodingCode()} - TextEncoding.toBase64(${JSON.stringify(json)}, {gzip: false}); - `); - - await page.goto(`${treemapUrl}#${encoded}`); - await page.waitForFunction( - () => window.__treemapOptions || document.body.textContent.startsWith('Error')); + await loadFromEncodedUrl({options, usesGzip: true}); const optionsInPage = await page.evaluate(() => window.__treemapOptions); expect(optionsInPage.lhr.requestedUrl).toBe(options.lhr.requestedUrl); }); + + describe('handles errors', () => { + const errorTestCases = [ + { + options: {lhr: 'lol'}, + error: 'Error: provided json is not a Lighthouse result', + }, + { + options: {lhr: {noaudits: {}}}, + error: 'Error: provided json is not a Lighthouse result', + }, + { + options: {lhr: {audits: {}}}, + error: 'Error: provided Lighthouse result is missing audit: `script-treemap-data`', + }, + ]; + for (let i = 0; i < errorTestCases.length; i++) { + it(`case #${i + 1}`, async () => { + const testCase = errorTestCases[i]; + await loadFromEncodedUrl({options: testCase.options, usesGzip: false}); + const optionsInPage = await page.evaluate(() => window.__treemapOptions); + expect(optionsInPage).toBeUndefined(); + const error = await page.evaluate(() => document.querySelector('#lh-log').textContent); + expect(error).toBe(testCase.error); + pageErrors = []; + }); + } + }); }); describe('renders correctly', () => {