Repository: GoogleChrome/lighthouse
Pull request #12495 (2021-05-17 21:45:45)

misc(treemap): set focus-visible styles for view mode buttons
ref #11254

https://stackoverflow.com/a/38974812/2788187 really helped


Patch:
diff --git a/lighthouse-core/fraggle-rock/config/default-config.js b/lighthouse-core/fraggle-rock/config/default-config.js
index 8fc7ea931..7deeeddb1 100644
--- a/lighthouse-core/fraggle-rock/config/default-config.js
+++ b/lighthouse-core/fraggle-rock/config/default-config.js
@@ -32,6 +32,7 @@ const artifacts = {
   MainDocumentContent: '',
   MetaElements: '',
   NetworkUserAgent: '',
+  OptimizedImages: '',
   PasswordInputsWithPreventedPaste: '',
   RobotsTxt: '',
   SourceMaps: '',
@@ -76,6 +77,7 @@ const defaultConfig = {
     {id: artifacts.MainDocumentContent, gatherer: 'main-document-content'},
     {id: artifacts.MetaElements, gatherer: 'meta-elements'},
     {id: artifacts.NetworkUserAgent, gatherer: 'network-user-agent'},
+    {id: artifacts.OptimizedImages, gatherer: 'dobetterweb/optimized-images'},
     {id: artifacts.PasswordInputsWithPreventedPaste, gatherer: 'dobetterweb/password-inputs-with-prevented-paste'},
     {id: artifacts.RobotsTxt, gatherer: 'seo/robots-txt'},
     {id: artifacts.SourceMaps, gatherer: 'source-maps'},
@@ -118,6 +120,7 @@ const defaultConfig = {
         artifacts.MainDocumentContent,
         artifacts.MetaElements,
         artifacts.NetworkUserAgent,
+        artifacts.OptimizedImages,
         artifacts.PasswordInputsWithPreventedPaste,
         artifacts.RobotsTxt,
         artifacts.SourceMaps,
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
index 7f8297be4..610da8f46 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
@@ -11,11 +11,12 @@
 'use strict';
 
 const log = require('lighthouse-logger');
-const Gatherer = require('../gatherer.js');
+const FRGatherer = require('../../../fraggle-rock/gather/base-gatherer.js');
 const URL = require('../../../lib/url-shim.js');
 const NetworkRequest = require('../../../lib/network-request.js');
 const Sentry = require('../../../lib/sentry.js');
-const Driver = require('../../driver.js'); // eslint-disable-line no-unused-vars
+const NetworkRecords = require('../../../computed/network-records.js');
+const DevtoolsLog = require('../devtools-log.js');
 
 // Image encoding can be slow and we don't want to spend forever on it.
 // Cap our encoding to 5 seconds, anything after that will be estimated.
@@ -32,7 +33,13 @@ const IMAGE_REGEX = /^image\/((x|ms|x-ms)-)?(png|bmp|jpeg)$/;
 
 /** @typedef {{requestId: string, url: string, mimeType: string, resourceSize: number}} SimplifiedNetworkRecord */
 
-class OptimizedImages extends Gatherer {
+class OptimizedImages extends FRGatherer {
+  /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
+  meta = {
+    supportedModes: ['timespan', 'navigation'],
+    dependencies: {DevtoolsLog: DevtoolsLog.symbol},
+  }
+
   constructor() {
     super();
     this._encodingStartAt = 0;
@@ -70,25 +77,25 @@ class OptimizedImages extends Gatherer {
   }
 
   /**
-   * @param {Driver} driver
+   * @param {LH.Gatherer.FRProtocolSession} session
    * @param {string} requestId
    * @param {'jpeg'|'webp'} encoding Either webp or jpeg.
    * @return {Promise<LH.Crdp.Audits.GetEncodedResponseResponse>}
    */
-  _getEncodedResponse(driver, requestId, encoding) {
+  _getEncodedResponse(session, requestId, encoding) {
     requestId = NetworkRequest.getRequestIdForBackend(requestId);
 
     const quality = encoding === 'jpeg' ? JPEG_QUALITY : WEBP_QUALITY;
     const params = {requestId, encoding, quality, sizeOnly: true};
-    return driver.sendCommand('Audits.getEncodedResponse', params);
+    return session.sendCommand('Audits.getEncodedResponse', params);
   }
 
   /**
-   * @param {Driver} driver
+   * @param {LH.Gatherer.FRProtocolSession} session
    * @param {SimplifiedNetworkRecord} networkRecord
    * @return {Promise<{originalSize: number, jpegSize?: number, webpSize?: number}>}
    */
-  async calculateImageStats(driver, networkRecord) {
+  async calculateImageStats(session, networkRecord) {
     const originalSize = networkRecord.resourceSize;
     // Once we've hit our execution time limit or when the image is too big, don't try to re-encode it.
     // Images in this execution path will fallback to byte-per-pixel heuristics on the audit side.
@@ -97,8 +104,8 @@ class OptimizedImages extends Gatherer {
       return {originalSize, jpegSize: undefined, webpSize: undefined};
     }
 
-    const jpegData = await this._getEncodedResponse(driver, networkRecord.requestId, 'jpeg');
-    const webpData = await this._getEncodedResponse(driver, networkRecord.requestId, 'webp');
+    const jpegData = await this._getEncodedResponse(session, networkRecord.requestId, 'jpeg');
+    const webpData = await this._getEncodedResponse(session, networkRecord.requestId, 'webp');
 
     return {
       originalSize,
@@ -108,11 +115,11 @@ class OptimizedImages extends Gatherer {
   }
 
   /**
-   * @param {Driver} driver
+   * @param {LH.Gatherer.FRProtocolSession} session
    * @param {Array<SimplifiedNetworkRecord>} imageRecords
    * @return {Promise<LH.Artifacts['OptimizedImages']>}
    */
-  async computeOptimizedImages(driver, imageRecords) {
+  async computeOptimizedImages(session, imageRecords) {
     this._encodingStartAt = Date.now();
 
     /** @type {LH.Artifacts['OptimizedImages']} */
@@ -120,7 +127,7 @@ class OptimizedImages extends Gatherer {
 
     for (const record of imageRecords) {
       try {
-        const stats = await this.calculateImageStats(driver, record);
+        const stats = await this.calculateImageStats(session, record);
         /** @type {LH.Artifacts.OptimizedImage} */
         const image = {failed: false, ...stats, ...record};
         results.push(image);
@@ -145,26 +152,40 @@ class OptimizedImages extends Gatherer {
   }
 
   /**
-   * @param {LH.Gatherer.PassContext} passContext
-   * @param {LH.Gatherer.LoadData} loadData
+   * @param {LH.Gatherer.FRTransitionalContext} context
+   * @param {LH.Artifacts.NetworkRequest[]} networkRecords
    * @return {Promise<LH.Artifacts['OptimizedImages']>}
    */
-  afterPass(passContext, loadData) {
-    const networkRecords = loadData.networkRecords;
+  async _getArtifact(context, networkRecords) {
     const imageRecords = OptimizedImages
       .filterImageRequests(networkRecords)
       .sort((a, b) => b.resourceSize - a.resourceSize);
 
-    return Promise.resolve()
-      .then(_ => this.computeOptimizedImages(passContext.driver, imageRecords))
-      .then(results => {
-        const successfulResults = results.filter(result => !result.failed);
-        if (results.length && !successfulResults.length) {
-          throw new Error('All image optimizations failed');
-        }
+    const results = await this.computeOptimizedImages(context.driver.defaultSession, imageRecords);
+    const successfulResults = results.filter(result => !result.failed);
+    if (results.length && !successfulResults.length) {
+      throw new Error('All image optimizations failed');
+    }
+    return results;
+  }
 
-        return results;
-      });
+  /**
+   * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context
+   * @return {Promise<LH.Artifacts['OptimizedImages']>}
+   */
+  async getArtifact(context) {
+    const devtoolsLog = context.dependencies.DevtoolsLog;
+    const networkRecords = await NetworkRecords.request(devtoolsLog, context);
+    return this._getArtifact(context, networkRecords);
+  }
+
+  /**
+   * @param {LH.Gatherer.PassContext} passContext
+   * @param {LH.Gatherer.LoadData} loadData
+   * @return {Promise<LH.Artifacts['OptimizedImages']>}
+   */
+  async afterPass(passContext, loadData) {
+    return this._getArtifact({...passContext, dependencies: {}}, loadData.networkRecords);
   }
 }
 
diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html
index 9fdeeacc9..127bcc517 100644
--- a/lighthouse-treemap/app/index.html
+++ b/lighthouse-treemap/app/index.html
@@ -22,7 +22,7 @@ Unless required by applicable law or agreed to in writing, software distributed
     <div class="lh-settings">
       <header>
         <div class="lh-header--section">
-          <span>
+          <span class="lh-logotitle">
             <!-- Lighthouse logo. Stolen from templates.html -->
             <svg class="lh-topbar__logo" viewBox="0 0 24 24">
               <defs>
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js
index ee7e39a0b..0060d59d7 100644
--- a/lighthouse-treemap/app/src/main.js
+++ b/lighthouse-treemap/app/src/main.js
@@ -85,6 +85,7 @@ class TreemapViewer {
     /*  eslint-enable no-unused-expressions */
 
     this.createHeader();
+    this.toggleTable(window.innerWidth >= 600);
     this.initListeners();
     this.setSelector({type: 'group', value: 'scripts'});
     this.render();
@@ -542,11 +543,14 @@ class TreemapViewer {
     });
   }
 
-  toggleTable() {
+  /**
+   * @param {boolean=} show
+   */
+  toggleTable(show) {
     const mainEl = TreemapUtil.find('main');
-    mainEl.classList.toggle('lh-main--show-table');
+    mainEl.classList.toggle('lh-main--show-table', show);
     const buttonEl = TreemapUtil.find('.lh-button--toggle-table');
-    buttonEl.classList.toggle('lh-button--active');
+    buttonEl.classList.toggle('lh-button--active', show);
   }
 
   resize() {
@@ -636,17 +640,20 @@ function renderViewModeButtons(viewModes) {
     if (!viewMode.enabled) viewModeEl.classList.add('view-mode--disabled');
     viewModeEl.id = `view-mode--${viewMode.id}`;
 
-    const labelEl = TreemapUtil.createChildOf(viewModeEl, 'label');
-    TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__label').textContent = viewMode.label;
-    TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__sublabel lh-text-dim').textContent =
-      ` (${viewMode.subLabel})`;
-
-    const inputEl = TreemapUtil.createChildOf(labelEl, 'input', 'view-mode__button', {
+    const inputEl = TreemapUtil.createChildOf(viewModeEl, 'input', 'view-mode__button', {
+      id: `view-mode--${viewMode.id}__label`,
       type: 'radio',
       name: 'view-mode',
       disabled: viewMode.enabled ? undefined : '',
     });
 
+    const labelEl = TreemapUtil.createChildOf(viewModeEl, 'label', undefined, {
+      for: inputEl.id,
+    });
+    TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__label').textContent = viewMode.label;
+    TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__sublabel lh-text-dim').textContent =
+      ` (${viewMode.subLabel})`;
+
     inputEl.addEventListener('click', () => {
       treemapViewer.setViewMode(viewMode);
       treemapViewer.render();
diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css
index c8af99a21..e756e247f 100644
--- a/lighthouse-treemap/app/styles/treemap.css
+++ b/lighthouse-treemap/app/styles/treemap.css
@@ -56,6 +56,7 @@ body {
 /* TODO: BEM is backwards here and many other places */
 .lh-header--section {
   display: flex;
+  flex-wrap: wrap;
   align-items: center;
   justify-content: space-between;
 }
@@ -74,9 +75,15 @@ body {
   justify-content: flex-end;
 }
 
+.lh-logotitle {
+  display: flex;
+  align-items: center;
+}
+
 .bundle-selector {
-  width: 50%;
+  flex: 1;
   padding: 2px;
+  margin: 2px;
 }
 
 .bundle-selector {
@@ -134,6 +141,9 @@ body {
   padding: 5px;
   font-size: 16px;
 }
+.view-mode input[type='radio']:focus-visible + label {
+  outline: -webkit-focus-ring-color auto 1px;
+}
 .view-mode--disabled {
   text-decoration: line-through;
 }
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index be1a84b9b..aed8b7a8c 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -26,7 +26,6 @@ declare global {
       | 'InspectorIssues'
       | 'Manifest'
       | 'MixedContent'
-      | 'OptimizedImages'
       | 'ResponseCompression'
       | 'ScriptElements'
       | 'ServiceWorker'


Test patch:
diff --git a/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js b/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js
index 6f671b11e..0782a592d 100644
--- a/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js
+++ b/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js
@@ -8,8 +8,9 @@
 /* eslint-env jest */
 
 const OptimizedImages = require('../../../../gather/gatherers/dobetterweb/optimized-images.js');
+const {createMockContext} = require('../../../fraggle-rock/gather/mock-driver.js');
 
-let options;
+let context = createMockContext();
 let optimizedImages;
 
 const traceData = {
@@ -112,19 +113,16 @@ describe('Optimized images', () => {
   // Reset the Gatherer before each test.
   beforeEach(() => {
     optimizedImages = new OptimizedImages();
-    options = {
-      url: 'http://google.com/',
-      driver: {
-        sendCommand: function(command, params) {
-          const encodedSize = params.encoding === 'webp' ? 60 : 80;
-          return Promise.resolve({encodedSize});
-        },
-      },
-    };
+    context = createMockContext();
+    context.url = 'http://google.com';
+    context.driver.defaultSession.sendCommand.mockImplementation((_, params) => {
+      const encodedSize = params.encoding === 'webp' ? 60 : 80;
+      return Promise.resolve({encodedSize});
+    });
   });
 
   it('returns all images, sorted with sizes', async () => {
-    const artifact = await optimizedImages.afterPass(options, traceData);
+    const artifact = await optimizedImages.afterPass(context, traceData);
     expect(artifact).toHaveLength(5);
     expect(artifact).toMatchObject([
       {
@@ -162,16 +160,16 @@ describe('Optimized images', () => {
 
   it('handles partial driver failure', () => {
     let calls = 0;
-    options.driver.sendCommand = () => {
+    context.driver.defaultSession.sendCommand.mockImplementation(() => {
       calls++;
       if (calls > 2) {
         return Promise.reject(new Error('whoops driver failed'));
       } else {
         return Promise.resolve({encodedSize: 60});
       }
-    };
+    });
 
-    return optimizedImages.afterPass(options, traceData).then(artifact => {
+    return optimizedImages.afterPass(context, traceData).then(artifact => {
       const failed = artifact.find(record => record.failed);
 
       expect(artifact).toHaveLength(5);
@@ -194,7 +192,7 @@ describe('Optimized images', () => {
       ],
     };
 
-    const artifact = await optimizedImages.afterPass(options, traceData);
+    const artifact = await optimizedImages.afterPass(context, traceData);
     expect(artifact).toHaveLength(1);
   });
 });