Repository: GoogleChrome/lighthouse
Pull request #12392 (2021-04-22 00:16:28)

report: add treemap button, refactor icon styles
Only shows if there is treemap data present.

We can also use this new icon button in devtools for the "View Trace" button.

ref #11254

### tool menu before

![image](https://user-images.githubusercontent.com/4071474/116304974-5df57b00-a760-11eb-9d19-39c27b2b352f.png)
![image](https://user-images.githubusercontent.com/4071474/116305025-6d74c400-a760-11eb-80b4-4921f5b20601.png)


## second pass

![image](https://user-images.githubusercontent.com/4071474/116306623-8a11fb80-a762-11eb-87e1-64d4af5e6c57.png)
![image](https://user-images.githubusercontent.com/4071474/116306658-94cc9080-a762-11eb-96ab-1b39a7a5a334.png)



## first pass

![image](https://user-images.githubusercontent.com/4071474/115637465-716e9500-a2cd-11eb-8e5e-d2ac86a44e45.png)

hover

![image](https://user-images.githubusercontent.com/4071474/115637479-77647600-a2cd-11eb-8b84-a5eac9107bed.png)



connorjclark: > Could we get the shape and style of this new button to match "View Trace"? Or are you planning to change "View Trace" down the line when treemap moves into the default config?

![image](https://user-images.githubusercontent.com/4071474/115771816-9f0a1b80-a36b-11eb-8da6-ab6cce7ebba0.png)
yeah i'll tweak the icon button a bit.

I planned to change the view trace button to use this icon button next release.
connorjclark: Updated first post with new screenshots. I refactored `tool-icon` (renamed `report-icon`) to be reusable, and for the invert trick we use for the icon to not bleed over into other parts of the element (ty psuedoelements)

Patch:
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index 0ced62626..889621601 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -2087,6 +2087,9 @@
   "lighthouse-core/report/html/renderer/util.js | varianceDisclaimer": {
     "message": "Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics."
   },
+  "lighthouse-core/report/html/renderer/util.js | viewTreemapLabel": {
+    "message": "View Treemap"
+  },
   "lighthouse-core/report/html/renderer/util.js | warningAuditsGroupTitle": {
     "message": "Passed audits but with warnings"
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 775cb3a4f..5b90f1951 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -2087,6 +2087,9 @@
   "lighthouse-core/report/html/renderer/util.js | varianceDisclaimer": {
     "message": "V̂ál̂úêś âŕê éŝt́îḿât́êd́ âńd̂ ḿâý v̂ár̂ý. T̂h́ê [ṕêŕf̂ór̂ḿâńĉé ŝćôŕê íŝ ćâĺĉúl̂át̂éd̂](https://web.dev/performance-scoring/) d́îŕêćt̂ĺŷ f́r̂óm̂ t́ĥéŝé m̂ét̂ŕîćŝ."
   },
+  "lighthouse-core/report/html/renderer/util.js | viewTreemapLabel": {
+    "message": "V̂íêẃ T̂ŕêém̂áp̂"
+  },
   "lighthouse-core/report/html/renderer/util.js | warningAuditsGroupTitle": {
     "message": "P̂áŝśêd́ âúd̂ít̂ś b̂út̂ ẃît́ĥ ẃâŕn̂ín̂ǵŝ"
   },
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 9d063abef..5658f81ab 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -151,6 +151,16 @@ class ReportUIFeatures {
       toggleInputEl.checked = true;
     }
 
+    const showTreemapApp =
+      this.json.audits['script-treemap-data'] && this.json.audits['script-treemap-data'].details;
+    if (showTreemapApp) {
+      this.addButton({
+        text: Util.i18n.strings.viewTreemapLabel,
+        icon: 'treemap',
+        onClick: () => ReportUIFeatures.openTreemap(this.json),
+      });
+    }
+
     // Fill in all i18n data.
     for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
       // These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
@@ -169,6 +179,25 @@ class ReportUIFeatures {
     this._templateContext = context;
   }
 
+  /**
+   * @param {{text: string, icon?: string, onClick: () => void}} opts
+   */
+  addButton(opts) {
+    const metricsEl = this._dom.find('.lh-audit-group--metrics', this._document);
+    const classes = [
+      'lh-button',
+    ];
+    if (opts.icon) {
+      classes.push('report-icon');
+      classes.push(`report-icon--${opts.icon}`);
+    }
+    const buttonEl = this._dom.createChildOf(metricsEl, 'button', classes.join(' '));
+    buttonEl.addEventListener('click', opts.onClick);
+    buttonEl.textContent = opts.text;
+    metricsEl.append(buttonEl);
+    return buttonEl;
+  }
+
   /**
    * Finds the first scrollable ancestor of `element`. Falls back to the document.
    * @param {Element} element
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index e9bbb9dda..04704d614 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -560,6 +560,8 @@ Util.UIStrings = {
 
   /** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */
   thirdPartyResourcesLabel: 'Show 3rd-party resources',
+  /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data releated to the reports (script bytes and coverage, resource breakdown, etc.) */
+  viewTreemapLabel: 'View Treemap',
 
   /** Option in a dropdown menu that opens a small, summary report in a print dialog.  */
   dropdownPrintSummary: 'Print Summary',
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index f34d02119..18107a56f 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -116,9 +116,10 @@
   --report-font-family-monospace: 'Roboto Mono', 'Menlo', 'dejavu sans mono', 'Consolas', 'Lucida Console', monospace;
   --report-font-family: Roboto, Helvetica, Arial, sans-serif;
   --report-font-size: 16px;
-  --report-monospace-font-size: calc(var(--report-font-size) * 0.85);
+  --report-icon-size: var(--score-icon-background-size);
   --report-line-height: 24px;
   --report-min-width: 400px;
+  --report-monospace-font-size: calc(var(--report-font-size) * 0.85);
   --report-text-color-secondary: var(--color-gray-800);
   --report-text-color: var(--color-gray-900);
   --report-width: calc(60 * var(--report-font-size));
@@ -139,7 +140,6 @@
   --sticky-header-background-color: var(--report-background-color);
   --table-higlight-background-color: hsla(0, 0%, 75%, 0.1);
   --tools-icon-color: var(--color-gray-600);
-  --tools-icon-size: var(--score-icon-background-size);
   --topbar-background-color: var(--color-gray-100);
   --topbar-height: 32px;
   --topbar-logo-size: 24px;
@@ -394,7 +394,11 @@
 /* Report header */
 
 .report-icon {
+  display: flex;
+  align-items: center;
+  padding: 10px 12px;
   opacity: 0.7;
+  cursor: pointer;
 }
 .report-icon:hover {
   opacity: 1;
@@ -404,21 +408,47 @@
   pointer-events: none;
 }
 
-.report-icon--print {
+.report-icon::before {
+  content: "";
+  margin-right: 5px;
+  background-repeat: no-repeat;
+  width: var(--report-icon-size);
+  height: var(--report-icon-size);
+}
+.dark .report-icon::before {
+  filter: invert(1);
+}
+.report-icon--print::before {
   background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z"/><path fill="none" d="M0 0h24v24H0z"/></svg>');
 }
-.report-icon--copy {
+.report-icon--copy::before {
   background-image: url('data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>');
 }
-.report-icon--open {
+.report-icon--open::before {
   background-image: url('data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>');
 }
-.report-icon--download {
+.report-icon--download::before {
   background-image: url('data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
 }
-.report-icon--dark {
+.report-icon--dark::before {
   background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 100 125"><path d="M50 23.587c-16.27 0-22.799 12.574-22.799 21.417 0 12.917 10.117 22.451 12.436 32.471h20.726c2.32-10.02 12.436-19.554 12.436-32.471 0-8.843-6.528-21.417-22.799-21.417zM39.637 87.161c0 3.001 1.18 4.181 4.181 4.181h.426l.41 1.231C45.278 94.449 46.042 95 48.019 95h3.963c1.978 0 2.74-.551 3.365-2.427l.409-1.231h.427c3.002 0 4.18-1.18 4.18-4.181V80.91H39.637v6.251zM50 18.265c1.26 0 2.072-.814 2.072-2.073v-9.12C52.072 5.813 51.26 5 50 5c-1.259 0-2.072.813-2.072 2.073v9.12c0 1.259.813 2.072 2.072 2.072zM68.313 23.727c.994.774 2.135.634 2.91-.357l5.614-7.187c.776-.992.636-2.135-.356-2.909-.992-.776-2.135-.636-2.91.357l-5.613 7.186c-.778.993-.636 2.135.355 2.91zM91.157 36.373c-.306-1.222-1.291-1.815-2.513-1.51l-8.85 2.207c-1.222.305-1.814 1.29-1.51 2.512.305 1.223 1.291 1.814 2.513 1.51l8.849-2.206c1.223-.305 1.816-1.291 1.511-2.513zM86.757 60.48l-8.331-3.709c-1.15-.512-2.225-.099-2.736 1.052-.512 1.151-.1 2.224 1.051 2.737l8.33 3.707c1.15.514 2.225.101 2.736-1.05.513-1.149.1-2.223-1.05-2.737zM28.779 23.37c.775.992 1.917 1.131 2.909.357.992-.776 1.132-1.917.357-2.91l-5.615-7.186c-.775-.992-1.917-1.132-2.909-.357s-1.131 1.917-.356 2.909l5.614 7.187zM21.715 39.583c.305-1.223-.288-2.208-1.51-2.513l-8.849-2.207c-1.222-.303-2.208.289-2.513 1.511-.303 1.222.288 2.207 1.511 2.512l8.848 2.206c1.222.304 2.208-.287 2.513-1.509zM21.575 56.771l-8.331 3.711c-1.151.511-1.563 1.586-1.05 2.735.511 1.151 1.586 1.563 2.736 1.052l8.331-3.711c1.151-.511 1.563-1.586 1.05-2.735-.512-1.15-1.585-1.562-2.736-1.052z"/></svg>');
 }
+.report-icon--treemap::before {
+  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="black"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 5v14h19V5H3zm2 2h15v4H5V7zm0 10v-4h4v4H5zm6 0v-4h9v4h-9z"/></svg>');
+}
+
+.lh-button {
+  margin: 10px;
+  height: 30px;
+  border: 1px solid var(--color-gray-600);
+  border-radius: 4px;
+  font-weight: bold;
+  color: rgb(26, 115, 232);
+  background-color: var(--report-background-color);
+}
+.dark .lh-button {
+  color: var(--color-blue-200);
+}
 
 /* Node */
 .lh-node__snippet {
@@ -787,8 +817,6 @@
   background: var(--color-fail);
 }
 
-
-
 /* Filmstrip */
 
 .lh-filmstrip-container {
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index 512a301f6..623ee7aac 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -268,11 +268,11 @@ limitations under the License.
     .lh-tools {
       margin-left: auto;
       will-change: transform;
-      min-width: var(--tools-icon-size);
+      min-width: var(--report-icon-size);
     }
     .lh-tools__button {
-      width: var(--tools-icon-size);
-      height: var(--tools-icon-size);
+      width: var(--report-icon-size);
+      height: var(--report-icon-size);
       cursor: pointer;
       margin-right: 5px;
       /* This is actually a button element, but we want to style it like a transparent div. */
@@ -292,7 +292,7 @@ limitations under the License.
     }
     .lh-tools__button.active + .lh-tools__dropdown {
       opacity: 1;
-      clip: rect(-1px, 187px, 242px, -3px);
+      clip: rect(-1px, 194px, 242px, -3px);
       visibility: visible;
     }
     .lh-tools__dropdown {
@@ -312,7 +312,6 @@ limitations under the License.
       transition: all 200ms cubic-bezier(0,0,0.2,1);
     }
     .lh-tools__dropdown a {
-      display: block;
       color: currentColor;
       text-decoration: none;
       white-space: nowrap;
@@ -324,27 +323,6 @@ limitations under the License.
       background-color: var(--color-gray-200);
       outline: none;
     }
-    .lh-tools__dropdown .report-icon {
-      cursor: pointer;
-      background-repeat: no-repeat;
-      background-position: 8px 50%;
-      background-size: 18px;
-      background-color: transparent;
-      text-indent: 18px;
-    }
-    .dark .report-icon {
-      color: var(--color-gray-900);
-      filter: invert(1);
-    }
-    .dark .lh-tools__dropdown a:hover,
-    .dark .lh-tools__dropdown a:focus {
-      background-color: #BDBDBD;
-    }
-    /* copy icon needs slight adjustments to look great */
-    .lh-tools__dropdown .report-icon--copy {
-      background-size: 16px;
-      background-position: 9px 50%;
-    }
     /* save-as-gist option hidden in report */
     .lh-tools__dropdown .lh-tools--gist {
       display: none;
@@ -403,7 +381,7 @@ limitations under the License.
     <a href="" class="lh-topbar__url" target="_blank" rel="noopener"></a>
 
     <div class="lh-tools">
-      <button id="lh-tools-button" class="report-icon report-icon--share lh-tools__button" title="Tools menu" aria-label="Toggle report tools menu" aria-haspopup="menu" aria-expanded="false" aria-controls="lh-tools-dropdown">
+      <button id="lh-tools-button" class="lh-tools__button" title="Tools menu" aria-label="Toggle report tools menu" aria-haspopup="menu" aria-expanded="false" aria-controls="lh-tools-dropdown">
         <svg width="100%" height="100%" viewBox="0 0 24 24">
             <path d="M0 0h24v24H0z" fill="none"/>
             <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto
index dfdec7b56..3388ec3e1 100644
--- a/proto/lighthouse-result.proto
+++ b/proto/lighthouse-result.proto
@@ -485,6 +485,9 @@ message I18n {
 
     // Label for a row in a table that shows the version of the Axe library used
     string runtime_settings_axe_version = 46;
+
+    // Label for a button that opens the Treemap App
+    string view_treemap_label = 47;
   }
 
   // The message holding all formatted strings


Test patch:
diff --git a/lighthouse-cli/test/fixtures/csp.html b/lighthouse-cli/test/fixtures/csp.html
new file mode 100644
index 000000000..2a0dae1f0
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/csp.html
@@ -0,0 +1,16 @@
+<!--
+ * Copyright 2021 The Lighthouse Authors. 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.
+-->
+<html>
+  <head>
+    <script>
+      // Use a source map so we can test the injected iframe fetcher.
+      //# sourceMappingURL=http://localhost:10200/source-map/script.js.map
+    </script>
+  </head>
+  <body>
+    <h1>Test this page with a CSP</h1>
+  </body>
+</html>
diff --git a/lighthouse-cli/test/fixtures/static-server.js b/lighthouse-cli/test/fixtures/static-server.js
index 44f12f045..94644b039 100644
--- a/lighthouse-cli/test/fixtures/static-server.js
+++ b/lighthouse-cli/test/fixtures/static-server.js
@@ -17,7 +17,7 @@ const mime = require('mime-types');
 const parseQueryString = require('querystring').parse;
 const parseURL = require('url').parse;
 const URLSearchParams = require('url').URLSearchParams;
-const HEADER_SAFELIST = new Set(['x-robots-tag', 'link']);
+const HEADER_SAFELIST = new Set(['x-robots-tag', 'link', 'content-security-policy']);
 
 const lhRootDirPath = path.join(__dirname, '../../../');
 
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/core-tests.js b/lighthouse-cli/test/smokehouse/test-definitions/core-tests.js
index 273dfde63..7ab882157 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/core-tests.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/core-tests.js
@@ -88,6 +88,10 @@ const smokeTests = [{
   id: 'screenshot',
   expectations: require('./screenshot/expectations.js'),
   config: require('./screenshot/screenshot-config.js'),
+}, {
+  id: 'csp',
+  expectations: require('./csp/csp-expectations.js'),
+  config: require('./csp/csp-config.js'),
 }];
 
 module.exports = smokeTests;
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-config.js b/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-config.js
new file mode 100644
index 000000000..cf27a373a
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-config.js
@@ -0,0 +1,11 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. 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';
+
+/** @type {LH.Config.Json} */
+module.exports = {
+  extends: 'lighthouse:default',
+};
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js
new file mode 100644
index 000000000..51b4fdad0
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js
@@ -0,0 +1,90 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. 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';
+
+/**
+ * @param {[string, string][]} headers
+ * @return {string}
+ */
+function headersParam(headers) {
+  const headerString = new URLSearchParams(headers).toString();
+  return new URLSearchParams([['extra_header', headerString]]).toString();
+}
+
+// Only allow the empty script with the source map.
+// Hash generated using https://strict-csp-codelab.glitch.me/csp_sha256_util.html
+// Easiest way to get script contents with whitespace is by copying script node in DevTools.
+const blockAllExceptInlineScriptCsp = headersParam([[
+  'Content-Security-Policy',
+  `default-src 'none'; script-src 'sha256-qZLV55/xxILbIrha9pgX0OdkZMhOlaIgfpEo/6Dly2U='`,
+]]);
+
+/**
+ * @type {Array<Smokehouse.ExpectedRunnerResult>}
+ */
+module.exports = [
+  {
+    artifacts: {
+      RobotsTxt: {
+        status: 404,
+        content: null,
+      },
+      InspectorIssues: {contentSecurityPolicy: []},
+      SourceMaps: [{
+        sourceMapUrl: 'http://localhost:10200/source-map/script.js.map',
+        map: {},
+        errorMessage: undefined,
+      }],
+    },
+    lhr: {
+      requestedUrl: 'http://localhost:10200/csp.html',
+      finalUrl: 'http://localhost:10200/csp.html',
+      audits: {},
+    },
+  },
+  {
+    artifacts: {
+      RobotsTxt: {
+        status: null,
+        content: null,
+      },
+      InspectorIssues: {
+        contentSecurityPolicy: [
+          {
+            // TODO: Fix connect-src violation when fetching robots.txt.
+            // https://github.com/GoogleChrome/lighthouse/issues/10225
+            blockedURL: 'http://localhost:10200/robots.txt',
+            violatedDirective: 'connect-src',
+            isReportOnly: false,
+            contentSecurityPolicyViolationType: 'kURLViolation',
+          },
+          {
+            // TODO: Fix style-src-elem violation when checking tap targets.
+            // https://github.com/GoogleChrome/lighthouse/issues/11862
+            violatedDirective: 'style-src-elem',
+            isReportOnly: false,
+            contentSecurityPolicyViolationType: 'kInlineViolation',
+          },
+        ],
+      },
+      SourceMaps: [{
+        // Doesn't trigger a CSP violation because iframe is injected after InspectorIssues gatherer finishes.
+        // https://github.com/GoogleChrome/lighthouse/pull/12044#issuecomment-788274938
+        //
+        // Fixed with new fetcher using M92.
+        _maxChromiumMilestone: 91,
+        sourceMapUrl: 'http://localhost:10200/source-map/script.js.map',
+        errorMessage: 'Error: Timed out fetching resource.',
+        map: undefined,
+      }],
+    },
+    lhr: {
+      requestedUrl: 'http://localhost:10200/csp.html?' + blockAllExceptInlineScriptCsp,
+      finalUrl: 'http://localhost:10200/csp.html?' + blockAllExceptInlineScriptCsp,
+      audits: {},
+    },
+  },
+];
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 fde6aa5ba..767d36b45 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
@@ -35,6 +35,7 @@ describe('ReportUIFeatures', () => {
 
   /**
    * @param {LH.JSON} lhr
+   * @return {HTMLElement}
    */
   function render(lhr) {
     const detailsRenderer = new DetailsRenderer(dom);
@@ -553,6 +554,16 @@ 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();
+
+      sampleResults.audits['script-treemap-data'] = {details: {}};
+      expect(render(sampleResults).querySelector('.lh-button.report-icon--treemap')).toBeTruthy();
+    });
+  });
+
   describe('data-i18n', () => {
     it('should have only valid data-i18n values in template', () => {
       const container = render(sampleResults);
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index a51d1b3e6..ae4ee1fe3 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -7024,6 +7024,7 @@
       "throttlingProvided": "Provided by environment",
       "toplevelWarningsMessage": "There were issues affecting this run of Lighthouse:",
       "varianceDisclaimer": "Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.",
+      "viewTreemapLabel": "View Treemap",
       "warningAuditsGroupTitle": "Passed audits but with warnings",
       "warningHeader": "Warnings: "
     },