Repository: GoogleChrome/lighthouse
Pull request #7359 (2019-03-02 02:06:14)

misc: use jsdoc-style comments on Flags properties
converts them to `/** */` style so they appear on hover in vscode, etc


Patch:
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index bcb475ced..b3e593d29 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -47,8 +47,8 @@ class Driver {
      */
     this._eventEmitter = /** @type {CrdpEventEmitter} */ (new EventEmitter());
     this._connection = connection;
-    // currently only used by WPT where just Page and Network are needed
-    this._devtoolsLog = new DevtoolsLog(/^(Page|Network)\./);
+    // Used to save network and lifecycle protocol traffic. Just Page, Network, and Target are needed.
+    this._devtoolsLog = new DevtoolsLog(/^(Page|Network|Target)\./);
     this.online = true;
     /** @type {Map<string, number>} */
     this._domainEnabledCounts = new Map();
@@ -69,6 +69,20 @@ class Driver {
      */
     this._monitoredUrl = null;
 
+    let targetProxyMessageId = 0;
+    this.on('Target.attachedToTarget', event => {
+      targetProxyMessageId++;
+      // We're only interested in network requests from iframes for now as those are "part of the page".
+      if (event.targetInfo.type !== 'iframe') return;
+
+      // We want to receive information about network requests from iframes, so enable the Network domain.
+      // Network events from subtargets will be stringified and sent back on `Target.receivedMessageFromTarget`.
+      this.sendCommand('Target.sendMessageToTarget', {
+        message: JSON.stringify({id: targetProxyMessageId, method: 'Network.enable'}),
+        sessionId: event.sessionId,
+      });
+    });
+
     connection.on('protocolevent', event => {
       this._devtoolsLog.record(event);
       if (this._networkStatusMonitor) {
@@ -1015,6 +1029,12 @@ class Driver {
     await this._beginNetworkStatusMonitoring(url);
     await this._clearIsolatedContextId();
 
+    // Enable auto-attaching to subtargets so we receive iframe information
+    await this.sendCommand('Target.setAutoAttach', {
+      autoAttach: true,
+      waitForDebuggerOnStart: false,
+    });
+
     await this.sendCommand('Page.enable');
     await this.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
     await this.sendCommand('Emulation.setScriptExecutionDisabled', {value: disableJS});
diff --git a/lighthouse-core/lib/network-recorder.js b/lighthouse-core/lib/network-recorder.js
index 4264e34eb..ed38a3498 100644
--- a/lighthouse-core/lib/network-recorder.js
+++ b/lighthouse-core/lib/network-recorder.js
@@ -303,15 +303,26 @@ class NetworkRecorder extends EventEmitter {
     request.onResourceChangedPriority(data);
   }
 
+  /**
+   * Events from targets other than the main frame are proxied through `Target.receivedMessageFromTarget`.
+   * Their payloads are JSON-stringified into the `.message` property
+   * @param {LH.Crdp.Target.ReceivedMessageFromTargetEvent} data
+   */
+  onReceivedMessageFromTarget(data) {
+    /** @type {LH.Protocol.RawMessage} */
+    const protocolMessage = JSON.parse(data.message);
+
+    // Message was a response to some command, not an event, so we'll ignore it.
+    if ('id' in protocolMessage) return;
+    // Message was an event, replay it through our normal dispatch process.
+    this.dispatch(protocolMessage);
+  }
+
   /**
    * Routes network events to their handlers, so we can construct networkRecords
    * @param {LH.Protocol.RawEventMessage} event
    */
   dispatch(event) {
-    if (!event.method.startsWith('Network.')) {
-      return;
-    }
-
     switch (event.method) {
       case 'Network.requestWillBeSent': return this.onRequestWillBeSent(event.params);
       case 'Network.requestServedFromCache': return this.onRequestServedFromCache(event.params);
@@ -320,6 +331,7 @@ class NetworkRecorder extends EventEmitter {
       case 'Network.loadingFinished': return this.onLoadingFinished(event.params);
       case 'Network.loadingFailed': return this.onLoadingFailed(event.params);
       case 'Network.resourceChangedPriority': return this.onResourceChangedPriority(event.params);
+      case 'Target.receivedMessageFromTarget': return this.onReceivedMessageFromTarget(event.params); // eslint-disable-line max-len
       default: return;
     }
   }
diff --git a/types/externs.d.ts b/types/externs.d.ts
index 455587626..44810a44d 100644
--- a/types/externs.d.ts
+++ b/types/externs.d.ts
@@ -59,19 +59,19 @@ declare global {
 
     /** Simulation settings that control the amount of network & cpu throttling in the run. */
     interface ThrottlingSettings {
-      // The round trip time in milliseconds
+      /** The round trip time in milliseconds. */
       rttMs?: number;
-      // The network throughput in kilobytes per second
+      /** The network throughput in kilobytes per second. */
       throughputKbps?: number;
       // devtools settings
-      // The network request latency in milliseconds
+      /** The network request latency in milliseconds. */
       requestLatencyMs?: number;
-      // The network download throughput in kilobytes per second
+      /** The network download throughput in kilobytes per second. */
       downloadThroughputKbps?: number;
-      // The network upload throughput in kilobytes per second
+      /** The network upload throughput in kilobytes per second. */
       uploadThroughputKbps?: number;
       // used by both
-      // The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>)
+      /** The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>). */
       cpuSlowdownMultiplier?: number
     }
 
@@ -89,39 +89,39 @@ declare global {
      * interface and the Config's `settings` object.
      */
     interface SharedFlagsSettings {
-      // The output types (json, html, ...)
+      /** The type(s) of report output to be produced. */
       output?: OutputMode|OutputMode[];
-      // The locale setting
+      /** The locale to use for the output. */
       locale?: Locale;
-      // The maximum amount of time to wait for a page to load in ms
+      /** The maximum amount of time to wait for a page to load, in ms. */
       maxWaitForLoad?: number;
-      // List of URL patterns to block
+      /** List of URL patterns to block. */
       blockedUrlPatterns?: string[] | null;
-      // Comma-delimited list of trace categories to include
+      /** Comma-delimited list of trace categories to include. */
       additionalTraceCategories?: string | null;
-      // Flag indicating the run should only audit
+      /** Flag indicating the run should only audit. */
       auditMode?: boolean | string;
-      // Flag indicating the run should only gather
+      /** Flag indicating the run should only gather. */
       gatherMode?: boolean | string;
-      // Flag indicating that the browser storage should not be reset for the audit
+      /** Flag indicating that the browser storage should not be reset for the audit. */
       disableStorageReset?: boolean;
-      // Flag indicating that there shouldn't be any emulation during the run
+      /** Flag indicating that there shouldn't be any emulation during the run. */
       disableDeviceEmulation?: boolean;
-      // The form factor the emulation should use
+      /** The form factor the emulation should use. */
       emulatedFormFactor?: 'mobile'|'desktop'|'none';
-      // The method used to throttle the network
+      /** The method used to throttle the network. */
       throttlingMethod?: 'devtools'|'simulate'|'provided';
-      // The throttling config settings
+      /** The throttling config settings. */
       throttling?: ThrottlingSettings;
-      // List of audits that the run should conduct
+      /** If present, the run should only conduct this list of audits. */
       onlyAudits?: string[] | null;
-      // List of categories of audits the run should conduct
+      /** If present, the run should only conduct this list of categories. */
       onlyCategories?: string[] | null;
-      // List of audits that the run should skip
+      /** If present, the run should skip this list of audits. */
       skipAudits?: string[] | null;
-      // List of extra HTTP Headers to include
+      /** List of extra HTTP Headers to include. */
       extraHeaders?: Crdp.Network.Headers | null; // See extraHeaders TODO in bin.js
-      // Precomputed lantern estimates to use instead of observed analysis
+      /** Precomputed lantern estimates to use instead of observed analysis. */
       precomputedLanternData?: PrecomputedLanternData | null;
     }
 
@@ -130,9 +130,13 @@ declare global {
      * Lighthouse module but will not end up in the Config settings.
      */
     export interface Flags extends SharedFlagsSettings {
+      /** The port to use for the debugging protocol, if manually connecting. */
       port?: number;
+      /** The hostname to use for the debugging protocol, if manually connecting. */
       hostname?: string;
+      /** The level of logging to enable. */
       logLevel?: 'silent'|'error'|'info'|'verbose';
+      /** The path to the config JSON. */
       configPath?: string;
     }
 
@@ -143,31 +147,35 @@ declare global {
     export interface CliFlags extends Flags {
       _: string[];
       chromeFlags: string;
-      // Output path for the generated assets
+      /** Output path for the generated results. */
       outputPath: string;
-      // Flag to save the trace contents and screenshots to disk
+      /** Flag to save the trace contents and screenshots to disk. */
       saveAssets: boolean;
-      // Flag to open the report immediately
+      /** Flag to open the report immediately. */
       view: boolean;
-      // Enables error reporting
+      /** Flag to enable error reporting. */
       enableErrorReporting?: boolean;
-      // Flag to print out a list of all audits + categories
+      /** Flag to print a list of all audits + categories. */
       listAllAudits: boolean;
-      // Flag to print out a list of all required trace categories
+      /** Flag to print a list of all required trace categories. */
       listTraceCategories: boolean;
-      // A preset audit of selected audit categories to run
+      /** A preset audit of selected audit categories to run. */
       preset?: 'full'|'mixed-content'|'perf';
+      /** A flag to enable logLevel 'verbose'. */
       verbose: boolean;
+      /** A flag to enable logLevel 'silent'. */
       quiet: boolean;
-      // following are given defaults in cli-flags, so are not optional like in Flags or SharedFlagsSettings
-      output: OutputMode[];
-      port: number;
-      hostname: string;
+      /** A flag to print the normalized config for the given config and options, then exit. */
       printConfig: boolean;
-      // Path to the file where precomputed lantern data should be read from.
+      /** Path to the file where precomputed lantern data should be read from. */
       precomputedLanternDataPath?: string;
-      // Path to the file where precomputed lantern data should be written to.
+      /** Path to the file where precomputed lantern data should be written to. */
       lanternDataOutputPath?: string;
+
+      // The following are given defaults in cli-flags, so are not optional like in Flags or SharedFlagsSettings.
+      output: OutputMode[];
+      port: number;
+      hostname: string;
     }
 
     export interface RunnerResult {


Test patch:
diff --git a/lighthouse-cli/test/fixtures/oopif.html b/lighthouse-cli/test/fixtures/oopif.html
new file mode 100644
index 000000000..b27db2d3c
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/oopif.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Where is my iframe?</title>
+  </head>
+  <body>
+    <h1>Hello frames</h1>
+    <iframe name="oopif" src="https://airhorner.com"></iframe>
+  </body>
+</html>
diff --git a/lighthouse-cli/test/smokehouse/oopif-config.js b/lighthouse-cli/test/smokehouse/oopif-config.js
new file mode 100644
index 000000000..ebc5fa22b
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/oopif-config.js
@@ -0,0 +1,18 @@
+/**
+ * @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';
+
+/**
+ * Config file for running the OOPIF tests
+ */
+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
new file mode 100644
index 000000000..94eac432d
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/oopif-expectations.js
@@ -0,0 +1,28 @@
+/**
+ * @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';
+
+/**
+ * Expected Lighthouse audit values for sites with OOPIFS.
+ */
+module.exports = [
+  {
+    requestedUrl: 'http://localhost:10200/oopif.html',
+    finalUrl: 'http://localhost:10200/oopif.html',
+    audits: {
+      'network-requests': {
+        details: {
+          items: {
+            // The page itself only makes a few requests.
+            // We want to make sure we are finding the iframe's requests (airhorner) while being flexible enough
+            // to allow changes to the live site.
+            length: '>10',
+          },
+        },
+      },
+    },
+  },
+];
diff --git a/lighthouse-cli/test/smokehouse/run-smoke.js b/lighthouse-cli/test/smokehouse/run-smoke.js
index fd891c093..5399eb77b 100644
--- a/lighthouse-cli/test/smokehouse/run-smoke.js
+++ b/lighthouse-cli/test/smokehouse/run-smoke.js
@@ -35,6 +35,11 @@ const SMOKETESTS = [{
   expectations: smokehouseDir + 'error-expectations.js',
   config: smokehouseDir + 'error-config.js',
   batch: 'errors',
+}, {
+  id: 'oopif',
+  expectations: smokehouseDir + 'oopif-expectations.js',
+  config: smokehouseDir + 'oopif-config.js',
+  batch: 'parallel-first',
 }, {
   id: 'pwa',
   expectations: smokehouseDir + 'pwa-expectations.js',
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index 5befd18dd..b69ff4ddc 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -487,6 +487,7 @@ describe('.gotoURL', () => {
       .mockResponse('Page.setLifecycleEventsEnabled', {})
       .mockResponse('Emulation.setScriptExecutionDisabled', {})
       .mockResponse('Page.navigate', {})
+      .mockResponse('Target.setAutoAttach', {})
       .mockResponse('Runtime.evaluate', {});
   });
 
@@ -930,3 +931,36 @@ describe('.goOnline', () => {
     });
   });
 });
+
+describe('Multi-target management', () => {
+  it('enables the Network domain for iframes', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Target.sendMessageToTarget', {});
+
+    driver._eventEmitter.emit('Target.attachedToTarget', {
+      sessionId: 123,
+      targetInfo: {type: 'iframe'},
+    });
+    await flushAllTimersAndMicrotasks();
+
+    const sendMessageArgs = connectionStub.sendCommand
+      .findInvocation('Target.sendMessageToTarget');
+    expect(sendMessageArgs).toEqual({
+      message: '{"id":1,"method":"Network.enable"}',
+      sessionId: 123,
+    });
+  });
+
+  it('ignores other target types', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+    .mockResponse('Target.sendMessageToTarget', {});
+
+    driver._eventEmitter.emit('Target.attachedToTarget', {
+      sessionId: 123,
+      targetInfo: {type: 'service_worker'},
+    });
+    await flushAllTimersAndMicrotasks();
+
+    expect(connectionStub.sendCommand).not.toHaveBeenCalled();
+  });
+});