Repository: GoogleChrome/lighthouse
Pull request #9688 (2019-09-17 21:56:00)

report: update favicon

![image](https://user-images.githubusercontent.com/4071474/65082647-50102f00-d95b-11e9-886f-c70c70d1ddb0.png)

This favicon is great because it's lower detailed and kinda-monochrome, so it is much clearer than the current favicon.

This is also the image we will use for the extension browser action image.

brendankenny: Is it kind of blurry or just the screenshot? Is there a crisper version we could generate at that size @yuinchien?
yuin-at-cca: @connorjclark @brendankenny I provided the asset size according to connor's request, if it's still not crisp let me know, i can export 2x and try. 
brendankenny: > @connorjclark @brendankenny I provided the asset size according to connor's request, if it's still not crisp let me know, i can export 2x and try.

I could totally be wrong, I'm only basing this on the screenshot. Here it is as a png:
![little-lighthouse](https://user-images.githubusercontent.com/316891/65084165-6973a400-d978-11e9-9deb-89167c174b05.png)

Maybe we'd need to get into pixel art to get it crisper and we can consider that another time

paulirish: > Here it is as a png:

maybe GH messed with your upload, but that png didnt match what i saw when i viewed the data uri directly.

here's that but zoomed without smoothing:

![image](https://user-images.githubusercontent.com/39191/65084666-c3686f80-d960-11e9-9545-5cf3f6a9e047.png)


pixel-wise.. seems good.
brendankenny: maybe it's a retina thing, then? I don't know how scaling works for favicons, though.

<img width="236" alt="favicons" src="https://user-images.githubusercontent.com/316891/65085128-47c7ec00-d97b-11e9-825c-149c8f32ee58.png">

yuinchien: Seems like we need to export 2x asset for retina. Will make sure to deliver them first thing tomorrow.
connorjclark: yeah looks like 32x32 is a safe bet

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index f2483a6a6..6d28a329d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -73,6 +73,14 @@ module.exports = {
     'valid-jsdoc': 0,
     'arrow-parens': 0,
   },
+  overrides: [
+    {
+      files: ['lighthouse-cli/test/smokehouse/**/*expectations.js'],
+      rules: {
+        'max-len': 0,
+      },
+    },
+  ],
   parserOptions: {
     ecmaVersion: 2018,
     ecmaFeatures: {
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 085e1f0dd..ce9800a5f 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -135,6 +135,7 @@ const defaultConfig = {
       'link-elements',
       'meta-elements',
       'script-elements',
+      'iframe-elements',
       'dobetterweb/appcache',
       'dobetterweb/doctype',
       'dobetterweb/domstats',
diff --git a/lighthouse-core/gather/gatherers/iframe-elements.js b/lighthouse-core/gather/gatherers/iframe-elements.js
new file mode 100644
index 000000000..97aea7d0b
--- /dev/null
+++ b/lighthouse-core/gather/gatherers/iframe-elements.js
@@ -0,0 +1,55 @@
+/**
+ * @license Copyright 2019 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';
+
+const Gatherer = require('./gatherer.js');
+const pageFunctions = require('../../lib/page-functions.js');
+
+/* eslint-env browser, node */
+
+/**
+ * @return {LH.Artifacts['IFrameElements']}
+ */
+/* istanbul ignore next */
+function collectIFrameElements() {
+  // @ts-ignore - put into scope via stringification
+  const iFrameElements = getElementsInDocument('iframe'); // eslint-disable-line no-undef
+  return iFrameElements.map(/** @param {HTMLIFrameElement} node */ (node) => {
+    const clientRect = node.getBoundingClientRect();
+    const {top, bottom, left, right, width, height} = clientRect;
+    return {
+      id: node.id,
+      src: node.src,
+      clientRect: {top, bottom, left, right, width, height},
+      // @ts-ignore - put into scope via stringification
+      isPositionFixed: isPositionFixed(node), // eslint-disable-line no-undef
+    };
+  });
+}
+
+class IFrameElements extends Gatherer {
+  /**
+   * @param {LH.Gatherer.PassContext} passContext
+   * @return {Promise<LH.Artifacts['IFrameElements']>}
+   * @override
+   */
+  async afterPass(passContext) {
+    const driver = passContext.driver;
+
+    const expression = `(() => {
+      ${pageFunctions.getOuterHTMLSnippetString};
+      ${pageFunctions.getElementsInDocumentString};
+      ${pageFunctions.isPositionFixedString};
+      return (${collectIFrameElements})();
+    })()`;
+
+    /** @type {LH.Artifacts['IFrameElements']} */
+    const iframeElements = await driver.evaluateAsync(expression, {useIsolation: true});
+    return iframeElements;
+  }
+}
+
+module.exports = IFrameElements;
diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js
index e6eff94c3..e0e406482 100644
--- a/lighthouse-core/lib/i18n/i18n.js
+++ b/lighthouse-core/lib/i18n/i18n.js
@@ -135,21 +135,6 @@ function lookupLocale(locale) {
   return closestLocale || 'en';
 }
 
-/**
- * Preformat values for the message based on how they're used, like KB or milliseconds.
- * @param {string} icuMessage
- * @param {MessageFormat} messageFormatter
- * @param {Record<string, string | number>} [values]
- * @return {Record<string, string | number>}
- */
-function _preformatValues(icuMessage, messageFormatter, values = {}) {
-  const clonedValues = JSON.parse(JSON.stringify(values));
-
-  const elements = collectAllCustomElementsFromICU(messageFormatter.getAst().elements);
-
-  return _processParsedElements(icuMessage, Array.from(elements.values()), clonedValues);
-}
-
 /**
  * Function to retrieve all 'argumentElement's from an ICU message. An argumentElement
  * is an ICU element with an argument in it, like '{varName}' or '{varName, number, bytes}'. This
@@ -188,16 +173,21 @@ function collectAllCustomElementsFromICU(icuElements, seenElementsById = new Map
 }
 
 /**
- * This function takes a list of ICU argumentElements and a map of values and
- * will apply Lighthouse custom formatting to the values based on the argumentElement
- * format style.
- *
+ * Returns a copy of the `values` object, with the values formatted based on how
+ * they will be used in the `icuMessage`, e.g. KB or milliseconds. The original
+ * object is unchanged.
  * @param {string} icuMessage
- * @param {Array<ArgumentElement>} argumentElements
- * @param {Record<string, string | number>} [values]
+ * @param {MessageFormat} messageFormatter
+ * @param {Readonly<Record<string, string | number>>} values
  * @return {Record<string, string | number>}
  */
-function _processParsedElements(icuMessage, argumentElements, values = {}) {
+function _preformatValues(icuMessage, messageFormatter, values) {
+  const elementMap = collectAllCustomElementsFromICU(messageFormatter.getAst().elements);
+  const argumentElements = [...elementMap.values()];
+
+  /** @type {Record<string, string | number>} */
+  const formattedValues = {};
+
   for (const {id, format} of argumentElements) {
     // Throw an error if a message's value isn't provided
     if (id && (id in values) === false) {
@@ -205,10 +195,14 @@ function _processParsedElements(icuMessage, argumentElements, values = {}) {
         `that wasn't provided`);
     }
 
+    const value = values[id];
+
     // Direct `{id}` replacement and non-numeric values need no formatting.
-    if (!format || format.type !== 'numberFormat') continue;
+    if (!format || format.type !== 'numberFormat') {
+      formattedValues[id] = value;
+      continue;
+    }
 
-    const value = values[id];
     if (typeof value !== 'number') {
       throw new Error(`ICU Message "${icuMessage}" contains a numeric reference ("${id}") ` +
         'but provided value was not a number');
@@ -217,28 +211,34 @@ function _processParsedElements(icuMessage, argumentElements, values = {}) {
     // Format values for known styles.
     if (format.style === 'milliseconds') {
       // Round all milliseconds to the nearest 10.
-      values[id] = Math.round(value / 10) * 10;
+      formattedValues[id] = Math.round(value / 10) * 10;
     } else if (format.style === 'seconds' && id === 'timeInMs') {
       // Convert all seconds to the correct unit (currently only for `timeInMs`).
-      values[id] = Math.round(value / 100) / 10;
+      formattedValues[id] = Math.round(value / 100) / 10;
     } else if (format.style === 'bytes') {
       // Replace all the bytes with KB.
-      values[id] = value / 1024;
+      formattedValues[id] = value / 1024;
+    } else {
+      // For all other number styles, the value isn't changed.
+      formattedValues[id] = value;
     }
   }
 
   // Throw an error if a value is provided but has no placeholder in the message.
   for (const valueId of Object.keys(values)) {
-    // errorCode is a special case always allowed to help ease of LHError use.
-    if (valueId === 'errorCode') continue;
+    if (valueId in formattedValues) continue;
 
-    if (!argumentElements.find(el => el.id === valueId)) {
-      throw new Error(`Provided value "${valueId}" does not match any placeholder in ` +
-        `ICU message "${icuMessage}"`);
+    // errorCode is a special case always allowed to help LHError ease-of-use.
+    if (valueId === 'errorCode') {
+      formattedValues.errorCode = values.errorCode;
+      continue;
     }
+
+    throw new Error(`Provided value "${valueId}" does not match any placeholder in ` +
+      `ICU message "${icuMessage}"`);
   }
 
-  return values;
+  return formattedValues;
 }
 
 /**
@@ -260,7 +260,7 @@ const _ICUMsgNotFoundMsg = 'ICU message not found in destination locale';
  * @param {Record<string, string | number>} [values]
  * @return {{formattedString: string, icuMessage: string}}
  */
-function _formatIcuMessage(locale, icuMessageId, uiStringMessage, values) {
+function _formatIcuMessage(locale, icuMessageId, uiStringMessage, values = {}) {
   const localeMessages = LOCALES[locale];
   if (!localeMessages) throw new Error(`Unsupported locale '${locale}'`);
   let localeMessage = localeMessages[icuMessageId] && localeMessages[icuMessageId].message;
diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js
index e6073668f..d54fc4070 100644
--- a/lighthouse-core/lib/page-functions.js
+++ b/lighthouse-core/lib/page-functions.js
@@ -225,6 +225,43 @@ function getNodeSelector(node) {
   return parts.join(' > ');
 }
 
+/**
+ * This function checks if an element or an ancestor of an element is `position:fixed`.
+ * In addition we ensure that the element is capable of behaving as a `position:fixed`
+ * element, checking that it lives within a scrollable ancestor.
+ * @param {HTMLElement} element
+ * @return {boolean}
+ */
+/* istanbul ignore next */
+function isPositionFixed(element) {
+  /**
+   * @param {HTMLElement} element
+   * @param {string} attr
+   * @return {string}
+   */
+  function getStyleAttrValue(element, attr) {
+    // Check style before computedStyle as computedStyle is expensive.
+    return element.style[attr] || window.getComputedStyle(element)[attr];
+  }
+
+  // Position fixed/sticky has no effect in case when document does not scroll.
+  const htmlEl = document.querySelector('html');
+  if (htmlEl.scrollHeight <= htmlEl.clientHeight ||
+      !['scroll', 'auto', 'visible'].includes(getStyleAttrValue(htmlEl, 'overflowY'))) {
+    return false;
+  }
+
+  let currentEl = element;
+  while (currentEl) {
+    const position = getStyleAttrValue(currentEl, 'position');
+    if ((position === 'fixed' || position === 'sticky')) {
+      return true;
+    }
+    currentEl = currentEl.parentElement;
+  }
+  return false;
+}
+
 /**
  * Generate a human-readable label for the given element, based on end-user facing
  * strings like the innerText or alt attribute.
@@ -280,4 +317,5 @@ module.exports = {
   getNodeSelector: getNodeSelector,
   getNodeLabel: getNodeLabel,
   getNodeLabelString: getNodeLabel.toString(),
+  isPositionFixedString: isPositionFixed.toString(),
 };
diff --git a/lighthouse-core/report/html/report-template.html b/lighthouse-core/report/html/report-template.html
index 830ba2a3e..e5950a40b 100644
--- a/lighthouse-core/report/html/report-template.html
+++ b/lighthouse-core/report/html/report-template.html
@@ -19,7 +19,7 @@ limitations under the License.
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
-  <link rel="icon" href="data:image/png;base64,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">
+  <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEhklEQVR4AWJxL/BhIAesev1U5tcflpncgNrKIsqNIwzC9feMpDUzs70kOczMzMzJJcxwCTMzncPMnOwtzBwzMzPb0vRfeZPp0VhPS5I39V5fdiXV1/VD+9QC7OVn9BsyH1XIoEI1PfmJvLFowVV564+34DFUHudbmfDh4kVXh//7XwE+WjS/YfXZe3yr4j2rqj1AIhSB7hZ8ZtPZu/zw8cK523U4wE1/rvPfWrz4zs0m9ZdC9yUJAlASdBAgocRegfF/f3/h/PuaFsxMdwjAR0vm1+06eMMfIrhLqTWqdH4EumU2SPfMhigJAlRQbZrgrRsl9U+Y2DYDFCz3ILC9kiAiqSrMwbWT0nceEnR+9Kggc2zjOJCASDENkg0a5HfZZgDP81CM3CrQs2Z1+o7DJ6ePr8sK0AOCHv5Jjdt3evyYSaZ351VIStIxPRAUtrBYbxC6w+BZ0ivVSBKkIhJhemSyZpfB00EiPO2VjzYkxhcqXQqCWCShGplvi3y0QxqbuBurMjyJeWnkHZuAEgIQGsUBqwrfjZ+IlBgKyRJzVVYF8O6qFWdh86YzQzMrZigYmxAyfvHgLZQ/LC1CbeniW2Hkqr/PH16SgvGuf2/uzNMBwJA/njxizGPtSyAf7EziJCMGRDRdhoAC4PL1A/SrKQMAAQkEfpJAcRQdrBJ7gNwjSpJsdwK+CANBkqa1LgQB4IicV9nYUct7gaxuDJUErQIiEAiMxLVOFlKzIktPpT0ggpdpC/8YAHnxbgkUY4tAAFSR7AAXNyAAWHJrA/kHGjzg5nleuwFO7Nd/IoDw4Pm58+4jNLmYG0wRA5bErc2Mr3Y+dXTDW1VvwqbJkzMCHQ4S1GTCBOIgUHJrGdEwqzR+jAp/o2qAZelUDoQnruEEdDclJI6576AlNVfc+22XN/+Y1vnJD0Yind6UpEEvn/Hqq15EYjCW7jZCJEpnNvDgkyelDjs106kuux2AAXCSobULOWP8mLhYlpoDMK4qAFXJGk+grtH8YXVz5KJblqaG1+VUdTc0I290bmUQAriGITRbdQnom0aoFj8kx1+wMD2ifncAXUQE4SkDqN1hE0jEophs1SUwZAOhUAiMCLwRtamtTZtbbmZErSAUHbSysaoEmnrsakiMiUAURi283gN6wans9oX8rOCrj7/JP35DFD+iQ7Au/K2KE1jzx6ujjUnXFH9KjEq6ZlhsTBICrNLJf47Pv/pkHzvup1w4dmUbEei0+bcXRqJuh5kVARQ8byyYxOwNGr7A87xh1tp8sGT+uMInrwi++Xj7TQz2d27NvwEkrOflAFQGIDA5khASBCGdO2/Z/MnLPwYfv5TFhjW7QhVKAB6afwe2LpFlFsCnlQEosgQgDsdOG1/LKeNqJS4JCSPJ/i+TakwEARor7gER1Iva5JmPOJK0RUqmoPnnlzFCtmIAhAAQEIQRgDaiYPIauNXcnDlRIrWNFY3hm7PG9YRqr7IV7HrCgAC17befjEvRq2nGhAHtBqDpOuI/I1diUUAMYIxEdyejBJqLnNoszGZtfiX/CztGv2mq+sdaAAAAAElFTkSuQmCC">
   <title>Lighthouse Report</title>
   <style>/*%%LIGHTHOUSE_CSS%%*/</style>
 </head>
diff --git a/lighthouse-viewer/app/index.html b/lighthouse-viewer/app/index.html
index 7c5e24e72..1487e3764 100644
--- a/lighthouse-viewer/app/index.html
+++ b/lighthouse-viewer/app/index.html
@@ -11,7 +11,7 @@ Unless required by applicable law or agreed to in writing, software distributed
   <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
   <title>Lighthouse Report Viewer</title>
   <link rel="manifest" href="manifest.json">
-  <link rel="icon" href="data:image/png;base64,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">
+  <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEhklEQVR4AWJxL/BhIAesev1U5tcflpncgNrKIsqNIwzC9feMpDUzs70kOczMzMzJJcxwCTMzncPMnOwtzBwzMzPb0vRfeZPp0VhPS5I39V5fdiXV1/VD+9QC7OVn9BsyH1XIoEI1PfmJvLFowVV564+34DFUHudbmfDh4kVXh//7XwE+WjS/YfXZe3yr4j2rqj1AIhSB7hZ8ZtPZu/zw8cK523U4wE1/rvPfWrz4zs0m9ZdC9yUJAlASdBAgocRegfF/f3/h/PuaFsxMdwjAR0vm1+06eMMfIrhLqTWqdH4EumU2SPfMhigJAlRQbZrgrRsl9U+Y2DYDFCz3ILC9kiAiqSrMwbWT0nceEnR+9Kggc2zjOJCASDENkg0a5HfZZgDP81CM3CrQs2Z1+o7DJ6ePr8sK0AOCHv5Jjdt3evyYSaZ351VIStIxPRAUtrBYbxC6w+BZ0ivVSBKkIhJhemSyZpfB00EiPO2VjzYkxhcqXQqCWCShGplvi3y0QxqbuBurMjyJeWnkHZuAEgIQGsUBqwrfjZ+IlBgKyRJzVVYF8O6qFWdh86YzQzMrZigYmxAyfvHgLZQ/LC1CbeniW2Hkqr/PH16SgvGuf2/uzNMBwJA/njxizGPtSyAf7EziJCMGRDRdhoAC4PL1A/SrKQMAAQkEfpJAcRQdrBJ7gNwjSpJsdwK+CANBkqa1LgQB4IicV9nYUct7gaxuDJUErQIiEAiMxLVOFlKzIktPpT0ggpdpC/8YAHnxbgkUY4tAAFSR7AAXNyAAWHJrA/kHGjzg5nleuwFO7Nd/IoDw4Pm58+4jNLmYG0wRA5bErc2Mr3Y+dXTDW1VvwqbJkzMCHQ4S1GTCBOIgUHJrGdEwqzR+jAp/o2qAZelUDoQnruEEdDclJI6576AlNVfc+22XN/+Y1vnJD0Yind6UpEEvn/Hqq15EYjCW7jZCJEpnNvDgkyelDjs106kuux2AAXCSobULOWP8mLhYlpoDMK4qAFXJGk+grtH8YXVz5KJblqaG1+VUdTc0I290bmUQAriGITRbdQnom0aoFj8kx1+wMD2ifncAXUQE4SkDqN1hE0jEophs1SUwZAOhUAiMCLwRtamtTZtbbmZErSAUHbSysaoEmnrsakiMiUAURi283gN6wans9oX8rOCrj7/JP35DFD+iQ7Au/K2KE1jzx6ujjUnXFH9KjEq6ZlhsTBICrNLJf47Pv/pkHzvup1w4dmUbEei0+bcXRqJuh5kVARQ8byyYxOwNGr7A87xh1tp8sGT+uMInrwi++Xj7TQz2d27NvwEkrOflAFQGIDA5khASBCGdO2/Z/MnLPwYfv5TFhjW7QhVKAB6afwe2LpFlFsCnlQEosgQgDsdOG1/LKeNqJS4JCSPJ/i+TakwEARor7gER1Iva5JmPOJK0RUqmoPnnlzFCtmIAhAAQEIQRgDaiYPIauNXcnDlRIrWNFY3hm7PG9YRqr7IV7HrCgAC17befjEvRq2nGhAHtBqDpOuI/I1diUUAMYIxEdyejBJqLnNoszGZtfiX/CztGv2mq+sdaAAAAAElFTkSuQmCC">
   <meta name="theme-color" content="#304ffe">
   <link rel="stylesheet" href="styles/viewer.css">
 </head>
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 9e93814da..50b188531 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -61,6 +61,8 @@ declare global {
     export interface PublicGathererArtifacts {
       /** Console deprecation and intervention warnings logged by Chrome during page load. */
       ConsoleMessages: Crdp.Log.EntryAddedEvent[];
+      /** All the iframe elements in the page.*/
+      IFrameElements: Artifacts.IFrameElement[];
       /** Information on size and loading for all the images in the page. Natural size information for `picture` and CSS images is only available if the image was one of the largest 50 images. */
       ImageElements: Artifacts.ImageElement[];
       /** All the link elements on the page or equivalently declared in `Link` headers. @see https://html.spec.whatwg.org/multipage/links.html */
@@ -184,6 +186,24 @@ declare global {
         params: {name: string; value: string}[];
       }
 
+      export interface IFrameElement {
+        /** The `id` attribute of the iframe. */
+        id: string,
+        /** The `src` attribute of the iframe. */
+        src: string,
+        /** The iframe's ClientRect. @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect */
+        clientRect: {
+          top: number;
+          bottom: number;
+          left: number;
+          right: number;
+          width: number;
+          height: number;
+        },
+        /** If the iframe or an ancestor of the iframe is fixed in position. */
+        isPositionFixed: boolean,
+      }
+
       /** @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#Attributes */
       export interface LinkElement {
         /** The `rel` attribute of the link, normalized to lower case. @see https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types */


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index 5932fa49c..3f92a647b 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -1144,6 +1144,9 @@ Object {
         Object {
           "path": "script-elements",
         },
+        Object {
+          "path": "iframe-elements",
+        },
         Object {
           "path": "dobetterweb/appcache",
         },
diff --git a/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html b/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html
index 0f50e9022..32e759932 100644
--- a/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html
+++ b/lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html
@@ -30,7 +30,7 @@
 <!-- Note: these will only fail when using the static-server.js, which supports the ?delay=true param.
      If you're using your own server, the resource will load instantly and the
      stylesheets will be ignored for being below the threshold. -->
-<link rel="stylesheet" href="./dobetterweb/dbw_tester.css?delay=100"> <!-- FAIL, when run under smokehouse -->
+<link rel="stylesheet" href="./dbw_tester.css?delay=100">
 <link rel="stylesheet" href="./unknown404.css?delay=200"> <!-- FAIL -->
 <link rel="stylesheet" href="./dbw_tester.css?delay=2200"> <!-- FAIL -->
 <link rel="stylesheet" href="./dbw_disabled.css?delay=200&isdisabled" disabled> <!-- PASS -->
diff --git a/lighthouse-cli/test/fixtures/oopif.html b/lighthouse-cli/test/fixtures/oopif.html
index df3cb0c1b..d3bdb265c 100644
--- a/lighthouse-cli/test/fixtures/oopif.html
+++ b/lighthouse-cli/test/fixtures/oopif.html
@@ -1,10 +1,8 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <title>Where is my iframe?</title>
-  </head>
   <body>
     <h1>Hello frames</h1>
-    <iframe name="oopif" src="https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/" style="width: 100vw; height: 100vh;"></iframe>
+    <iframe id="oopif" name="oopif" src="https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/" style="width: 100vw; height: 110vh;"></iframe>
+    <iframe id="outer-iframe" src="http://localhost:10200/online-only.html" style="position: fixed"></iframe>
   </body>
 </html>
diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
index 2a2597af7..c6a86c2d9 100644
--- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
+++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
@@ -20,7 +20,7 @@ module.exports = [
         {
           tag: {
             tagName: 'LINK',
-            url: 'http://localhost:10200/dobetterweb/dobetterweb/dbw_tester.css?delay=100',
+            url: 'http://localhost:10200/dobetterweb/dbw_tester.css?delay=100',
           },
         },
         {
@@ -81,9 +81,38 @@ module.exports = [
         'errors-in-console': {
           score: 0,
           details: {
-            items: {
-              length: 8,
-            },
+            items: [
+              {
+                source: 'network',
+                description: 'Failed to load resource: the server responded with a status of 404 (Not Found)',
+                url: 'http://localhost:10200/dobetterweb/unknown404.css?delay=200',
+              },
+              {
+                source: 'other',
+                description: 'Application Cache Error event: Manifest fetch failed (404) http://localhost:10200/dobetterweb/clock.appcache',
+                url: 'http://localhost:10200/dobetterweb/dbw_tester.html',
+              },
+              {
+                source: 'network',
+                description: 'Failed to load resource: the server responded with a status of 404 (Not Found)',
+                url: 'http://localhost:10200/dobetterweb/fcp-delayer.js?delay=5000',
+              },
+              {
+                source: 'network',
+                description: 'Failed to load resource: the server responded with a status of 404 (Not Found)',
+                url: 'http://localhost:10200/favicon.ico',
+              },
+              {
+                source: 'network',
+                description: 'Failed to load resource: the server responded with a status of 404 (Not Found)',
+                url: 'http://localhost:10200/dobetterweb/unknown404.css?delay=200',
+              },
+              {
+                source: 'Runtime.exception',
+                description: 'Error: An error\n    at http://localhost:10200/dobetterweb/dbw_tester.html:57:38',
+                url: 'http://localhost:10200/dobetterweb/dbw_tester.html',
+              },
+            ],
           },
         },
         'is-on-https': {
@@ -143,7 +172,7 @@ module.exports = [
           details: {
             items: [
               {
-                url: 'http://localhost:10200/dobetterweb/dobetterweb/dbw_tester.css?delay=100',
+                url: 'http://localhost:10200/dobetterweb/dbw_tester.css?delay=100',
               },
               {
                 url: 'http://localhost:10200/dobetterweb/unknown404.css?delay=200',
diff --git a/lighthouse-cli/test/smokehouse/oopif-config.js b/lighthouse-cli/test/smokehouse/oopif-config.js
index ebc5fa22b..bc069999c 100644
--- a/lighthouse-cli/test/smokehouse/oopif-config.js
+++ b/lighthouse-cli/test/smokehouse/oopif-config.js
@@ -10,9 +10,4 @@
  */
 module.exports = {
   extends: 'lighthouse:default',
-  settings: {
-    onlyAudits: [
-      'network-requests',
-    ],
-  },
 };
diff --git a/lighthouse-cli/test/smokehouse/oopif-expectations.js b/lighthouse-cli/test/smokehouse/oopif-expectations.js
index a73509e06..5c624c999 100644
--- a/lighthouse-cli/test/smokehouse/oopif-expectations.js
+++ b/lighthouse-cli/test/smokehouse/oopif-expectations.js
@@ -27,5 +27,27 @@ module.exports = [
         },
       },
     },
+    artifacts: {
+      IFrameElements: [
+        {
+          id: 'oopif',
+          src: 'https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/',
+          clientRect: {
+            width: '>0',
+            height: '>0',
+          },
+          isPositionFixed: false,
+        },
+        {
+          id: 'outer-iframe',
+          src: 'http://localhost:10200/online-only.html',
+          clientRect: {
+            width: '>0',
+            height: '>0',
+          },
+          isPositionFixed: true,
+        },
+      ],
+    },
   },
 ];
diff --git a/lighthouse-core/test/lib/i18n/i18n-test.js b/lighthouse-core/test/lib/i18n/i18n-test.js
index f78ed3d9c..d4dcb7d25 100644
--- a/lighthouse-core/test/lib/i18n/i18n-test.js
+++ b/lighthouse-core/test/lib/i18n/i18n-test.js
@@ -81,7 +81,6 @@ describe('i18n', () => {
       expect(formattedStr).toEqual('happy test');
     });
 
-
     it('returns the formatted string with replacements', () => {
       const UIStrings = {testMessage: 'replacement test ({errorCode})'};
       const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
@@ -97,6 +96,25 @@ describe('i18n', () => {
       expect(_ => i18n.getFormatted(str_(UIStrings.testMessage), 'still-not-a-locale'))
         .toThrow(`Unsupported locale 'still-not-a-locale'`);
     });
+
+    it('does not alter the passed-in replacement values object', () => {
+      const UIStrings = {
+        testMessage: 'needs {count, number, bytes}KB test {str} in {timeInMs, number, seconds}s',
+      };
+      const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
+
+      const replacements = {
+        count: 2555,
+        str: '*units*',
+        timeInMs: 314159265,
+      };
+      const replacementsClone = JSON.parse(JSON.stringify(replacements));
+
+      const formattedStr = i18n.getFormatted(str_(UIStrings.testMessage, replacements), 'en');
+      expect(formattedStr).toEqual('needs 2KB test *units* in 314,159.3s');
+
+      expect(replacements).toEqual(replacementsClone);
+    });
   });
 
   describe('#lookupLocale', () => {
@@ -265,5 +283,13 @@ describe('i18n', () => {
         // eslint-disable-next-line max-len
         .toThrow(`Provided value "sirNotAppearingInThisString" does not match any placeholder in ICU message "Hello {timeInMs, number, seconds} World"`);
     });
+
+    it('formats correctly with NaN and Infinity numeric values', () => {
+      const helloInfinityStr = str_(UIStrings.helloBytesWorld, {in: Infinity});
+      expect(helloInfinityStr).toBeDisplayString('Hello ∞ World');
+
+      const helloNaNStr = str_(UIStrings.helloBytesWorld, {in: NaN});
+      expect(helloNaNStr).toBeDisplayString('Hello NaN World');
+    });
   });
 });