async function waitForFullyLoaded(session, networkMonitor, options) {
  const {pauseAfterFcpMs, pauseAfterLoadMs, networkQuietThresholdMs,
    cpuQuietThresholdMs, maxWaitForLoadedMs, maxWaitForFcpMs} = options;
  const {waitForFcp, waitForLoadEvent, waitForNetworkIdle, waitForCPUIdle} =
    options._waitForTestOverrides || DEFAULT_WAIT_FUNCTIONS;
  /** @type {NodeJS.Timeout|undefined} */
  let maxTimeoutHandle;

  // Listener for FCP. Resolves pauseAfterFcpMs ms after first FCP event.
  const resolveOnFcp = maxWaitForFcpMs ?
    waitForFcp(session, pauseAfterFcpMs, maxWaitForFcpMs) :
    waitForNothing();
  // Listener for onload. Resolves pauseAfterLoadMs ms after load.
  const resolveOnLoadEvent = waitForLoadEvent(session, pauseAfterLoadMs);
  // General network listener. Resolves when the network has been 2-idle for networkQuietThresholdMs.
  const resolveOnNetworkIdle = waitForNetworkIdle(session, networkMonitor, {
    networkQuietThresholdMs,
    busyEvent: 'network-2-busy',
    idleEvent: 'network-2-idle',
    isIdle: recorder => recorder.is2Idle(),
  });
  // Critical network listener. Resolves when the network has had 0 critical requests for networkQuietThresholdMs.
  const resolveOnCriticalNetworkIdle = waitForNetworkIdle(session, networkMonitor, {
    networkQuietThresholdMs,
    busyEvent: 'network-critical-busy',
    idleEvent: 'network-critical-idle',
    isIdle: recorder => recorder.isCriticalIdle(),
  });
  // CPU listener. Resolves when the CPU has been idle for cpuQuietThresholdMs after network idle.
  let resolveOnCPUIdle = waitForNothing();

  if (log.isVerbose()) {
    resolveOnFcp.promise.then(() => {
      log.verbose('waitFor', 'resolveOnFcp fired');
    });
    resolveOnLoadEvent.promise.then(() => {
      log.verbose('waitFor', 'resolveOnLoadEvent fired');
    });
    resolveOnNetworkIdle.promise.then(() => {
      log.verbose('waitFor', 'resolveOnNetworkIdle fired');
    });
    resolveOnCriticalNetworkIdle.promise.then(() => {
      log.verbose('waitFor', 'resolveOnCriticalNetworkIdle fired');
    });
  }

  // Wait for all initial load promises. Resolves on cleanup function the clears load
  // timeout timer.
  /** @type {Promise<() => Promise<{timedOut: boolean}>>} */
  const loadPromise = Promise.all([
    resolveOnFcp.promise,
    resolveOnLoadEvent.promise,
    resolveOnNetworkIdle.promise,
    resolveOnCriticalNetworkIdle.promise,
  ]).then(() => {
    resolveOnCPUIdle = waitForCPUIdle(session, cpuQuietThresholdMs);
    return resolveOnCPUIdle.promise;
  }).then(() => {
    /** @return {Promise<{timedOut: boolean}>} */
    const cleanupFn = async function() {
      log.verbose('waitFor', 'loadEventFired and network considered idle');
      return {timedOut: false};
    };

    return cleanupFn;
  }).catch(err => {
    // Throw the error in the cleanupFn so we still cleanup all our handlers.
    return function() {
      throw err;
    };
  });

  // Last resort timeout. Resolves maxWaitForLoadedMs ms from now on
  // cleanup function that removes loadEvent and network idle listeners.
  /** @type {Promise<() => Promise<{timedOut: boolean}>>} */
  const maxTimeoutPromise = new Promise((resolve, reject) => {
    maxTimeoutHandle = setTimeout(resolve, maxWaitForLoadedMs);
  }).then(_ => {
    return async () => {
      log.warn('waitFor', 'Timed out waiting for page load. Checking if page is hung...');
      if (await isPageHung(session)) {
        log.warn('waitFor', 'Page appears to be hung, killing JavaScript...');
        // We don't await these, as we want to exit with PAGE_HUNG
        void session.sendCommandAndIgnore('Emulation.setScriptExecutionDisabled', {value: true});
        void session.sendCommandAndIgnore('Runtime.terminateExecution');
        throw new LighthouseError(LighthouseError.errors.PAGE_HUNG);
      }

      // Log remaining inflight requests if any.
      const inflightRequestUrls = networkMonitor
        .getInflightRequests()
        .map((request) => request.url);
      if (inflightRequestUrls.length > 0) {
        log.warn(
          'waitFor',
          'Remaining inflight requests URLs',
          inflightRequestUrls
        );
      }

      return {timedOut: true};
    };
  });

  // Wait for load or timeout and run the cleanup function the winner returns.
  const cleanupFn = await Promise.race([
    loadPromise,
    maxTimeoutPromise,
  ]);

  maxTimeoutHandle && clearTimeout(maxTimeoutHandle);
  resolveOnFcp.cancel();
  resolveOnLoadEvent.cancel();
  resolveOnNetworkIdle.cancel();
  resolveOnCPUIdle.cancel();

  return cleanupFn();
}
Returns a promise that resolves when:
- All of the following conditions have been met:
   - page has no security issues
   - pauseAfterLoadMs milliseconds have passed since the load event.
   - networkQuietThresholdMs milliseconds have passed since the last network request that exceeded
     2 inflight requests (network-2-quiet has been reached).
   - cpuQuietThresholdMs have passed since the last long task after network-2-quiet.
- maxWaitForLoadedMs milliseconds have passed.
See https://github.com/GoogleChrome/lighthouse/issues/627 for more.
@param {LH.Gatherer.ProtocolSession} session
@param {NetworkMonitor} networkMonitor
@param {WaitOptions} options
@return {Promise<{timedOut: boolean}>}