diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap index cc9bc18f322e..7847f74e50fb 100644 --- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap +++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap @@ -171,6 +171,9 @@ Object { Object { "path": "full-page-screenshot", }, + Object { + "path": "script-treemap-data", + }, Object { "path": "manual/pwa-cross-browser", }, diff --git a/lighthouse-core/audits/script-treemap-data.js b/lighthouse-core/audits/script-treemap-data.js index 8cfcc5442eff..63657c568337 100644 --- a/lighthouse-core/audits/script-treemap-data.js +++ b/lighthouse-core/audits/script-treemap-data.js @@ -257,13 +257,12 @@ class ScriptTreemapDataAudit extends Audit { * @return {Promise} */ static async audit(artifacts, context) { - const treemapData = await ScriptTreemapDataAudit.makeNodes(artifacts, context); + const nodes = await ScriptTreemapDataAudit.makeNodes(artifacts, context); - // TODO: when out of experimental should make a new detail type. - /** @type {LH.Audit.Details.DebugData} */ + /** @type {LH.Audit.Details.TreemapData} */ const details = { - type: 'debugdata', - treemapData, + type: 'treemap-data', + nodes, }; return { diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js index d859948b7157..271b67855487 100644 --- a/lighthouse-core/config/default-config.js +++ b/lighthouse-core/config/default-config.js @@ -238,6 +238,7 @@ const defaultConfig = { 'valid-source-maps', 'preload-lcp-image', 'full-page-screenshot', + 'script-treemap-data', 'manual/pwa-cross-browser', 'manual/pwa-page-transitions', 'manual/pwa-each-page-has-url', diff --git a/lighthouse-core/config/experimental-config.js b/lighthouse-core/config/experimental-config.js index efd98ab49bd1..5d716d3e245f 100644 --- a/lighthouse-core/config/experimental-config.js +++ b/lighthouse-core/config/experimental-config.js @@ -16,7 +16,6 @@ const config = { audits: [ 'autocomplete', 'large-javascript-libraries', - 'script-treemap-data', 'csp-xss', ], categories: { diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js index d866f17bb1b7..a65fa8702702 100644 --- a/lighthouse-core/report/html/renderer/details-renderer.js +++ b/lighthouse-core/report/html/renderer/details-renderer.js @@ -70,6 +70,7 @@ class DetailsRenderer { case 'screenshot': case 'debugdata': case 'full-page-screenshot': + case 'treemap-data': return null; default: { diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js index 5658f81abc17..3f59431148ea 100644 --- a/lighthouse-core/report/html/renderer/report-ui-features.js +++ b/lighthouse-core/report/html/renderer/report-ui-features.js @@ -183,7 +183,10 @@ class ReportUIFeatures { * @param {{text: string, icon?: string, onClick: () => void}} opts */ addButton(opts) { - const metricsEl = this._dom.find('.lh-audit-group--metrics', this._document); + const metricsEl = this._document.querySelector('.lh-audit-group--metrics'); + // Not supported without metrics group. + if (!metricsEl) return; + const classes = [ 'lh-button', ]; @@ -546,9 +549,8 @@ class ReportUIFeatures { * @param {LH.Result} json */ static openTreemap(json) { - const treemapDebugData = /** @type {LH.Audit.Details.DebugData} */ ( - json.audits['script-treemap-data'].details); - if (!treemapDebugData) { + const treemapData = json.audits['script-treemap-data'].details; + if (!treemapData) { throw new Error('no script treemap data found'); } diff --git a/lighthouse-core/report/html/report-template.html b/lighthouse-core/report/html/report-template.html index 74042a11934d..96c481103efe 100644 --- a/lighthouse-core/report/html/report-template.html +++ b/lighthouse-core/report/html/report-template.html @@ -48,11 +48,6 @@ // is in the document. const features = new ReportUIFeatures(dom); features.initFeatures(window.__LIGHTHOUSE_JSON__); - - // No UI for treemap yet. For now, must run this command in console. - globalThis._tmpFeatures = features; - const command = 'ReportUIFeatures.openTreemap(_tmpFeatures.json);'; - console.log(`For treemap viewer, run: ${command}`); } window.addEventListener('DOMContentLoaded', __initLighthouseReport__); diff --git a/lighthouse-core/test/audits/script-treemap-data-test.js b/lighthouse-core/test/audits/script-treemap-data-test.js index 1e5ee83440a1..70d05c618b01 100644 --- a/lighthouse-core/test/audits/script-treemap-data-test.js +++ b/lighthouse-core/test/audits/script-treemap-data-test.js @@ -28,7 +28,7 @@ function generateRecord(url, resourceSize, resourceType) { describe('ScriptTreemapData audit', () => { describe('squoosh fixture', () => { - /** @type {LH.Treemap.Node[]} */ + /** @type {LH.Audit.Details.TreemapData} */ let treemapData; beforeAll(async () => { const context = {computedCache: new Map()}; @@ -52,13 +52,15 @@ describe('ScriptTreemapData audit', () => { ScriptElements: [{src: scriptUrl, content}, noSourceMapScript], }; const results = await ScriptTreemapData.audit(artifacts, context); + if (!results.details || results.details.type !== 'treemap-data') { + throw new Error('should not happen.'); + } - // @ts-expect-error: Debug data. - treemapData = results.details.treemapData; + treemapData = results.details; }); it('has nodes', () => { - expect(treemapData.find(s => s.name === 'https://sqoosh.app/no-map-or-usage.js')) + expect(treemapData.nodes.find(s => s.name === 'https://sqoosh.app/no-map-or-usage.js')) .toMatchInlineSnapshot(` Object { "name": "https://sqoosh.app/no-map-or-usage.js", @@ -66,7 +68,7 @@ describe('ScriptTreemapData audit', () => { } `); - const bundleNode = treemapData.find(s => s.name === 'https://squoosh.app/main-app.js'); + const bundleNode = treemapData.nodes.find(s => s.name === 'https://squoosh.app/main-app.js'); // @ts-expect-error const unmapped = bundleNode.children[0].children.find(m => m.name === '(unmapped)'); expect(unmapped).toMatchInlineSnapshot(` @@ -77,13 +79,13 @@ describe('ScriptTreemapData audit', () => { } `); - expect(JSON.stringify(treemapData).length).toMatchInlineSnapshot(`6740`); - expect(treemapData).toMatchSnapshot(); + expect(JSON.stringify(treemapData.nodes).length).toMatchInlineSnapshot(`6740`); + expect(treemapData.nodes).toMatchSnapshot(); }); }); describe('coursehero fixture', () => { - /** @type {LH.Treemap.Node[]} */ + /** @type {LH.Audit.Details.TreemapData} */ let treemapData; beforeAll(async () => { const context = {computedCache: new Map()}; @@ -106,20 +108,22 @@ describe('ScriptTreemapData audit', () => { ScriptElements: [{src: scriptUrl1, content}, {src: scriptUrl2, content}], }; const results = await ScriptTreemapData.audit(artifacts, context); + if (!results.details || results.details.type !== 'treemap-data') { + throw new Error('should not happen.'); + } - // @ts-expect-error: Debug data. - treemapData = results.details.treemapData; + treemapData = results.details; }); it('has nodes', () => { - expect(JSON.stringify(treemapData).length).toMatchInlineSnapshot(`86817`); - expect(treemapData).toMatchSnapshot(); + expect(JSON.stringify(treemapData.nodes).length).toMatchInlineSnapshot(`86817`); + expect(treemapData.nodes).toMatchSnapshot(); }); it('finds duplicates', () => { - expect(JSON.stringify(treemapData).length).toMatchInlineSnapshot(`86817`); + expect(JSON.stringify(treemapData.nodes).length).toMatchInlineSnapshot(`86817`); // @ts-ignore all these children exist. - const leafNode = treemapData[0]. + const leafNode = treemapData.nodes[0]. children[0]. children[0]. children[0]. diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js index d08a53861820..276690bc229b 100644 --- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js +++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js @@ -556,11 +556,13 @@ describe('ReportUIFeatures', () => { describe('treemap button', () => { it('should only show button if treemap data is available', () => { - expect(sampleResults.audits['script-treemap-data']).toBeUndefined(); - expect(render(sampleResults).querySelector('.lh-button.report-icon--treemap')).toBeNull(); + const lhr = JSON.parse(JSON.stringify(sampleResults)); + + expect(lhr.audits['script-treemap-data']).not.toBeUndefined(); + expect(render(lhr).querySelector('.lh-button.report-icon--treemap')).toBeTruthy(); - sampleResults.audits['script-treemap-data'] = {details: {}}; - expect(render(sampleResults).querySelector('.lh-button.report-icon--treemap')).toBeTruthy(); + delete lhr.audits['script-treemap-data']; + expect(render(lhr).querySelector('.lh-button.report-icon--treemap')).toBeNull(); }); }); diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index aab9cd781a92..b2003db5e94a 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -2505,6 +2505,42 @@ } } }, + "script-treemap-data": { + "id": "script-treemap-data", + "title": "Script Treemap Data", + "description": "Used for treemap app", + "score": null, + "scoreDisplayMode": "informative", + "details": { + "type": "treemap-data", + "nodes": [ + { + "name": "http://localhost:10200/dobetterweb/dbw_tester.html", + "resourceBytes": 5806 + }, + { + "name": "http://localhost:10200/dobetterweb/dbw_tester.js", + "resourceBytes": 48 + }, + { + "name": "http://localhost:10200/dobetterweb/empty_module.js?delay=500", + "resourceBytes": 60 + }, + { + "name": "http://localhost:10200/dobetterweb/fcp-delayer.js?delay=5000", + "resourceBytes": 60 + }, + { + "name": "http://localhost:10200/zone.js", + "resourceBytes": 30 + }, + { + "name": "http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js", + "resourceBytes": 63 + } + ] + } + }, "pwa-cross-browser": { "id": "pwa-cross-browser", "title": "Site works cross-browser", @@ -6396,6 +6432,24 @@ "duration": 100, "entryType": "measure" }, + { + "startTime": 0, + "name": "lh:audit:script-treemap-data", + "duration": 100, + "entryType": "measure" + }, + { + "startTime": 0, + "name": "lh:computed:JSBundles", + "duration": 100, + "entryType": "measure" + }, + { + "startTime": 0, + "name": "lh:computed:ModuleDuplication", + "duration": 100, + "entryType": "measure" + }, { "startTime": 0, "name": "lh:audit:pwa-cross-browser", diff --git a/lighthouse-treemap/app/debug.json b/lighthouse-treemap/app/debug.json index a9421d8a8487..9d5c3bd31361 100644 --- a/lighthouse-treemap/app/debug.json +++ b/lighthouse-treemap/app/debug.json @@ -12077,8 +12077,8 @@ "score": null, "scoreDisplayMode": "informative", "details": { - "type": "debugdata", - "treemapData": [ + "type": "treemap-data", + "nodes": [ { "name": "https://www.coursehero.com/", "resourceBytes": 69863 diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index 5edfd5a8c9b0..d3159c556216 100644 --- a/lighthouse-treemap/app/src/main.js +++ b/lighthouse-treemap/app/src/main.js @@ -33,18 +33,14 @@ class TreemapViewer { * @param {HTMLElement} el */ constructor(options, el) { - const treemapDebugData = /** @type {LH.Audit.Details.DebugData} */ ( - options.lhr.audits['script-treemap-data'].details); - if (!treemapDebugData || !treemapDebugData.treemapData) { + const scriptTreemapData = options.lhr.audits['script-treemap-data'].details; + if (!scriptTreemapData || scriptTreemapData.type !== 'treemap-data') { throw new Error('missing script-treemap-data'); } - /** @type {LH.Treemap.Node[]} */ - const scriptNodes = treemapDebugData.treemapData; - /** @type {{[group: string]: LH.Treemap.Node[]}} */ this.depthOneNodesByGroup = { - scripts: scriptNodes, + scripts: scriptTreemapData.nodes, }; /** @@ -78,7 +74,7 @@ class TreemapViewer { this.viewModes; /** @type {RenderState=} */ this.previousRenderState; - /** @type {WeakMap} */ + /** @type {WeakMap} */ this.tableRowToNodeMap = new WeakMap(); /** @type {WebTreeMap} */ this.treemap; @@ -425,7 +421,7 @@ class TreemapViewer { const tableEl = TreemapUtil.find('.lh-table'); tableEl.innerHTML = ''; - /** @type {Array<{node: LH.Treemap.Node, name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */ + /** @type {Array<{node: NodeWithElement, name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */ const data = []; TreemapUtil.walk(this.currentTreemapRoot, (node, path) => { if (node.children) return; diff --git a/lighthouse-treemap/app/src/util.js b/lighthouse-treemap/app/src/util.js index e9c0d8260830..c025d0e9854b 100644 --- a/lighthouse-treemap/app/src/util.js +++ b/lighthouse-treemap/app/src/util.js @@ -14,7 +14,7 @@ class TreemapUtil { /** * @param {LH.Treemap.Node} node - * @param {(node: LH.Treemap.Node, path: string[]) => void} fn + * @param {(node: NodeWithElement, path: string[]) => void} fn * @param {string[]=} path */ static walk(node, fn, path) { diff --git a/lighthouse-treemap/types/treemap.d.ts b/lighthouse-treemap/types/treemap.d.ts index f74fd5fa9cad..90f85a9e8600 100644 --- a/lighthouse-treemap/types/treemap.d.ts +++ b/lighthouse-treemap/types/treemap.d.ts @@ -21,6 +21,11 @@ declare global { viewMode: LH.Treemap.ViewMode; } + interface NodeWithElement extends LH.Treemap.Node { + /** webtreemap adds dom to node data. */ + dom?: HTMLElement; + } + var webtreemap: { TreeMap: typeof WebTreeMap; render(el: HTMLElement, data: any, options: WebTreeMapOptions): void; diff --git a/types/audit-details.d.ts b/types/audit-details.d.ts index b982b7fc0d8b..6eab46c378f0 100644 --- a/types/audit-details.d.ts +++ b/types/audit-details.d.ts @@ -9,6 +9,7 @@ declare global { export type Details = Details.CriticalRequestChain | Details.DebugData | + Details.TreemapData | Details.Filmstrip | Details.List | Details.Opportunity | @@ -97,6 +98,11 @@ declare global { [p: string]: any; } + export interface TreemapData { + type: 'treemap-data'; + nodes: LH.Treemap.Node[]; + } + /** String enum of possible types of values found within table items. */ type ItemValueType = 'bytes' | 'code' | 'link' | 'ms' | 'multi' | 'node' | 'source-location' | 'numeric' | 'text' | 'thumbnail' | 'timespanMs' | 'url'; diff --git a/types/audit.d.ts b/types/audit.d.ts index 131009067bd9..68716939255d 100644 --- a/types/audit.d.ts +++ b/types/audit.d.ts @@ -134,7 +134,7 @@ declare global { /** The unit of `numericValue`, used when the consumer wishes to convert numericValue to a display string. */ numericUnit?: string; /** Extra information about the page provided by some types of audits, in one of several possible forms that can be rendered in the HTML report. */ - details?: FormattedIcu; + details?: FormattedIcu; } export interface Results { diff --git a/types/treemap.d.ts b/types/treemap.d.ts index 74c968fab452..e1ec262512a0 100644 --- a/types/treemap.d.ts +++ b/types/treemap.d.ts @@ -40,8 +40,6 @@ declare global { /** If present, this module is a duplicate. String is normalized source path. See ModuleDuplication.normalizeSource */ duplicatedNormalizedModuleName?: string; children?: Node[]; - /** Added by webtreemap lib. */ - dom?: HTMLElement; } } }