Repository: GoogleChrome/lighthouse
Pull request #12764 (2021-07-08 20:51:03)

core(fr): limit scope of audits to applicable modes
**Summary**
Updates several more audits to accurately reflect their supported gather modes. Most of this is restricting the supportedMode to navigation even if the artifacts are available, a few optional notApplicable states, and then minor gatherer fixes too.

The remaining ones are due to...

- `simulate` throttlingMethod still being default in timespan (tracked by #11313)
- Opportunities that haven't been converted yet but are still on the burndown.
- Network analysis failing when there are no network records at all.

**Related Issues/PRs**
ref #11313 



Patch:
diff --git a/lighthouse-core/audits/apple-touch-icon.js b/lighthouse-core/audits/apple-touch-icon.js
index 121d8f211..3b5387d4a 100644
--- a/lighthouse-core/audits/apple-touch-icon.js
+++ b/lighthouse-core/audits/apple-touch-icon.js
@@ -38,6 +38,7 @@ class AppleTouchIcon extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['LinkElements'],
     };
   }
diff --git a/lighthouse-core/audits/dobetterweb/geolocation-on-start.js b/lighthouse-core/audits/dobetterweb/geolocation-on-start.js
index ba3a3b0e3..0c04104ae 100644
--- a/lighthouse-core/audits/dobetterweb/geolocation-on-start.js
+++ b/lighthouse-core/audits/dobetterweb/geolocation-on-start.js
@@ -37,6 +37,7 @@ class GeolocationOnStart extends ViolationAudit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['ConsoleMessages'],
     };
   }
diff --git a/lighthouse-core/audits/dobetterweb/notification-on-start.js b/lighthouse-core/audits/dobetterweb/notification-on-start.js
index 7f32402ac..4959cb180 100644
--- a/lighthouse-core/audits/dobetterweb/notification-on-start.js
+++ b/lighthouse-core/audits/dobetterweb/notification-on-start.js
@@ -37,6 +37,7 @@ class NotificationOnStart extends ViolationAudit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['ConsoleMessages'],
     };
   }
diff --git a/lighthouse-core/audits/final-screenshot.js b/lighthouse-core/audits/final-screenshot.js
index 9628f5ce1..a67fd069b 100644
--- a/lighthouse-core/audits/final-screenshot.js
+++ b/lighthouse-core/audits/final-screenshot.js
@@ -20,7 +20,7 @@ class FinalScreenshot extends Audit {
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
       title: 'Final Screenshot',
       description: 'The last screenshot captured of the pageload.',
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'GatherContext'],
     };
   }
 
@@ -37,6 +37,10 @@ class FinalScreenshot extends Audit {
     const finalScreenshot = screenshots[screenshots.length - 1];
 
     if (!finalScreenshot) {
+      // If a timespan didn't happen to contain frames, that's fine. Just mark not applicable.
+      if (artifacts.GatherContext.gatherMode === 'timespan') return {notApplicable: true, score: 1};
+
+      // If it was another mode, that's a fatal error.
       throw new LHError(LHError.errors.NO_SCREENSHOTS);
     }
 
diff --git a/lighthouse-core/audits/font-display.js b/lighthouse-core/audits/font-display.js
index 88ec63517..921f19f02 100644
--- a/lighthouse-core/audits/font-display.js
+++ b/lighthouse-core/audits/font-display.js
@@ -48,6 +48,7 @@ class FontDisplay extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['devtoolsLogs', 'CSSUsage', 'URL'],
     };
   }
diff --git a/lighthouse-core/audits/installable-manifest.js b/lighthouse-core/audits/installable-manifest.js
index 18a0d3767..7ae7ecabb 100644
--- a/lighthouse-core/audits/installable-manifest.js
+++ b/lighthouse-core/audits/installable-manifest.js
@@ -120,6 +120,7 @@ class InstallableManifest extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['URL', 'WebAppManifest', 'InstallabilityErrors'],
     };
   }
diff --git a/lighthouse-core/audits/maskable-icon.js b/lighthouse-core/audits/maskable-icon.js
index e60e09569..49f2f9eec 100644
--- a/lighthouse-core/audits/maskable-icon.js
+++ b/lighthouse-core/audits/maskable-icon.js
@@ -42,6 +42,7 @@ class MaskableIcon extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['WebAppManifest', 'InstallabilityErrors'],
     };
   }
diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js
index ea444e23e..38cf845a2 100644
--- a/lighthouse-core/audits/metrics.js
+++ b/lighthouse-core/audits/metrics.js
@@ -28,6 +28,7 @@ class Metrics extends Audit {
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
       title: 'Metrics',
       description: 'Collects all available metrics.',
+      supportedModes: ['navigation'],
       requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
diff --git a/lighthouse-core/audits/performance-budget.js b/lighthouse-core/audits/performance-budget.js
index 217b51537..e737e78ea 100644
--- a/lighthouse-core/audits/performance-budget.js
+++ b/lighthouse-core/audits/performance-budget.js
@@ -39,6 +39,7 @@ class ResourceBudget extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
+      supportedModes: ['navigation'],
       requiredArtifacts: ['devtoolsLogs', 'URL'],
     };
   }
diff --git a/lighthouse-core/audits/screenshot-thumbnails.js b/lighthouse-core/audits/screenshot-thumbnails.js
index 260935d23..277cc720b 100644
--- a/lighthouse-core/audits/screenshot-thumbnails.js
+++ b/lighthouse-core/audits/screenshot-thumbnails.js
@@ -25,7 +25,7 @@ class ScreenshotThumbnails extends Audit {
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
       title: 'Screenshot Thumbnails',
       description: 'This is what the load of your site looked like.',
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'GatherContext'],
     };
   }
 
@@ -70,7 +70,7 @@ class ScreenshotThumbnails extends Audit {
    * @param {LH.Audit.Context} context
    * @return {Promise<LH.Audit.Product>}
    */
-  static async audit(artifacts, context) {
+  static async _audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     /** @type {Map<SpeedlineFrame, string>} */
     const cachedThumbnails = new Map();
@@ -133,6 +133,31 @@ class ScreenshotThumbnails extends Audit {
       },
     };
   }
+
+  /**
+   * @param {LH.Artifacts} artifacts
+   * @param {LH.Audit.Context} context
+   * @return {Promise<LH.Audit.Product>}
+   */
+  static async audit(artifacts, context) {
+    try {
+      return await this._audit(artifacts, context);
+    } catch (err) {
+      const noFramesErrors = new Set([
+        LHError.errors.NO_SCREENSHOTS.code,
+        LHError.errors.SPEEDINDEX_OF_ZERO.code,
+        LHError.errors.NO_SPEEDLINE_FRAMES.code,
+        LHError.errors.INVALID_SPEEDLINE.code,
+      ]);
+
+      // If a timespan didn't happen to contain frames, that's fine. Just mark not applicable.
+      if (noFramesErrors.has(err.code) && artifacts.GatherContext.gatherMode === 'timespan') {
+        return {notApplicable: true, score: 1};
+      }
+
+      throw err;
+    }
+  }
 }
 
 module.exports = ScreenshotThumbnails;
diff --git a/lighthouse-core/audits/seo/canonical.js b/lighthouse-core/audits/seo/canonical.js
index 6cb0ce20e..0c4c38ae8 100644
--- a/lighthouse-core/audits/seo/canonical.js
+++ b/lighthouse-core/audits/seo/canonical.js
@@ -68,6 +68,7 @@ class Canonical extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['LinkElements', 'URL', 'devtoolsLogs'],
     };
   }
diff --git a/lighthouse-core/audits/seo/hreflang.js b/lighthouse-core/audits/seo/hreflang.js
index 852673e46..99bf3e1e2 100644
--- a/lighthouse-core/audits/seo/hreflang.js
+++ b/lighthouse-core/audits/seo/hreflang.js
@@ -79,6 +79,7 @@ class Hreflang extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['LinkElements', 'URL'],
     };
   }
diff --git a/lighthouse-core/audits/seo/http-status-code.js b/lighthouse-core/audits/seo/http-status-code.js
index 6fd28525b..8b48a9252 100644
--- a/lighthouse-core/audits/seo/http-status-code.js
+++ b/lighthouse-core/audits/seo/http-status-code.js
@@ -6,10 +6,11 @@
 'use strict';
 
 const Audit = require('../audit.js');
-const MainResource = require('../../computed/main-resource.js');
+const NetworkRecords = require('../../computed/network-records.js');
 const HTTP_UNSUCCESSFUL_CODE_LOW = 400;
 const HTTP_UNSUCCESSFUL_CODE_HIGH = 599;
 const i18n = require('../../lib/i18n/i18n.js');
+const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
 
 const UIStrings = {
   /** Title of a Lighthouse audit that provides detail on the HTTP status code a page responds with. This descriptive title is shown when the page has responded with a valid HTTP status code. */
@@ -33,7 +34,7 @@ class HTTPStatusCode extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
-      requiredArtifacts: ['devtoolsLogs', 'URL'],
+      requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
     };
   }
 
@@ -42,26 +43,30 @@ class HTTPStatusCode extends Audit {
    * @param {LH.Audit.Context} context
    * @return {Promise<LH.Audit.Product>}
    */
-  static audit(artifacts, context) {
+  static async audit(artifacts, context) {
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const URL = artifacts.URL;
+    const networkRecords = await NetworkRecords.request(devtoolsLog, context);
+    const mainResource = NetworkAnalyzer.findOptionalMainDocument(networkRecords, URL.finalUrl);
 
-    return MainResource.request({devtoolsLog, URL}, context)
-      .then(mainResource => {
-        const statusCode = mainResource.statusCode;
+    if (!mainResource) {
+      if (artifacts.GatherContext.gatherMode === 'timespan') return {notApplicable: true, score: 1};
+      throw new Error(`Unable to locate main resource`);
+    }
 
-        if (statusCode >= HTTP_UNSUCCESSFUL_CODE_LOW &&
+    const statusCode = mainResource.statusCode;
+
+    if (statusCode >= HTTP_UNSUCCESSFUL_CODE_LOW &&
           statusCode <= HTTP_UNSUCCESSFUL_CODE_HIGH) {
-          return {
-            score: 0,
-            displayValue: `${statusCode}`,
-          };
-        }
+      return {
+        score: 0,
+        displayValue: `${statusCode}`,
+      };
+    }
 
-        return {
-          score: 1,
-        };
-      });
+    return {
+      score: 1,
+    };
   }
 }
 
diff --git a/lighthouse-core/audits/seo/is-crawlable.js b/lighthouse-core/audits/seo/is-crawlable.js
index b5be35cf3..8bf4960ec 100644
--- a/lighthouse-core/audits/seo/is-crawlable.js
+++ b/lighthouse-core/audits/seo/is-crawlable.js
@@ -80,6 +80,7 @@ class IsCrawlable extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['MetaElements', 'RobotsTxt', 'URL', 'devtoolsLogs'],
     };
   }
diff --git a/lighthouse-core/audits/splash-screen.js b/lighthouse-core/audits/splash-screen.js
index d7e1a8c11..2dbcd3606 100644
--- a/lighthouse-core/audits/splash-screen.js
+++ b/lighthouse-core/audits/splash-screen.js
@@ -45,6 +45,7 @@ class SplashScreen extends MultiCheckAudit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['WebAppManifest', 'InstallabilityErrors'],
     };
   }
diff --git a/lighthouse-core/audits/themed-omnibox.js b/lighthouse-core/audits/themed-omnibox.js
index 0a4a6bff2..ba341339d 100644
--- a/lighthouse-core/audits/themed-omnibox.js
+++ b/lighthouse-core/audits/themed-omnibox.js
@@ -42,6 +42,7 @@ class ThemedOmnibox extends MultiCheckAudit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
+      supportedModes: ['navigation'],
       requiredArtifacts: ['WebAppManifest', 'InstallabilityErrors', 'MetaElements'],
     };
   }
diff --git a/lighthouse-core/computed/resource-summary.js b/lighthouse-core/computed/resource-summary.js
index fd208b8b7..31eb2f672 100644
--- a/lighthouse-core/computed/resource-summary.js
+++ b/lighthouse-core/computed/resource-summary.js
@@ -9,7 +9,6 @@ const makeComputedArtifact = require('./computed-artifact.js');
 const NetworkRecords = require('./network-records.js');
 const URL = require('../lib/url-shim.js');
 const NetworkRequest = require('../lib/network-request.js');
-const MainResource = require('./main-resource.js');
 const Budget = require('../config/budget.js');
 const Util = require('../../report/renderer/util.js');
 
@@ -107,11 +106,8 @@ class ResourceSummary {
    * @return {Promise<Record<LH.Budget.ResourceType,ResourceEntry>>}
    */
   static async compute_(data, context) {
-    const [networkRecords, mainResource] = await Promise.all([
-      NetworkRecords.request(data.devtoolsLog, context),
-      MainResource.request({devtoolsLog: data.devtoolsLog, URL: data.URL}, context),
-    ]);
-    return ResourceSummary.summarize(networkRecords, mainResource.url, data.budgets);
+    const networkRecords = await NetworkRecords.request(data.devtoolsLog, context);
+    return ResourceSummary.summarize(networkRecords, data.URL.finalUrl, data.budgets);
   }
 }
 
diff --git a/lighthouse-core/gather/gatherers/script-elements.js b/lighthouse-core/gather/gatherers/script-elements.js
index f399cfe4e..fda2d6a65 100644
--- a/lighthouse-core/gather/gatherers/script-elements.js
+++ b/lighthouse-core/gather/gatherers/script-elements.js
@@ -82,7 +82,7 @@ class ScriptElements extends FRGatherer {
   async _getArtifact(context, networkRecords, formFactor) {
     const session = context.driver.defaultSession;
     const executionContext = context.driver.executionContext;
-    const mainResource = NetworkAnalyzer.findMainDocument(networkRecords, context.url);
+    const mainResource = NetworkAnalyzer.findOptionalMainDocument(networkRecords, context.url);
 
     const scripts = await executionContext.evaluate(collectAllScriptElements, {
       args: [],
@@ -94,7 +94,7 @@ class ScriptElements extends FRGatherer {
     });
 
     for (const script of scripts) {
-      if (script.content) script.requestId = mainResource.requestId;
+      if (mainResource && script.content) script.requestId = mainResource.requestId;
     }
 
     const scriptRecords = networkRecords
diff --git a/lighthouse-core/gather/gatherers/seo/tap-targets.js b/lighthouse-core/gather/gatherers/seo/tap-targets.js
index e7511a69c..c978bd971 100644
--- a/lighthouse-core/gather/gatherers/seo/tap-targets.js
+++ b/lighthouse-core/gather/gatherers/seo/tap-targets.js
@@ -285,6 +285,18 @@ function gatherTapTargets(tapTargetsSelector, className) {
 /* c8 ignore stop */
 
 class TapTargets extends FRGatherer {
+  constructor() {
+    super();
+    /**
+     * This needs to be in the constructor.
+     * https://github.com/GoogleChrome/lighthouse/issues/12134
+     * @type {LH.Gatherer.GathererMeta}
+     */
+    this.meta = {
+      supportedModes: ['snapshot', 'navigation'],
+    };
+  }
+
   /**
    * @param {LH.Gatherer.FRProtocolSession} session
    * @param {string} className
diff --git a/lighthouse-core/gather/gatherers/trace-elements.js b/lighthouse-core/gather/gatherers/trace-elements.js
index fd5de00f1..1eac943db 100644
--- a/lighthouse-core/gather/gatherers/trace-elements.js
+++ b/lighthouse-core/gather/gatherers/trace-elements.js
@@ -21,6 +21,7 @@ const Sentry = require('../../lib/sentry.js');
 const Trace = require('./trace.js');
 const ProcessedTrace = require('../../computed/processed-trace.js');
 const ProcessedNavigation = require('../../computed/processed-navigation.js');
+const LighthouseError = require('../../lib/lh-error.js');
 
 /** @typedef {{nodeId: number, score?: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[]}} TraceElementData */
 
@@ -257,7 +258,16 @@ class TraceElements extends FRGatherer {
     }
 
     const processedTrace = await ProcessedTrace.request(trace, context);
-    const {largestContentfulPaintEvt} = await ProcessedNavigation.request(processedTrace, context);
+    const {largestContentfulPaintEvt} = await ProcessedNavigation
+      .request(processedTrace, context)
+      .catch(err => {
+        // If we were running in timespan mode and there was no paint, treat LCP as missing.
+        if (context.gatherMode === 'timespan' && err.code === LighthouseError.errors.NO_FCP.code) {
+          return {largestContentfulPaintEvt: undefined};
+        }
+
+        throw err;
+      });
     const {mainThreadEvents} = processedTrace;
 
     const lcpNodeId = TraceElements.getNodeIDFromTraceEvent(largestContentfulPaintEvt);
diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js
index aff72ded3..3fb24c3fa 100644
--- a/lighthouse-core/scripts/i18n/collect-strings.js
+++ b/lighthouse-core/scripts/i18n/collect-strings.js
@@ -521,9 +521,9 @@ function parseUIStrings(sourceStr, liveUIStrings) {
  * Collects all LHL messsages defined in UIString from Javascript files in dir,
  * and converts them into CTC.
  * @param {string} dir absolute path
- * @return {Record<string, CtcMessage>}
+ * @return {Promise<Record<string, CtcMessage>>}
  */
-function collectAllStringsInDir(dir) {
+async function collectAllStringsInDir(dir) {
   /** @type {Record<string, CtcMessage>} */
   const strings = {};
 
@@ -538,9 +538,9 @@ function collectAllStringsInDir(dir) {
     if (!process.env.CI) console.log('Collecting from', relativeToRootPath);
 
     const content = fs.readFileSync(absolutePath, 'utf8');
-    const exportVars = require(absolutePath);
+    const exportVars = await import(absolutePath);
     const regexMatch = content.match(UISTRINGS_REGEX);
-    const exportedUIStrings = exportVars.UIStrings;
+    const exportedUIStrings = exportVars.UIStrings || (exportVars.default && exportVars.default.UIStrings);
 
     if (!regexMatch) {
       // No UIStrings found in the file text or exports, so move to the next.
@@ -674,14 +674,13 @@ function resolveMessageCollisions(strings) {
   }
 }
 
-// Test if called from the CLI or as a module.
-if (require.main === module) {
+async function main() {
   /** @type {Record<string, CtcMessage>} */
   const strings = {};
 
   for (const folderWithStrings of foldersWithStrings) {
     console.log(`\n====\nCollecting strings from ${folderWithStrings}\n====`);
-    const moreStrings = collectAllStringsInDir(folderWithStrings);
+    const moreStrings = await collectAllStringsInDir(folderWithStrings);
     Object.assign(strings, moreStrings);
   }
 
@@ -715,6 +714,11 @@ if (require.main === module) {
   console.log('✨ Complete!');
 }
 
+// Test if called from the CLI or as a module.
+if (require.main === module) {
+  main();
+}
+
 module.exports = {
   parseUIStrings,
   createPsuedoLocaleStrings,
diff --git a/report/renderer/util.js b/report/renderer/util.js
index 3b682f50d..18e2ab8b6 100644
--- a/report/renderer/util.js
+++ b/report/renderer/util.js
@@ -645,3 +645,6 @@ if (typeof module !== 'undefined' && module.exports) {
 } else {
   self.Util = Util;
 }
+
+// TODO(esmodules): export these strings too, then collect-strings will work when this file is esm.
+// export const UIStrings = Util.UIStrings;


Test patch:
diff --git a/lighthouse-core/test/audits/final-screenshot-test.js b/lighthouse-core/test/audits/final-screenshot-test.js
index 0544f9523..255e6efaa 100644
--- a/lighthouse-core/test/audits/final-screenshot-test.js
+++ b/lighthouse-core/test/audits/final-screenshot-test.js
@@ -5,23 +5,48 @@
  */
 'use strict';
 
-const assert = require('assert').strict;
-
 const FinalScreenshotAudit = require('../../audits/final-screenshot.js');
 const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
+const noScreenshotsTrace = {traceEvents: pwaTrace.traceEvents.filter(e => e.name !== 'Screenshot')};
 
 /* eslint-env jest */
 
 describe('Final screenshot', () => {
+  let context;
+
+  beforeEach(() => {
+    context = {computedCache: new Map()};
+  });
+
   it('should extract a final screenshot from a trace', async () => {
     const artifacts = Object.assign({
       traces: {defaultPass: pwaTrace},
+      GatherContext: {gatherMode: 'timespan'},
     });
-    const results = await FinalScreenshotAudit.audit(artifacts, {computedCache: new Map()});
+    const results = await FinalScreenshotAudit.audit(artifacts, context);
+
+    expect(results.score).toEqual(1);
+    expect(results.details.timing).toEqual(818);
+    expect(results.details.timestamp).toEqual(225414990064);
+    expect(results.details.data).toContain('data:image/jpeg;base64,/9j/4AAQSkZJRgABA');
+  });
+
+  it('should returns not applicable for missing screenshots in timespan mode', async () => {
+    const artifacts = {
+      traces: {defaultPass: noScreenshotsTrace},
+      GatherContext: {gatherMode: 'timespan'},
+    };
+
+    const results = await FinalScreenshotAudit.audit(artifacts, context);
+    expect(results.notApplicable).toEqual(true);
+  });
+
+  it('should throws for missing screenshots in navigation mode', async () => {
+    const artifacts = {
+      traces: {defaultPass: noScreenshotsTrace},
+      GatherContext: {gatherMode: 'navigation'},
+    };
 
-    assert.equal(results.score, 1);
-    assert.equal(results.details.timing, 818);
-    assert.equal(results.details.timestamp, 225414990064);
-    assert.ok(results.details.data.startsWith('data:image/jpeg;base64,/9j/4AAQSkZJRgABA'));
+    await expect(FinalScreenshotAudit.audit(artifacts, context)).rejects.toThrow();
   });
 });
diff --git a/lighthouse-core/test/audits/screenshot-thumbnails-test.js b/lighthouse-core/test/audits/screenshot-thumbnails-test.js
index 26b84e509..65aa0b384 100644
--- a/lighthouse-core/test/audits/screenshot-thumbnails-test.js
+++ b/lighthouse-core/test/audits/screenshot-thumbnails-test.js
@@ -11,6 +11,7 @@ const assert = require('assert').strict;
 
 const ScreenshotThumbnailsAudit = require('../../audits/screenshot-thumbnails.js');
 const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
+const noScreenshotsTrace = {traceEvents: pwaTrace.traceEvents.filter(e => e.name !== 'Screenshot')};
 const pwaDevtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
 
 /* eslint-env jest */
@@ -20,6 +21,7 @@ describe('Screenshot thumbnails', () => {
     const options = {minimumTimelineDuration: 500};
     const settings = {throttlingMethod: 'provided'};
     const artifacts = {
+      GatherContext: {gatherMode: 'timespan'},
       traces: {defaultPass: pwaTrace},
       devtoolsLogs: {}, // empty devtools logs to test just thumbnails without TTI behavior
     };
@@ -42,10 +44,38 @@ describe('Screenshot thumbnails', () => {
     });
   }, 10000);
 
+  it('should throw when screenshots are missing in navigation', async () => {
+    const options = {minimumTimelineDuration: 500};
+    const settings = {throttlingMethod: 'provided'};
+    const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
+      traces: {defaultPass: noScreenshotsTrace},
+      devtoolsLogs: {}, // empty devtools logs to test just thumbnails without TTI behavior
+    };
+
+    const context = {settings, options, computedCache: new Map()};
+    await expect(ScreenshotThumbnailsAudit.audit(artifacts, context)).rejects.toThrow();
+  });
+
+  it('should be notApplicable when screenshots are missing in timespan', async () => {
+    const options = {minimumTimelineDuration: 500};
+    const settings = {throttlingMethod: 'provided'};
+    const artifacts = {
+      GatherContext: {gatherMode: 'timespan'},
+      traces: {defaultPass: noScreenshotsTrace},
+      devtoolsLogs: {}, // empty devtools logs to test just thumbnails without TTI behavior
+    };
+
+    const context = {settings, options, computedCache: new Map()};
+    const results = await ScreenshotThumbnailsAudit.audit(artifacts, context);
+    expect(results.notApplicable).toBe(true);
+  });
+
   it('should scale the timeline to last visual change', () => {
     const options = {minimumTimelineDuration: 500};
     const settings = {throttlingMethod: 'devtools'};
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: pwaTrace},
       devtoolsLogs: {defaultPass: pwaDevtoolsLog},
     };
@@ -60,6 +90,7 @@ describe('Screenshot thumbnails', () => {
   it('should scale the timeline to minimumTimelineDuration', () => {
     const settings = {throttlingMethod: 'simulate'};
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: pwaTrace},
     };
 
@@ -80,6 +111,7 @@ describe('Screenshot thumbnails', () => {
 
     const settings = {throttlingMethod: 'simulate'};
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: infiniteTrace},
     };
     const context = {settings, options: {}, computedCache: new Map()};
diff --git a/lighthouse-core/test/audits/seo/http-status-code-test.js b/lighthouse-core/test/audits/seo/http-status-code-test.js
index 5df7955c8..9d614afaf 100644
--- a/lighthouse-core/test/audits/seo/http-status-code-test.js
+++ b/lighthouse-core/test/audits/seo/http-status-code-test.js
@@ -24,6 +24,7 @@ describe('SEO: HTTP code audit', () => {
       const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]);
 
       const artifacts = {
+        GatherContext: {gatherMode: 'timespan'},
         devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: devtoolsLog},
         URL: {finalUrl},
       };
@@ -46,6 +47,7 @@ describe('SEO: HTTP code audit', () => {
     const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]);
 
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: devtoolsLog},
       URL: {finalUrl},
     };
@@ -54,4 +56,30 @@ describe('SEO: HTTP code audit', () => {
       assert.equal(auditResult.score, 1);
     });
   });
+
+  it('throws when main resource cannot be found in navigation', async () => {
+    const finalUrl = 'https://example.com';
+
+    const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
+      devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: []},
+      URL: {finalUrl},
+    };
+
+    const resultPromise = HTTPStatusCodeAudit.audit(artifacts, {computedCache: new Map()});
+    await expect(resultPromise).rejects.toThrow();
+  });
+
+  it('notApplicable when main resource cannot be found in timespan', async () => {
+    const finalUrl = 'https://example.com';
+
+    const artifacts = {
+      GatherContext: {gatherMode: 'timespan'},
+      devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: []},
+      URL: {finalUrl},
+    };
+
+    const results = await HTTPStatusCodeAudit.audit(artifacts, {computedCache: new Map()});
+    expect(results.notApplicable).toBe(true);
+  });
 });
diff --git a/lighthouse-core/test/fraggle-rock/api-test-pptr.js b/lighthouse-core/test/fraggle-rock/api-test-pptr.js
index b8a648603..e1ea99ef1 100644
--- a/lighthouse-core/test/fraggle-rock/api-test-pptr.js
+++ b/lighthouse-core/test/fraggle-rock/api-test-pptr.js
@@ -99,7 +99,7 @@ describe('Fraggle Rock API', () => {
 
       const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`82`);
+      expect(auditResults.length).toMatchInlineSnapshot(`79`);
 
       expect(erroredAudits).toHaveLength(0);
       expect(failedAudits.map(audit => audit.id)).toContain('label');
@@ -130,9 +130,9 @@ describe('Fraggle Rock API', () => {
         notApplicableAudits,
       } = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`63`);
+      expect(auditResults.length).toMatchInlineSnapshot(`56`);
 
-      expect(notApplicableAudits.length).toMatchInlineSnapshot(`8`);
+      expect(notApplicableAudits.length).toMatchInlineSnapshot(`6`);
       expect(notApplicableAudits.map(audit => audit.id)).not.toContain('server-response-time');
 
       expect(erroredAudits).toHaveLength(0);
@@ -172,13 +172,13 @@ describe('Fraggle Rock API', () => {
       if (!result) throw new Error('Lighthouse failed to produce a result');
 
       const {auditResults, erroredAudits, notApplicableAudits} = getAuditsBreakdown(result.lhr);
-      expect(auditResults.length).toMatchInlineSnapshot(`63`);
+      expect(auditResults.length).toMatchInlineSnapshot(`56`);
 
-      expect(notApplicableAudits.length).toMatchInlineSnapshot(`5`);
+      expect(notApplicableAudits.length).toMatchInlineSnapshot(`8`);
       expect(notApplicableAudits.map(audit => audit.id)).toContain('server-response-time');
 
       // TODO(FR-COMPAT): Reduce this number by handling the error, making N/A, or removing timespan support.
-      expect(erroredAudits.length).toMatchInlineSnapshot(`22`);
+      expect(erroredAudits.length).toMatchInlineSnapshot(`12`);
     });
   });
 
@@ -194,7 +194,7 @@ describe('Fraggle Rock API', () => {
       const {lhr} = result;
       const {auditResults, failedAudits, erroredAudits} = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`152`);
+      expect(auditResults.length).toMatchInlineSnapshot(`153`);
       expect(erroredAudits).toHaveLength(0);
 
       const failedAuditIds = failedAudits.map(audit => audit.id);
diff --git a/lighthouse-core/test/gather/gatherers/trace-elements-test.js b/lighthouse-core/test/gather/gatherers/trace-elements-test.js
index 3119c5429..31043d37b 100644
--- a/lighthouse-core/test/gather/gatherers/trace-elements-test.js
+++ b/lighthouse-core/test/gather/gatherers/trace-elements-test.js
@@ -680,6 +680,58 @@ describe('Trace Elements gatherer - Animated Elements', () => {
       },
     ]);
   });
+
+
+  it('properly handles timespans without FCP', async () => {
+    const animationNodeData = {
+      traceEventType: 'animation',
+      devtoolsNodePath: '1,HTML,1,BODY,1,DIV',
+      selector: 'body > div#animated',
+      nodeLabel: 'div',
+      snippet: '<div id="animated">',
+      boundingRect: {
+        top: 60,
+        bottom: 200,
+        left: 60,
+        right: 100,
+        width: 40,
+        height: 140,
+      },
+    };
+    const connectionStub = new Connection();
+    connectionStub.sendCommand = createMockSendCommandFn()
+      // Animation 1
+      .mockResponse('DOM.resolveNode', {object: {objectId: 5}})
+      .mockResponse('Runtime.callFunctionOn', {result: {value: animationNodeData}});
+    const driver = new Driver(connectionStub);
+
+    const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000});
+    trace.traceEvents = trace.traceEvents.filter(event => event.name !== 'firstContentfulPaint');
+    trace.traceEvents.push(makeAnimationTraceEvent('0x363db876c8', 'b', {id: '1', nodeId: 5}));
+    trace.traceEvents.push(makeAnimationTraceEvent('0x363db876c8', 'n', {
+      compositeFailed: 8192,
+      unsupportedProperties: ['height'],
+    }));
+
+    const gatherer = new TraceElementsGatherer();
+    gatherer.animationIdToName.set('1', 'example');
+
+    const result = await gatherer._getArtifact({
+      driver,
+      gatherMode: 'timespan',
+      computedCache: new Map(),
+    }, trace);
+
+    expect(result).toEqual([
+      {
+        ...animationNodeData,
+        animations: [
+          {name: 'example', failureReasonsMask: 8192, unsupportedProperties: ['height']},
+        ],
+        nodeId: 5,
+      },
+    ]);
+  });
 });
 
 describe('instrumentation', () => {