async _evaluateInContext(expression, contextId, timeout) {
    // `__lighthouseExecutionContextUniqueIdentifier` is only used by the FullPageScreenshot gatherer.
    // See `getNodeDetails` in page-functions.
    const uniqueExecutionContextIdentifier = contextId === undefined ?
      undefined :
      this._executionContextIdentifiersCreated;

    const evaluationParams = {
      // We need to explicitly wrap the raw expression for several purposes:
      // 1. Ensure that the expression will be a native Promise and not a polyfill/non-Promise.
      // 2. Ensure that errors in the expression are captured by the Promise.
      // 3. Ensure that errors captured in the Promise are converted into plain-old JS Objects
      //    so that they can be serialized properly b/c JSON.stringify(new Error('foo')) === '{}'
      //
      // `__lighthouseExecutionContextUniqueIdentifier` is only used by the FullPageScreenshot gatherer.
      // See `getNodeDetails` in page-functions.
      expression: `(function wrapInNativePromise() {
        ${ExecutionContext._cachedNativesPreamble};
        globalThis.__lighthouseExecutionContextUniqueIdentifier =
          ${uniqueExecutionContextIdentifier};
        ${pageFunctions.esbuildFunctionWrapperString}
        return new Promise(function (resolve) {
          return Promise.resolve()
            .then(_ => ${expression})
            .catch(${pageFunctions.wrapRuntimeEvalErrorInBrowser})
            .then(resolve);
        });
      }())
      //# sourceURL=_lighthouse-eval.js`,
      includeCommandLineAPI: true,
      awaitPromise: true,
      returnByValue: true,
      timeout,
      contextId,
    };

    this._session.setNextProtocolTimeout(timeout);
    const response = await this._session.sendCommand('Runtime.evaluate', evaluationParams);

    const ex = response.exceptionDetails;
    if (ex) {
      // An error occurred before we could even create a Promise, should be *very* rare.
      // Also occurs when the expression is not valid JavaScript.
      const elidedExpression = expression.replace(/\s+/g, ' ').substring(0, 100);
      const messageLines = [
        'Runtime.evaluate exception',
        `Expression: ${elidedExpression}\n---- (elided)`,
        !ex.stackTrace ? `Parse error at: ${ex.lineNumber + 1}:${ex.columnNumber + 1}` : null,
        ex.exception?.description || ex.text,
      ].filter(Boolean);
      const evaluationError = new Error(messageLines.join('\n'));
      return Promise.reject(evaluationError);
    }

    // Protocol should always return a 'result' object, but it is sometimes undefined.  See #6026.
    if (response.result === undefined) {
      return Promise.reject(
        new Error('Runtime.evaluate response did not contain a "result" object'));
    }
    const value = response.result.value;
    if (value?.__failedInBrowser) {
      return Promise.reject(Object.assign(new Error(), value));
    } else {
      return value;
    }
  }
Evaluate an expression in the given execution context; an undefined contextId implies the main
page without isolation.
@param {string} expression
@param {number|undefined} contextId
@param {number} timeout
@return {Promise<*>}