lighthouse 8.6.0-dev.20211025 → 8.6.0-dev.20211029

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/report/flow.js +11 -11
  2. package/dist/report/standalone.js +2 -2
  3. package/flow-report/assets/styles.css +57 -1
  4. package/flow-report/src/common.tsx +8 -8
  5. package/flow-report/src/header.tsx +7 -8
  6. package/flow-report/src/i18n/ui-strings.js +2 -0
  7. package/flow-report/src/summary/category.tsx +73 -4
  8. package/flow-report/src/summary/summary.tsx +2 -1
  9. package/flow-report/src/util.ts +4 -4
  10. package/flow-report/src/wrappers/markdown.tsx +27 -0
  11. package/flow-report/src/wrappers/report.tsx +12 -0
  12. package/flow-report/test/common-test.tsx +6 -6
  13. package/flow-report/test/flow-report-pptr-test.ts +46 -0
  14. package/flow-report/test/summary/category-test.tsx +111 -19
  15. package/flow-report/test/wrappers/markdown-test.tsx +29 -0
  16. package/jest.config.js +2 -1
  17. package/lighthouse-core/audits/byte-efficiency/modern-image-formats.js +4 -2
  18. package/lighthouse-core/audits/byte-efficiency/offscreen-images.js +11 -10
  19. package/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js +5 -3
  20. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +2 -1
  21. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +2 -1
  22. package/lighthouse-core/audits/errors-in-console.js +0 -2
  23. package/lighthouse-core/audits/image-aspect-ratio.js +5 -5
  24. package/lighthouse-core/audits/image-size-responsive.js +6 -6
  25. package/lighthouse-core/audits/preload-lcp-image.js +7 -5
  26. package/lighthouse-core/audits/script-treemap-data.js +1 -1
  27. package/lighthouse-core/audits/unsized-images.js +2 -3
  28. package/lighthouse-core/computed/image-records.js +4 -3
  29. package/lighthouse-core/config/default-config.js +0 -3
  30. package/lighthouse-core/fraggle-rock/config/default-config.js +0 -3
  31. package/lighthouse-core/gather/driver/storage.js +0 -1
  32. package/lighthouse-core/gather/gatherers/image-elements.js +0 -3
  33. package/package.json +4 -4
  34. package/report/README.md +1 -1
  35. package/report/generator/README.md +1 -1
  36. package/report/renderer/category-renderer.js +7 -0
  37. package/report/renderer/pwa-category-renderer.js +0 -10
  38. package/report/renderer/report-renderer.js +1 -1
  39. package/report/test/renderer/category-renderer-test.js +41 -0
  40. package/report/test/renderer/pwa-category-renderer-test.js +4 -4
  41. package/report/test/renderer/text-encoding-test.js +1 -1
  42. package/shared/localization/format.js +5 -9
  43. package/shared/localization/locales/ar-XB.json +8 -20
  44. package/shared/localization/locales/ar.json +8 -20
  45. package/shared/localization/locales/bg.json +8 -20
  46. package/shared/localization/locales/ca.json +8 -20
  47. package/shared/localization/locales/cs.json +8 -20
  48. package/shared/localization/locales/da.json +8 -20
  49. package/shared/localization/locales/de.json +8 -20
  50. package/shared/localization/locales/el.json +8 -20
  51. package/shared/localization/locales/en-GB.json +8 -20
  52. package/shared/localization/locales/en-US.json +27 -36
  53. package/shared/localization/locales/en-XA.json +8 -20
  54. package/shared/localization/locales/en-XL.json +27 -36
  55. package/shared/localization/locales/es-419.json +8 -20
  56. package/shared/localization/locales/es.json +8 -20
  57. package/shared/localization/locales/fi.json +8 -20
  58. package/shared/localization/locales/fil.json +8 -20
  59. package/shared/localization/locales/fr.json +8 -20
  60. package/shared/localization/locales/he.json +8 -20
  61. package/shared/localization/locales/hi.json +8 -20
  62. package/shared/localization/locales/hr.json +8 -20
  63. package/shared/localization/locales/hu.json +8 -20
  64. package/shared/localization/locales/id.json +8 -20
  65. package/shared/localization/locales/it.json +8 -20
  66. package/shared/localization/locales/ja.json +8 -20
  67. package/shared/localization/locales/ko.json +8 -20
  68. package/shared/localization/locales/lt.json +8 -20
  69. package/shared/localization/locales/lv.json +8 -20
  70. package/shared/localization/locales/nl.json +8 -20
  71. package/shared/localization/locales/no.json +8 -20
  72. package/shared/localization/locales/pl.json +8 -20
  73. package/shared/localization/locales/pt-PT.json +8 -20
  74. package/shared/localization/locales/pt.json +8 -20
  75. package/shared/localization/locales/ro.json +8 -20
  76. package/shared/localization/locales/ru.json +8 -20
  77. package/shared/localization/locales/sk.json +8 -20
  78. package/shared/localization/locales/sl.json +8 -20
  79. package/shared/localization/locales/sr-Latn.json +8 -20
  80. package/shared/localization/locales/sr.json +8 -20
  81. package/shared/localization/locales/sv.json +8 -20
  82. package/shared/localization/locales/ta.json +8 -20
  83. package/shared/localization/locales/te.json +8 -20
  84. package/shared/localization/locales/th.json +8 -20
  85. package/shared/localization/locales/tr.json +8 -20
  86. package/shared/localization/locales/uk.json +8 -20
  87. package/shared/localization/locales/vi.json +8 -20
  88. package/shared/localization/locales/zh-HK.json +8 -20
  89. package/shared/localization/locales/zh-TW.json +8 -20
  90. package/shared/localization/locales/zh.json +8 -20
  91. package/tsconfig-all.json +1 -1
  92. package/types/artifacts.d.ts +5 -4
  93. package/types/lhr/audit-details.d.ts +5 -5
  94. package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
  95. package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
@@ -11,6 +11,7 @@ import {SummaryTooltip} from '../../src/summary/category';
11
11
  import {flowResult} from '../sample-flow';
12
12
  import {I18nProvider} from '../../src/i18n/i18n';
13
13
  import {FlowResultContext} from '../../src/util';
14
+ import {ReportRendererProvider} from '../../src/wrappers/report-renderer';
14
15
 
15
16
  let wrapper: FunctionComponent;
16
17
 
@@ -18,9 +19,11 @@ beforeEach(() => {
18
19
  // Include sample flowResult for locale in I18nProvider.
19
20
  wrapper = ({children}) => (
20
21
  <FlowResultContext.Provider value={flowResult}>
21
- <I18nProvider>
22
- {children}
23
- </I18nProvider>
22
+ <ReportRendererProvider>
23
+ <I18nProvider>
24
+ {children}
25
+ </I18nProvider>
26
+ </ReportRendererProvider>
24
27
  </FlowResultContext.Provider>
25
28
  );
26
29
  });
@@ -31,14 +34,16 @@ describe('SummaryTooltip', () => {
31
34
  id: 'performance',
32
35
  score: 1,
33
36
  auditRefs: [
34
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
35
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
36
- {result: {score: 0, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
37
+ /* eslint-disable max-len */
38
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 1, group: 'diagnostics'},
39
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 1, group: 'diagnostics'},
40
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 3'}, weight: 1, group: 'diagnostics'},
41
+ /* eslint-enable max-len */
37
42
  ],
38
43
  };
39
44
 
40
45
  const root = render(
41
- <SummaryTooltip category={category} gatherMode="snapshot"/>,
46
+ <SummaryTooltip category={category} gatherMode="snapshot" url="https://example.com"/>,
42
47
  {wrapper}
43
48
  );
44
49
 
@@ -46,6 +51,7 @@ describe('SummaryTooltip', () => {
46
51
  expect(() => root.getByText(/^[0-9]+$/)).toThrow();
47
52
  expect(root.getByText('2 audits passed')).toBeTruthy();
48
53
  expect(root.getByText('3 passable audits')).toBeTruthy();
54
+ expect(root.getByText('https://example.com'));
49
55
  });
50
56
 
51
57
  it('renders tooltip without rating', async () => {
@@ -53,14 +59,16 @@ describe('SummaryTooltip', () => {
53
59
  id: 'performance',
54
60
  score: 1,
55
61
  auditRefs: [
56
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 0, group: 'diagnostics'},
57
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 0, group: 'diagnostics'},
58
- {result: {score: 0, scoreDisplayMode: 'binary'}, weight: 0, group: 'diagnostics'},
62
+ /* eslint-disable max-len */
63
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 0, group: 'diagnostics'},
64
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 0, group: 'diagnostics'},
65
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 3'}, weight: 0, group: 'diagnostics'},
66
+ /* eslint-enable max-len */
59
67
  ],
60
68
  };
61
69
 
62
70
  const root = render(
63
- <SummaryTooltip category={category} gatherMode="snapshot"/>,
71
+ <SummaryTooltip category={category} gatherMode="snapshot" url="https://example.com"/>,
64
72
  {wrapper}
65
73
  );
66
74
 
@@ -68,6 +76,7 @@ describe('SummaryTooltip', () => {
68
76
  expect(() => root.getByText(/^[0-9]+$/)).toThrow();
69
77
  expect(root.getByText('2 audits passed')).toBeTruthy();
70
78
  expect(root.getByText('3 passable audits')).toBeTruthy();
79
+ expect(root.getByText('https://example.com'));
71
80
  });
72
81
 
73
82
  it('renders scored category tooltip with score', async () => {
@@ -75,14 +84,16 @@ describe('SummaryTooltip', () => {
75
84
  id: 'performance',
76
85
  score: 1,
77
86
  auditRefs: [
78
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
79
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
80
- {result: {score: 0, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
87
+ /* eslint-disable max-len */
88
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 1, group: 'diagnostics'},
89
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 1, group: 'diagnostics'},
90
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 3'}, weight: 1, group: 'diagnostics'},
91
+ /* eslint-enable max-len */
81
92
  ],
82
93
  };
83
94
 
84
95
  const root = render(
85
- <SummaryTooltip category={category} gatherMode="navigation"/>,
96
+ <SummaryTooltip category={category} gatherMode="navigation" url="https://example.com"/>,
86
97
  {wrapper}
87
98
  );
88
99
 
@@ -90,6 +101,7 @@ describe('SummaryTooltip', () => {
90
101
  expect(root.getByText('100')).toBeTruthy();
91
102
  expect(root.getByText('2 audits passed')).toBeTruthy();
92
103
  expect(root.getByText('3 passable audits')).toBeTruthy();
104
+ expect(root.getByText('https://example.com'));
93
105
  });
94
106
 
95
107
  it('renders informative audit count if any', async () => {
@@ -97,14 +109,16 @@ describe('SummaryTooltip', () => {
97
109
  id: 'performance',
98
110
  score: 1,
99
111
  auditRefs: [
100
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
101
- {result: {score: 1, scoreDisplayMode: 'binary'}, weight: 1, group: 'diagnostics'},
102
- {result: {score: 0, scoreDisplayMode: 'informative'}, weight: 1, group: 'diagnostics'},
112
+ /* eslint-disable max-len */
113
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 1, group: 'diagnostics'},
114
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 1, group: 'diagnostics'},
115
+ {result: {score: null, scoreDisplayMode: 'informative', title: 'Audit 3'}, weight: 1, group: 'diagnostics'},
116
+ /* eslint-enable max-len */
103
117
  ],
104
118
  };
105
119
 
106
120
  const root = render(
107
- <SummaryTooltip category={category} gatherMode="navigation"/>,
121
+ <SummaryTooltip category={category} gatherMode="navigation" url="https://example.com"/>,
108
122
  {wrapper}
109
123
  );
110
124
 
@@ -113,5 +127,83 @@ describe('SummaryTooltip', () => {
113
127
  expect(root.getByText('2 audits passed')).toBeTruthy();
114
128
  expect(root.getByText('2 passable audits')).toBeTruthy();
115
129
  expect(root.getByText('1 informative audit')).toBeTruthy();
130
+ expect(root.getByText('https://example.com'));
131
+ });
132
+
133
+ it('renders highest impact audits', async () => {
134
+ const category: any = {
135
+ id: 'seo',
136
+ score: 1,
137
+ auditRefs: [
138
+ /* eslint-disable max-len */
139
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 1, group: 'group'},
140
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 2, group: 'group'},
141
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 3'}, weight: 3, group: 'group'},
142
+ /* eslint-enable max-len */
143
+ ],
144
+ };
145
+
146
+ const root = render(
147
+ <SummaryTooltip category={category} gatherMode="navigation" url="https://example.com"/>,
148
+ {wrapper}
149
+ );
150
+
151
+ const audits = root.getAllByText(/^Audit [0-9]$/);
152
+
153
+ expect(root.getByText('Highest impact')).toBeTruthy();
154
+ expect(audits.map(a => a.textContent)).toEqual([
155
+ 'Audit 3',
156
+ 'Audit 2',
157
+ ]);
158
+ });
159
+
160
+ it('renders highest impact audits in performance', async () => {
161
+ const category: any = {
162
+ id: 'performance',
163
+ score: 0.75,
164
+ auditRefs: [
165
+ /* eslint-disable max-len */
166
+ {result: {score: 0.75, scoreDisplayMode: 'numeric', title: 'Metric 1'}, weight: 1, group: 'metrics'},
167
+ {result: {score: 0, scoreDisplayMode: 'numeric', title: 'Audit 1', details: {type: 'opportunity', overallSavingsMs: 500}}, weight: 0, group: 'opportunities'},
168
+ {result: {score: 0, scoreDisplayMode: 'numeric', title: 'Audit 2', details: {type: 'opportunity', overallSavingsMs: 1000}}, weight: 0, group: 'opportunities'},
169
+ {result: {score: 0, scoreDisplayMode: 'numeric', title: 'Audit 3', details: {type: 'opportunity', overallSavingsMs: 100}}, weight: 0, group: 'opportunities'},
170
+ /* eslint-enable max-len */
171
+ ],
172
+ };
173
+
174
+ const root = render(
175
+ <SummaryTooltip category={category} gatherMode="navigation" url="https://example.com"/>,
176
+ {wrapper}
177
+ );
178
+
179
+ const audits = root.getAllByText(/^(Audit|Metric) [0-9]$/);
180
+
181
+ expect(root.getByText('Highest impact')).toBeTruthy();
182
+ expect(audits.map(a => a.textContent)).toEqual([
183
+ 'Audit 2',
184
+ 'Audit 1',
185
+ ]);
186
+ });
187
+
188
+ it('hides highest impact if nothing to show', async () => {
189
+ const category: any = {
190
+ id: 'performance',
191
+ score: 1,
192
+ auditRefs: [
193
+ /* eslint-disable max-len */
194
+ {result: {score: 1, scoreDisplayMode: 'binary', title: 'Audit 1'}, weight: 1, group: 'diagnostics'},
195
+ {result: {score: 0, scoreDisplayMode: 'binary', title: 'Audit 2'}, weight: 1},
196
+ {result: {score: null, scoreDisplayMode: 'informative', title: 'Audit 3'}, weight: 1, group: 'diagnostics'},
197
+ /* eslint-enable max-len */
198
+ ],
199
+ };
200
+
201
+ const root = render(
202
+ <SummaryTooltip category={category} gatherMode="navigation" url="https://example.com"/>,
203
+ {wrapper}
204
+ );
205
+
206
+ expect(() => root.getByText('Highest impact')).toThrow();
207
+ expect(() => root.getByText(/^Audit [0-9]$/)).toThrow();
116
208
  });
117
209
  });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
3
+ * 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
4
+ * 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.
5
+ */
6
+
7
+ import {render} from '@testing-library/preact';
8
+ import {FunctionComponent} from 'preact';
9
+
10
+ import {Markdown} from '../../src/wrappers/markdown';
11
+ import {ReportRendererProvider} from '../../src/wrappers/report-renderer';
12
+
13
+ let wrapper: FunctionComponent;
14
+
15
+ beforeEach(() => {
16
+ wrapper = ({children}) => (
17
+ <ReportRendererProvider>
18
+ {children}
19
+ </ReportRendererProvider>
20
+ );
21
+ });
22
+
23
+ describe('Markdown', () => {
24
+ it('renders markdown text', () => {
25
+ const root = render(<Markdown text="Some `fancy` text"/>, {wrapper});
26
+ const text = root.getByText(/^Some.*text$/);
27
+ expect(text.innerHTML).toEqual('Some <code>fancy</code> text');
28
+ });
29
+ });
package/jest.config.js CHANGED
@@ -13,13 +13,14 @@ module.exports = {
13
13
  '**/lighthouse-cli/**/*-test.js',
14
14
  '**/report/**/*-test.js',
15
15
  '**/lighthouse-core/test/fraggle-rock/**/*-test-pptr.js',
16
- '**/lighthouse-treemap/**/*-test.js',
16
+ '**/treemap/**/*-test.js',
17
17
  '**/viewer/**/*-test.js',
18
18
  '**/third-party/**/*-test.js',
19
19
  '**/clients/test/**/*-test.js',
20
20
  '**/shared/**/*-test.js',
21
21
  '**/build/**/*-test.js',
22
22
  ],
23
+ modulePathIgnorePatterns: ['<rootDir>/.tmp'],
23
24
  transform: {},
24
25
  prettierPath: null,
25
26
  projects: [
@@ -103,6 +103,8 @@ class ModernImageFormats extends ByteEfficiencyAudit {
103
103
  const items = [];
104
104
  const warnings = [];
105
105
  for (const image of images) {
106
+ const imageElement = imageElementsByURL.get(image.url);
107
+
106
108
  if (image.failed) {
107
109
  warnings.push(`Unable to decode ${URL.getURLDisplayName(image.url)}`);
108
110
  continue;
@@ -120,7 +122,6 @@ class ModernImageFormats extends ByteEfficiencyAudit {
120
122
  let fromProtocol = true;
121
123
 
122
124
  if (typeof webpSize === 'undefined') {
123
- const imageElement = imageElementsByURL.get(image.url);
124
125
  if (!imageElement) {
125
126
  warnings.push(`Unable to locate resource ${URL.getURLDisplayName(image.url)}`);
126
127
  continue;
@@ -155,6 +156,7 @@ class ModernImageFormats extends ByteEfficiencyAudit {
155
156
  const isCrossOrigin = !URL.originsMatch(pageURL, image.url);
156
157
 
157
158
  items.push({
159
+ node: imageElement ? ByteEfficiencyAudit.makeNodeItem(imageElement.node) : undefined,
158
160
  url,
159
161
  fromProtocol,
160
162
  isCrossOrigin,
@@ -166,7 +168,7 @@ class ModernImageFormats extends ByteEfficiencyAudit {
166
168
 
167
169
  /** @type {LH.Audit.Details.Opportunity['headings']} */
168
170
  const headings = [
169
- {key: 'url', valueType: 'thumbnail', label: ''},
171
+ {key: 'node', valueType: 'node', label: ''},
170
172
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
171
173
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
172
174
  {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
@@ -37,7 +37,7 @@ const IGNORE_THRESHOLD_IN_BYTES = 2048;
37
37
  const IGNORE_THRESHOLD_IN_PERCENT = 75;
38
38
  const IGNORE_THRESHOLD_IN_MS = 50;
39
39
 
40
- /** @typedef {{url: string, requestStartTime: number, totalBytes: number, wastedBytes: number, wastedPercent: number}} WasteResult */
40
+ /** @typedef {{node: LH.Audit.Details.NodeValue, url: string, requestStartTime: number, totalBytes: number, wastedBytes: number, wastedPercent: number}} WasteResult */
41
41
 
42
42
  class OffscreenImages extends ByteEfficiencyAudit {
43
43
  /**
@@ -100,6 +100,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
100
100
  }
101
101
 
102
102
  return {
103
+ node: ByteEfficiencyAudit.makeNodeItem(image.node),
103
104
  url,
104
105
  requestStartTime: networkRecord.startTime,
105
106
  totalBytes,
@@ -184,26 +185,26 @@ class OffscreenImages extends ByteEfficiencyAudit {
184
185
 
185
186
  /** @type {string[]} */
186
187
  const warnings = [];
187
- const resultsMap = images.reduce((results, image) => {
188
+ /** @type {Map<string, WasteResult>} */
189
+ const resultsMap = new Map();
190
+ for (const image of images) {
188
191
  const processed = OffscreenImages.computeWaste(image, viewportDimensions, networkRecords);
189
192
  if (processed === null) {
190
- return results;
193
+ continue;
191
194
  }
192
195
 
193
196
  if (processed instanceof Error) {
194
197
  warnings.push(processed.message);
195
198
  Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'});
196
- return results;
199
+ continue;
197
200
  }
198
201
 
199
202
  // If an image was used more than once, warn only about its least wasteful usage
200
- const existing = results.get(processed.url);
203
+ const existing = resultsMap.get(processed.url);
201
204
  if (!existing || existing.wastedBytes > processed.wastedBytes) {
202
- results.set(processed.url, processed);
205
+ resultsMap.set(processed.url, processed);
203
206
  }
204
-
205
- return results;
206
- }, /** @type {Map<string, WasteResult>} */ (new Map()));
207
+ }
207
208
 
208
209
  const settings = context.settings;
209
210
 
@@ -233,7 +234,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
233
234
 
234
235
  /** @type {LH.Audit.Details.Opportunity['headings']} */
235
236
  const headings = [
236
- {key: 'url', valueType: 'thumbnail', label: ''},
237
+ {key: 'node', valueType: 'node', label: ''},
237
238
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
238
239
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
239
240
  {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
@@ -76,10 +76,12 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
76
76
  const imageElementsByURL = new Map();
77
77
  imageElements.forEach(img => imageElementsByURL.set(img.src, img));
78
78
 
79
- /** @type {Array<{url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */
79
+ /** @type {Array<{node?: LH.Audit.Details.NodeValue, url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */
80
80
  const items = [];
81
81
  const warnings = [];
82
82
  for (const image of images) {
83
+ const imageElement = imageElementsByURL.get(image.url);
84
+
83
85
  if (image.failed) {
84
86
  warnings.push(`Unable to decode ${URL.getURLDisplayName(image.url)}`);
85
87
  continue;
@@ -91,7 +93,6 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
91
93
  let fromProtocol = true;
92
94
 
93
95
  if (typeof jpegSize === 'undefined') {
94
- const imageElement = imageElementsByURL.get(image.url);
95
96
  if (!imageElement) {
96
97
  warnings.push(`Unable to locate resource ${URL.getURLDisplayName(image.url)}`);
97
98
  continue;
@@ -115,6 +116,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
115
116
  const jpegSavings = UsesOptimizedImages.computeSavings({...image, jpegSize});
116
117
 
117
118
  items.push({
119
+ node: imageElement ? ByteEfficiencyAudit.makeNodeItem(imageElement.node) : undefined,
118
120
  url,
119
121
  fromProtocol,
120
122
  isCrossOrigin,
@@ -125,7 +127,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
125
127
 
126
128
  /** @type {LH.Audit.Details.Opportunity['headings']} */
127
129
  const headings = [
128
- {key: 'url', valueType: 'thumbnail', label: ''},
130
+ {key: 'node', valueType: 'node', label: ''},
129
131
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
130
132
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
131
133
  {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
@@ -70,6 +70,7 @@ class UsesResponsiveImagesSnapshot extends Audit {
70
70
  if (actualPixels - usedPixels > IGNORE_THRESHOLD_IN_PIXELS) score = 0;
71
71
 
72
72
  items.push({
73
+ node: Audit.makeNodeItem(image.node),
73
74
  url: URL.elideDataURI(image.src),
74
75
  displayedDimensions: `${displayed.width}x${displayed.height}`,
75
76
  actualDimensions: `${actual.width}x${actual.height}`,
@@ -79,7 +80,7 @@ class UsesResponsiveImagesSnapshot extends Audit {
79
80
  /** @type {LH.Audit.Details.Table['headings']} */
80
81
  const headings = [
81
82
  /* eslint-disable max-len */
82
- {key: 'url', itemType: 'thumbnail', text: ''},
83
+ {key: 'node', itemType: 'node', text: ''},
83
84
  {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
84
85
  {key: 'displayedDimensions', itemType: 'text', text: str_(UIStrings.columnDisplayedDimensions)},
85
86
  {key: 'actualDimensions', itemType: 'text', text: str_(UIStrings.columnActualDimensions)},
@@ -104,6 +104,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
104
104
  const wastedBytes = Math.round(totalBytes * wastedRatio);
105
105
 
106
106
  return {
107
+ node: ByteEfficiencyAudit.makeNodeItem(image.node),
107
108
  url,
108
109
  totalBytes,
109
110
  wastedBytes,
@@ -158,7 +159,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
158
159
 
159
160
  /** @type {LH.Audit.Details.Opportunity['headings']} */
160
161
  const headings = [
161
- {key: 'url', valueType: 'thumbnail', label: ''},
162
+ {key: 'node', valueType: 'node', label: ''},
162
163
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
163
164
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
164
165
  {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
@@ -88,8 +88,6 @@ class ErrorLogs extends Audit {
88
88
  return {
89
89
  source: item.source,
90
90
  description: item.text,
91
- // TODO: remove for v8 (url is covered in sourceLocation)
92
- url: item.url,
93
91
  sourceLocation: Audit.makeSourceLocationFromConsoleMessage(item),
94
92
  };
95
93
  });
@@ -51,7 +51,7 @@ class ImageAspectRatio extends Audit {
51
51
 
52
52
  /**
53
53
  * @param {WellDefinedImage} image
54
- * @return {{url: string, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}}
54
+ * @return {{url: string, node: LH.Audit.Details.NodeValue, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}}
55
55
  */
56
56
  static computeAspectRatios(image) {
57
57
  const url = URL.elideDataURI(image.src);
@@ -63,6 +63,7 @@ class ImageAspectRatio extends Audit {
63
63
 
64
64
  return {
65
65
  url,
66
+ node: Audit.makeNodeItem(image.node),
66
67
  displayedAspectRatio: `${image.displayedWidth} x ${image.displayedHeight}
67
68
  (${displayedAspectRatio.toFixed(2)})`,
68
69
  actualAspectRatio: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}
@@ -78,7 +79,7 @@ class ImageAspectRatio extends Audit {
78
79
  static audit(artifacts) {
79
80
  const images = artifacts.ImageElements;
80
81
 
81
- /** @type {Array<{url: string, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}>} */
82
+ /** @type {Array<{url: string, node: LH.Audit.Details.NodeValue, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}>} */
82
83
  const results = [];
83
84
  images.filter(image => {
84
85
  // - filter out css background images since we don't have a reliable way to tell if it's a
@@ -88,8 +89,7 @@ class ImageAspectRatio extends Audit {
88
89
  // - filter all svgs as they have no natural dimensions to audit
89
90
  // - filter out images that have falsy naturalWidth or naturalHeight
90
91
  return !image.isCss &&
91
- image.mimeType &&
92
- image.mimeType !== 'image/svg+xml' &&
92
+ URL.guessMimeType(image.src) !== 'image/svg+xml' &&
93
93
  image.naturalDimensions &&
94
94
  image.naturalDimensions.height > 5 &&
95
95
  image.naturalDimensions.width > 5 &&
@@ -105,7 +105,7 @@ class ImageAspectRatio extends Audit {
105
105
 
106
106
  /** @type {LH.Audit.Details.Table['headings']} */
107
107
  const headings = [
108
- {key: 'url', itemType: 'thumbnail', text: ''},
108
+ {key: 'node', itemType: 'node', text: ''},
109
109
  {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
110
110
  {key: 'displayedAspectRatio', itemType: 'text', text: str_(UIStrings.columnDisplayed)},
111
111
  {key: 'actualAspectRatio', itemType: 'text', text: str_(UIStrings.columnActual)},
@@ -45,7 +45,7 @@ const LARGE_IMAGE_FACTOR = 0.75;
45
45
  // considered SMALL.
46
46
  const SMALL_IMAGE_THRESHOLD = 64;
47
47
 
48
- /** @typedef {{url: string, elidedUrl: string, displayedSize: string, actualSize: string, actualPixels: number, expectedSize: string, expectedPixels: number}} Result */
48
+ /** @typedef {{url: string, node: LH.Audit.Details.NodeValue, displayedSize: string, actualSize: string, actualPixels: number, expectedSize: string, expectedPixels: number}} Result */
49
49
 
50
50
  /**
51
51
  * @param {{top: number, bottom: number, left: number, right: number}} imageRect
@@ -95,7 +95,7 @@ function isCandidate(image) {
95
95
  ) {
96
96
  return false;
97
97
  }
98
- if (image.mimeType === 'image/svg+xml') {
98
+ if (URL.guessMimeType(image.src) === 'image/svg+xml') {
99
99
  return false;
100
100
  }
101
101
  if (image.isCss) {
@@ -146,8 +146,8 @@ function getResult(image, DPR) {
146
146
  const [expectedWidth, expectedHeight] =
147
147
  expectedImageSize(image.displayedWidth, image.displayedHeight, DPR);
148
148
  return {
149
- url: image.src,
150
- elidedUrl: URL.elideDataURI(image.src),
149
+ url: URL.elideDataURI(image.src),
150
+ node: Audit.makeNodeItem(image.node),
151
151
  displayedSize: `${image.displayedWidth} x ${image.displayedHeight}`,
152
152
  actualSize: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}`,
153
153
  actualPixels: image.naturalDimensions.width * image.naturalDimensions.height,
@@ -263,8 +263,8 @@ class ImageSizeResponsive extends Audit {
263
263
 
264
264
  /** @type {LH.Audit.Details.Table['headings']} */
265
265
  const headings = [
266
- {key: 'url', itemType: 'thumbnail', text: ''},
267
- {key: 'elidedUrl', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
266
+ {key: 'node', itemType: 'node', text: ''},
267
+ {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
268
268
  {key: 'displayedSize', itemType: 'text', text: str_(UIStrings.columnDisplayed)},
269
269
  {key: 'actualSize', itemType: 'text', text: str_(UIStrings.columnActual)},
270
270
  {key: 'expectedSize', itemType: 'text', text: str_(UIStrings.columnExpected)},
@@ -106,13 +106,14 @@ class PreloadLCPImageAudit extends Audit {
106
106
 
107
107
  /**
108
108
  * Computes the estimated effect of preloading the LCP image.
109
+ * @param {LH.Artifacts.TraceElement|undefined} lcpElement
109
110
  * @param {LH.Gatherer.Simulation.GraphNetworkNode|undefined} lcpNode
110
111
  * @param {LH.Gatherer.Simulation.GraphNode} graph
111
112
  * @param {LH.Gatherer.Simulation.Simulator} simulator
112
- * @return {{wastedMs: number, results: Array<{url: string, wastedMs: number}>}}
113
+ * @return {{wastedMs: number, results: Array<{node: LH.Audit.Details.NodeValue, url: string, wastedMs: number}>}}
113
114
  */
114
- static computeWasteWithGraph(lcpNode, graph, simulator) {
115
- if (!lcpNode) {
115
+ static computeWasteWithGraph(lcpElement, lcpNode, graph, simulator) {
116
+ if (!lcpElement || !lcpNode) {
116
117
  return {
117
118
  wastedMs: 0,
118
119
  results: [],
@@ -185,6 +186,7 @@ class PreloadLCPImageAudit extends Audit {
185
186
  return {
186
187
  wastedMs,
187
188
  results: [{
189
+ node: Audit.makeNodeItem(lcpElement.node),
188
190
  url: lcpNode.record.url,
189
191
  wastedMs,
190
192
  }],
@@ -216,11 +218,11 @@ class PreloadLCPImageAudit extends Audit {
216
218
  const lcpNode = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
217
219
 
218
220
  const {results, wastedMs} =
219
- PreloadLCPImageAudit.computeWasteWithGraph(lcpNode, graph, simulator);
221
+ PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNode, graph, simulator);
220
222
 
221
223
  /** @type {LH.Audit.Details.Opportunity['headings']} */
222
224
  const headings = [
223
- {key: 'url', valueType: 'thumbnail', label: ''},
225
+ {key: 'node', valueType: 'node', label: ''},
224
226
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
225
227
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
226
228
  ];
@@ -8,7 +8,7 @@
8
8
  /**
9
9
  * @fileoverview
10
10
  * Creates nodes for treemap app.
11
- * Example output: lighthouse-treemap/app/debug.json
11
+ * Example output: treemap/app/debug.json
12
12
  */
13
13
 
14
14
  /**
@@ -106,7 +106,7 @@ class UnsizedImages extends Audit {
106
106
  * @return {boolean}
107
107
  */
108
108
  static isNonNetworkSvg(image) {
109
- const isSvg = image.mimeType === 'image/svg+xml';
109
+ const isSvg = URL.guessMimeType(image.src) === 'image/svg+xml';
110
110
  const urlScheme = image.src.slice(0, image.src.indexOf(':'));
111
111
  const isNonNetwork = URL.isNonNetworkProtocol(urlScheme);
112
112
  return isSvg && isNonNetwork;
@@ -147,9 +147,8 @@ class UnsizedImages extends Audit {
147
147
 
148
148
  /** @type {LH.Audit.Details.Table['headings']} */
149
149
  const headings = [
150
- {key: 'url', itemType: 'thumbnail', text: ''},
150
+ {key: 'node', itemType: 'node', text: ''},
151
151
  {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
152
- {key: 'node', itemType: 'node', text: str_(i18n.UIStrings.columnFailingElem)},
153
152
  ];
154
153
 
155
154
  return {
@@ -5,6 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
+ const URL = require('../lib/url-shim.js');
8
9
  const makeComputedArtifact = require('./computed-artifact.js');
9
10
 
10
11
  class ImageRecords {
@@ -28,12 +29,12 @@ class ImageRecords {
28
29
 
29
30
  /**
30
31
  * @param {{ImageElements: LH.Artifacts['ImageElements'], networkRecords: LH.Artifacts.NetworkRequest[]}} data
31
- * @return {Promise<LH.Artifacts.ImageElement[]>}
32
+ * @return {Promise<LH.Artifacts.ImageElementRecord[]>}
32
33
  */
33
34
  static async compute_(data) {
34
35
  const indexedNetworkRecords = ImageRecords.indexNetworkRecords(data.networkRecords);
35
36
 
36
- /** @type {LH.Artifacts.ImageElement[]} */
37
+ /** @type {LH.Artifacts.ImageElementRecord[]} */
37
38
  const imageRecords = [];
38
39
 
39
40
  for (const element of data.ImageElements) {
@@ -43,7 +44,7 @@ class ImageRecords {
43
44
  // Don't change the guessed mime type if no mime type was found.
44
45
  imageRecords.push({
45
46
  ...element,
46
- mimeType: mimeType ? mimeType : element.mimeType,
47
+ mimeType: mimeType ? mimeType : URL.guessMimeType(element.src),
47
48
  });
48
49
  }
49
50
 
@@ -147,7 +147,6 @@ const defaultConfig = {
147
147
  'form-elements',
148
148
  'main-document-content',
149
149
  'global-listeners',
150
- 'dobetterweb/appcache',
151
150
  'dobetterweb/doctype',
152
151
  'dobetterweb/domstats',
153
152
  'dobetterweb/optimized-images',
@@ -301,7 +300,6 @@ const defaultConfig = {
301
300
  'byte-efficiency/efficient-animated-content',
302
301
  'byte-efficiency/duplicated-javascript',
303
302
  'byte-efficiency/legacy-javascript',
304
- 'dobetterweb/appcache-manifest',
305
303
  'dobetterweb/doctype',
306
304
  'dobetterweb/charset',
307
305
  'dobetterweb/dom-size',
@@ -565,7 +563,6 @@ const defaultConfig = {
565
563
  {id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
566
564
  // General Group
567
565
  {id: 'no-unload-listeners', weight: 1, group: 'best-practices-general'},
568
- {id: 'appcache-manifest', weight: 1, group: 'best-practices-general'},
569
566
  {id: 'js-libraries', weight: 0, group: 'best-practices-general'},
570
567
  {id: 'deprecations', weight: 1, group: 'best-practices-general'},
571
568
  {id: 'errors-in-console', weight: 1, group: 'best-practices-general'},