lighthouse 9.3.1 → 9.4.0

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 (58) hide show
  1. package/dist/report/bundle.esm.js +38 -24
  2. package/dist/report/flow.js +5 -5
  3. package/dist/report/standalone.js +9 -9
  4. package/flow-report/src/topbar.tsx +1 -1
  5. package/flow-report/src/wrappers/report.tsx +2 -1
  6. package/flow-report/test/topbar-test.tsx +1 -1
  7. package/lighthouse-cli/bin.js +1 -4
  8. package/lighthouse-cli/run.js +1 -1
  9. package/lighthouse-cli/test/smokehouse/core-tests.js +4 -5
  10. package/lighthouse-cli/test/smokehouse/readme.md +4 -0
  11. package/lighthouse-cli/test/smokehouse/report-assert.js +36 -0
  12. package/lighthouse-core/audits/autocomplete.js +34 -37
  13. package/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
  14. package/lighthouse-core/audits/seo/hreflang.js +2 -17
  15. package/lighthouse-core/config/default-config.js +2 -2
  16. package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
  17. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +50 -31
  18. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -5
  19. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +4 -5
  20. package/lighthouse-core/fraggle-rock/user-flow.js +8 -6
  21. package/lighthouse-core/gather/driver/navigation.js +29 -10
  22. package/lighthouse-core/gather/driver/network-monitor.js +8 -6
  23. package/lighthouse-core/gather/driver/prepare.js +13 -7
  24. package/lighthouse-core/gather/driver/wait-for-condition.js +11 -4
  25. package/lighthouse-core/gather/gather-runner.js +1 -1
  26. package/lighthouse-core/gather/gatherers/full-page-screenshot.js +35 -9
  27. package/lighthouse-core/gather/gatherers/inputs.js +113 -0
  28. package/lighthouse-core/index.js +3 -3
  29. package/lighthouse-core/lib/page-functions.js +11 -1
  30. package/lighthouse-core/lib/sentry.js +39 -24
  31. package/lighthouse-core/runner.js +79 -52
  32. package/lighthouse-core/util-commonjs.js +4 -0
  33. package/package.json +2 -3
  34. package/readme.md +1 -1
  35. package/report/assets/templates.html +2 -5
  36. package/report/renderer/components.js +3 -3
  37. package/report/renderer/dom.js +1 -3
  38. package/report/renderer/report-ui-features.js +25 -17
  39. package/report/renderer/topbar-features.js +5 -1
  40. package/report/renderer/util.js +4 -0
  41. package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +107 -0
  42. package/report/test/renderer/report-renderer-axe-test.js +38 -17
  43. package/report/test-assets/faux-psi.js +2 -1
  44. package/report/types/report-renderer.d.ts +21 -2
  45. package/shared/localization/locales/en-US.json +7 -1
  46. package/shared/localization/locales/en-XL.json +7 -1
  47. package/third-party/axe/LICENSE +362 -0
  48. package/third-party/axe/README.md +6 -0
  49. package/third-party/axe/valid-langs.js +110 -0
  50. package/third-party/snyk/snapshot.json +1 -0
  51. package/tsconfig.json +1 -0
  52. package/types/artifacts.d.ts +14 -14
  53. package/types/global-lh.d.ts +1 -0
  54. package/types/parse-cache-control/index.d.ts +1 -0
  55. package/types/smokehouse.d.ts +3 -0
  56. package/changelog.md +0 -5803
  57. package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
  58. package/lighthouse-core/scripts/package.json +0 -4
@@ -28,14 +28,16 @@ const {version: lighthouseVersion} = require('../package.json');
28
28
  class Runner {
29
29
  /**
30
30
  * @template {LH.Config.Config | LH.Config.FRConfig} TConfig
31
- * @param {(runnerData: {config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
32
- * @param {{config: TConfig, computedCache: Map<string, ArbitraryEqualityMap>, driverMock?: Driver}} runOpts
31
+ * @param {LH.Artifacts} artifacts
32
+ * @param {{config: TConfig, driverMock?: Driver, computedCache: Map<string, ArbitraryEqualityMap>}} options
33
33
  * @return {Promise<LH.RunnerResult|undefined>}
34
34
  */
35
- static async run(gatherFn, runOpts) {
36
- const settings = runOpts.config.settings;
35
+ static async audit(artifacts, options) {
36
+ const {config, computedCache} = options;
37
+ const settings = config.settings;
38
+
37
39
  try {
38
- const runnerStatus = {msg: 'Runner setup', id: 'lh:runner:run'};
40
+ const runnerStatus = {msg: 'Audit phase', id: 'lh:runner:audit'};
39
41
  log.time(runnerStatus, 'verbose');
40
42
 
41
43
  /**
@@ -44,24 +46,14 @@ class Runner {
44
46
  */
45
47
  const lighthouseRunWarnings = [];
46
48
 
47
- const sentryContext = Sentry.getContext();
48
- Sentry.captureBreadcrumb({
49
- message: 'Run started',
50
- category: 'lifecycle',
51
- data: sentryContext?.extra,
52
- });
53
-
54
- const artifacts = await this.gatherAndManageArtifacts(gatherFn, runOpts);
55
-
56
49
  // Potentially quit early
57
50
  if (settings.gatherMode && !settings.auditMode) return;
58
51
 
59
- // Audit phase
60
- if (!runOpts.config.audits) {
52
+ if (!config.audits) {
61
53
  throw new Error('No audits to evaluate.');
62
54
  }
63
- const auditResultsById = await Runner._runAudits(settings, runOpts.config.audits, artifacts,
64
- lighthouseRunWarnings, runOpts.computedCache);
55
+ const auditResultsById = await Runner._runAudits(settings, config.audits, artifacts,
56
+ lighthouseRunWarnings, computedCache);
65
57
 
66
58
  // LHR construction phase
67
59
  const resultsStatus = {msg: 'Generating results...', id: 'lh:runner:generate'};
@@ -82,8 +74,8 @@ class Runner {
82
74
 
83
75
  /** @type {Record<string, LH.RawIcu<LH.Result.Category>>} */
84
76
  let categories = {};
85
- if (runOpts.config.categories) {
86
- categories = ReportScoring.scoreAllCategories(runOpts.config.categories, auditResultsById);
77
+ if (config.categories) {
78
+ categories = ReportScoring.scoreAllCategories(config.categories, auditResultsById);
87
79
  }
88
80
 
89
81
  log.timeEnd(resultsStatus);
@@ -108,7 +100,7 @@ class Runner {
108
100
  audits: auditResultsById,
109
101
  configSettings: settings,
110
102
  categories,
111
- categoryGroups: runOpts.config.groups || undefined,
103
+ categoryGroups: config.groups || undefined,
112
104
  stackPacks: stackPacks.getStackPacks(artifacts.Stacks),
113
105
  timing: this._getTiming(artifacts),
114
106
  i18n: {
@@ -134,12 +126,7 @@ class Runner {
134
126
 
135
127
  return {lhr, artifacts, report};
136
128
  } catch (err) {
137
- // i18n LighthouseError strings.
138
- if (err.friendlyMessage) {
139
- err.friendlyMessage = format.getFormatted(err.friendlyMessage, settings.locale);
140
- }
141
- await Sentry.captureException(err, {level: 'fatal'});
142
- throw err;
129
+ throw Runner.createRunnerError(err, settings);
143
130
  }
144
131
  }
145
132
 
@@ -150,38 +137,72 @@ class Runner {
150
137
  *
151
138
  * @template {LH.Config.Config | LH.Config.FRConfig} TConfig
152
139
  * @param {(runnerData: {config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
153
- * @param {{config: TConfig, driverMock?: Driver}} options
140
+ * @param {{config: TConfig, driverMock?: Driver, computedCache: Map<string, ArbitraryEqualityMap>}} options
154
141
  * @return {Promise<LH.Artifacts>}
155
142
  */
156
- static async gatherAndManageArtifacts(gatherFn, options) {
143
+ static async gather(gatherFn, options) {
157
144
  const settings = options.config.settings;
158
145
 
159
- // Gather phase
160
146
  // Either load saved artifacts from disk or from the browser.
161
- let artifacts;
162
- if (settings.auditMode && !settings.gatherMode) {
163
- // No browser required, just load the artifacts from disk.
164
- const path = this._getDataSavePath(settings);
165
- artifacts = assetSaver.loadArtifacts(path);
166
- const requestedUrl = artifacts.URL.requestedUrl;
167
-
168
- if (!requestedUrl) {
169
- throw new Error('Cannot run audit mode on empty URL');
170
- }
171
- } else {
172
- artifacts = await gatherFn({
173
- config: options.config,
174
- driverMock: options.driverMock,
147
+ try {
148
+ const sentryContext = Sentry.getContext();
149
+ Sentry.captureBreadcrumb({
150
+ message: 'Run started',
151
+ category: 'lifecycle',
152
+ data: sentryContext,
175
153
  });
176
154
 
177
- // -G means save these to disk (e.g. ./latest-run).
178
- if (settings.gatherMode) {
155
+ /** @type {LH.Artifacts} */
156
+ let artifacts;
157
+ if (settings.auditMode && !settings.gatherMode) {
158
+ // No browser required, just load the artifacts from disk.
179
159
  const path = this._getDataSavePath(settings);
180
- await assetSaver.saveArtifacts(artifacts, path);
160
+ artifacts = assetSaver.loadArtifacts(path);
161
+ const requestedUrl = artifacts.URL.requestedUrl;
162
+
163
+ if (!requestedUrl) {
164
+ throw new Error('Cannot run audit mode on empty URL');
165
+ }
166
+ } else {
167
+ const runnerStatus = {msg: 'Gather phase', id: 'lh:runner:gather'};
168
+ log.time(runnerStatus, 'verbose');
169
+
170
+ artifacts = await gatherFn({
171
+ config: options.config,
172
+ driverMock: options.driverMock,
173
+ });
174
+
175
+ log.timeEnd(runnerStatus);
176
+
177
+ // If `gather` is run multiple times before `audit`, the timing entries for each `gather` can pollute one another.
178
+ // We need to clear the timing entries at the end of gathering.
179
+ // Set artifacts.Timing again to ensure lh:runner:gather is included.
180
+ artifacts.Timing = log.takeTimeEntries();
181
+
182
+ // -G means save these to disk (e.g. ./latest-run).
183
+ if (settings.gatherMode) {
184
+ const path = this._getDataSavePath(settings);
185
+ await assetSaver.saveArtifacts(artifacts, path);
186
+ }
181
187
  }
188
+
189
+ return artifacts;
190
+ } catch (err) {
191
+ throw Runner.createRunnerError(err, settings);
182
192
  }
193
+ }
183
194
 
184
- return artifacts;
195
+ /**
196
+ * @param {any} err
197
+ * @param {LH.Config.Settings} settings
198
+ */
199
+ static createRunnerError(err, settings) {
200
+ // i18n LighthouseError strings.
201
+ if (err.friendlyMessage) {
202
+ err.friendlyMessage = format.getFormatted(err.friendlyMessage, settings.locale);
203
+ }
204
+ Sentry.captureException(err, {level: 'fatal'});
205
+ return err;
185
206
  }
186
207
 
187
208
  /**
@@ -197,8 +218,11 @@ class Runner {
197
218
  const timingEntriesKeyValues = [
198
219
  ...timingEntriesFromArtifacts,
199
220
  ...timingEntriesFromRunner,
200
- // As entries can share a name, dedupe based on the startTime timestamp
201
- ].map(entry => /** @type {[number, PerformanceEntry]} */ ([entry.startTime, entry]));
221
+ ].map(entry => /** @type {[string, PerformanceEntry]} */ ([
222
+ // As entries can share a name and start time, dedupe based on the name, startTime and duration
223
+ `${entry.startTime}-${entry.name}-${entry.duration}`,
224
+ entry,
225
+ ]));
202
226
  const timingEntries = Array.from(new Map(timingEntriesKeyValues).values())
203
227
  // Truncate timestamps to hundredths of a millisecond saves ~4KB. No need for microsecond
204
228
  // resolution.
@@ -212,8 +236,11 @@ class Runner {
212
236
  entryType: entry.entryType,
213
237
  };
214
238
  }).sort((a, b) => a.startTime - b.startTime);
215
- const runnerEntry = timingEntries.find(e => e.name === 'lh:runner:run');
216
- return {entries: timingEntries, total: runnerEntry?.duration || 0};
239
+ const gatherEntry = timingEntries.find(e => e.name === 'lh:runner:gather');
240
+ const auditEntry = timingEntries.find(e => e.name === 'lh:runner:audit');
241
+ const gatherTiming = gatherEntry?.duration || 0;
242
+ const auditTiming = auditEntry?.duration || 0;
243
+ return {entries: timingEntries, total: gatherTiming + auditTiming};
217
244
  }
218
245
 
219
246
  /**
@@ -630,6 +630,10 @@ const UIStrings = {
630
630
  thirdPartyResourcesLabel: 'Show 3rd-party resources',
631
631
  /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */
632
632
  viewTreemapLabel: 'View Treemap',
633
+ /** This label is for a button that will show the user a trace of the page. */
634
+ viewTraceLabel: 'View Trace',
635
+ /** This label is for a button that will show the user a trace of the page. */
636
+ viewOriginalTraceLabel: 'View Original Trace',
633
637
 
634
638
  /** Option in a dropdown menu that opens a small, summary report in a print dialog. */
635
639
  dropdownPrintSummary: 'Print Summary',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.3.1",
3
+ "version": "9.4.0",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -121,7 +121,6 @@
121
121
  "@types/lodash.set": "^4.3.6",
122
122
  "@types/node": "*",
123
123
  "@types/pako": "^1.0.1",
124
- "@types/raven": "^2.5.1",
125
124
  "@types/resize-observer-browser": "^0.1.1",
126
125
  "@types/semver": "^5.5.0",
127
126
  "@types/tabulator-tables": "^4.9.1",
@@ -183,6 +182,7 @@
183
182
  "webtreemap-cdt": "^3.2.1"
184
183
  },
185
184
  "dependencies": {
185
+ "@sentry/node": "^6.17.4",
186
186
  "axe-core": "4.3.5",
187
187
  "chrome-launcher": "^0.15.0",
188
188
  "configstore": "^5.0.1",
@@ -204,7 +204,6 @@
204
204
  "open": "^8.4.0",
205
205
  "parse-cache-control": "1.0.1",
206
206
  "ps-list": "^8.0.0",
207
- "raven": "^2.2.1",
208
207
  "robots-parser": "^3.0.0",
209
208
  "semver": "^5.3.0",
210
209
  "speedline-core": "^1.4.3",
package/readme.md CHANGED
@@ -313,7 +313,7 @@ yarn type-check
313
313
 
314
314
  ### Docs
315
315
 
316
- Some of our docs have tests that run only in CI by default. If you end up needing to modify our documentation, you'll need to run `yarn test-docs` locally to make sure they pass.
316
+ Some of our docs have tests that run only in CI by default. If you end up needing to modify our documentation, you'll need to run `yarn build-pack && yarn test-docs` locally to make sure they pass.
317
317
 
318
318
  **Additional Dependencies**
319
319
  - `brew install jq`
@@ -210,8 +210,8 @@ limitations under the License.
210
210
  <div class="lh-scores-wrapper">
211
211
  <div class="lh-scores-container">
212
212
  <div class="lh-pyro">
213
- <div class="lh-before"></div>
214
- <div class="lh-after"></div>
213
+ <div class="lh-pyro-before"></div>
214
+ <div class="lh-pyro-after"></div>
215
215
  </div>
216
216
  </div>
217
217
  </div>
@@ -463,9 +463,6 @@ limitations under the License.
463
463
  animation-delay: 2.25s, 2.25s, 2.25s;
464
464
  animation-duration: 1.25s, 1.25s, 6.25s;
465
465
  }
466
- .lh-fireworks-paused .lh-pyro > div {
467
- animation-play-state: paused;
468
- }
469
466
 
470
467
  @keyframes bang {
471
468
  to {
@@ -349,7 +349,7 @@ function createGaugePwaComponent(dom) {
349
349
  function createHeadingComponent(dom) {
350
350
  const el0 = dom.createFragment();
351
351
  const el1 = dom.createElement('style');
352
- el1.append('\n /* CSS Fireworks. Originally by Eddie Lin\n https://codepen.io/paulirish/pen/yEVMbP\n */\n .lh-pyro {\n display: none;\n z-index: 1;\n pointer-events: none;\n }\n .lh-score100 .lh-pyro {\n display: block;\n }\n .lh-score100 .lh-lighthouse stop:first-child {\n stop-color: hsla(200, 12%, 95%, 0);\n }\n .lh-score100 .lh-lighthouse stop:last-child {\n stop-color: hsla(65, 81%, 76%, 1);\n }\n\n .lh-pyro > .lh-pyro-before, .lh-pyro > .lh-pyro-after {\n position: absolute;\n width: 5px;\n height: 5px;\n border-radius: 2.5px;\n box-shadow: 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff;\n animation: 1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards;\n animation-delay: 1s, 1s, 1s;\n }\n\n .lh-pyro > .lh-pyro-after {\n animation-delay: 2.25s, 2.25s, 2.25s;\n animation-duration: 1.25s, 1.25s, 6.25s;\n }\n .lh-fireworks-paused .lh-pyro > div {\n animation-play-state: paused;\n }\n\n @keyframes bang {\n to {\n box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4, 58px -31.67px #7eeb47, 13px -141.67px #eb47c5, -19px 6.33px #7347eb, -2px -74.67px #ebd247, 24px -151.67px #eb47e0, 57px -138.67px #b4eb47, -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb, -16px -118.67px #47bfeb, 53px -84.67px #47eb83, 66px -57.67px #eb47bf, -93px -65.67px #91eb47, 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb, 61px -58.67px #47eb73, 5px -22.67px #47e8eb, -66px -28.67px #ebe247, 42px -123.67px #eb5547, -75px 26.33px #7beb47, 15px -52.67px #a147eb, 36px -51.67px #eb8347, -38px -12.67px #eb5547, -46px -59.67px #47eb81, 78px -114.67px #eb47ba, 15px -156.67px #eb47bf, -36px 1.33px #eb4783, -72px -86.67px #eba147, 31px -46.67px #ebe247, -68px 29.33px #47e2eb, -55px 19.33px #ebe047, -56px 27.33px #4776eb, -13px -91.67px #eb5547, -47px -138.67px #47ebc7, -18px -96.67px #eb47ac, 11px -88.67px #4783eb, -67px -28.67px #47baeb, 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791, -68px -4.67px #47eba7, 95px -37.67px #eb478b, -67px -162.67px #eb5d47, -54px -120.67px #eb6847, 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147, 6px -71.67px #ebbc47;\n }\n }\n @keyframes gravity {\n to {\n transform: translateY(80px);\n opacity: 0;\n }\n }\n @keyframes position {\n 0%, 19.9% {\n margin-top: 4%;\n margin-left: 47%;\n }\n 20%, 39.9% {\n margin-top: 7%;\n margin-left: 30%;\n }\n 40%, 59.9% {\n margin-top: 6%;\n margin-left: 70%;\n }\n 60%, 79.9% {\n margin-top: 3%;\n margin-left: 20%;\n }\n 80%, 99.9% {\n margin-top: 3%;\n margin-left: 80%;\n }\n }\n ');
352
+ el1.append('\n /* CSS Fireworks. Originally by Eddie Lin\n https://codepen.io/paulirish/pen/yEVMbP\n */\n .lh-pyro {\n display: none;\n z-index: 1;\n pointer-events: none;\n }\n .lh-score100 .lh-pyro {\n display: block;\n }\n .lh-score100 .lh-lighthouse stop:first-child {\n stop-color: hsla(200, 12%, 95%, 0);\n }\n .lh-score100 .lh-lighthouse stop:last-child {\n stop-color: hsla(65, 81%, 76%, 1);\n }\n\n .lh-pyro > .lh-pyro-before, .lh-pyro > .lh-pyro-after {\n position: absolute;\n width: 5px;\n height: 5px;\n border-radius: 2.5px;\n box-shadow: 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff;\n animation: 1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards;\n animation-delay: 1s, 1s, 1s;\n }\n\n .lh-pyro > .lh-pyro-after {\n animation-delay: 2.25s, 2.25s, 2.25s;\n animation-duration: 1.25s, 1.25s, 6.25s;\n }\n\n @keyframes bang {\n to {\n box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4, 58px -31.67px #7eeb47, 13px -141.67px #eb47c5, -19px 6.33px #7347eb, -2px -74.67px #ebd247, 24px -151.67px #eb47e0, 57px -138.67px #b4eb47, -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb, -16px -118.67px #47bfeb, 53px -84.67px #47eb83, 66px -57.67px #eb47bf, -93px -65.67px #91eb47, 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb, 61px -58.67px #47eb73, 5px -22.67px #47e8eb, -66px -28.67px #ebe247, 42px -123.67px #eb5547, -75px 26.33px #7beb47, 15px -52.67px #a147eb, 36px -51.67px #eb8347, -38px -12.67px #eb5547, -46px -59.67px #47eb81, 78px -114.67px #eb47ba, 15px -156.67px #eb47bf, -36px 1.33px #eb4783, -72px -86.67px #eba147, 31px -46.67px #ebe247, -68px 29.33px #47e2eb, -55px 19.33px #ebe047, -56px 27.33px #4776eb, -13px -91.67px #eb5547, -47px -138.67px #47ebc7, -18px -96.67px #eb47ac, 11px -88.67px #4783eb, -67px -28.67px #47baeb, 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791, -68px -4.67px #47eba7, 95px -37.67px #eb478b, -67px -162.67px #eb5d47, -54px -120.67px #eb6847, 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147, 6px -71.67px #ebbc47;\n }\n }\n @keyframes gravity {\n to {\n transform: translateY(80px);\n opacity: 0;\n }\n }\n @keyframes position {\n 0%, 19.9% {\n margin-top: 4%;\n margin-left: 47%;\n }\n 20%, 39.9% {\n margin-top: 7%;\n margin-left: 30%;\n }\n 40%, 59.9% {\n margin-top: 6%;\n margin-left: 70%;\n }\n 60%, 79.9% {\n margin-top: 3%;\n margin-left: 20%;\n }\n 80%, 99.9% {\n margin-top: 3%;\n margin-left: 80%;\n }\n }\n ');
353
353
  el0.append(el1);
354
354
  const el2 = dom.createElement('div', 'lh-header-container');
355
355
  const el3 = dom.createElement('div', 'lh-scores-wrapper-placeholder');
@@ -450,8 +450,8 @@ function createScoresWrapperComponent(dom) {
450
450
  const el2 = dom.createElement('div', 'lh-scores-wrapper');
451
451
  const el3 = dom.createElement('div', 'lh-scores-container');
452
452
  const el4 = dom.createElement('div', 'lh-pyro');
453
- const el5 = dom.createElement('div', 'lh-before');
454
- const el6 = dom.createElement('div', 'lh-after');
453
+ const el5 = dom.createElement('div', 'lh-pyro-before');
454
+ const el6 = dom.createElement('div', 'lh-pyro-after');
455
455
  el4.append(' ', el5, ' ', el6, ' ');
456
456
  el3.append(' ', el4, ' ');
457
457
  el2.append(' ', el3, ' ');
@@ -293,10 +293,8 @@ export class DOM {
293
293
  * @param {string} filename
294
294
  */
295
295
  saveFile(blob, filename) {
296
- const ext = blob.type.match('json') ? '.json' : '.html';
297
-
298
296
  const a = this.createElement('a');
299
- a.download = `${filename}${ext}`;
297
+ a.download = filename;
300
298
  this.safelySetBlobHref(a, blob);
301
299
  this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
302
300
  a.click();
@@ -70,12 +70,13 @@ export class ReportUIFeatures {
70
70
  this._setupThirdPartyFilter();
71
71
  this._setupElementScreenshotOverlay(this._dom.rootEl);
72
72
 
73
- let turnOffTheLights = false;
74
73
  // Do not query the system preferences for DevTools - DevTools should only apply dark theme
75
74
  // if dark is selected in the settings panel.
76
- const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks;
75
+ // TODO: set `disableDarkMode` in devtools and delete this special case.
76
+ const disableDarkMode = this._dom.isDevTools() ||
77
+ this._opts.disableDarkMode || this._opts.disableAutoDarkModeAndFireworks;
77
78
  if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) {
78
- turnOffTheLights = true;
79
+ toggleDarkTheme(this._dom, true);
79
80
  }
80
81
 
81
82
  // Fireworks!
@@ -85,13 +86,12 @@ export class ReportUIFeatures {
85
86
  const cat = lhr.categories[id];
86
87
  return cat && cat.score === 1;
87
88
  });
88
- if (scoresAll100 && !this._opts.disableAutoDarkModeAndFireworks) {
89
- turnOffTheLights = true;
89
+ const disableFireworks =
90
+ this._opts.disableFireworks || this._opts.disableAutoDarkModeAndFireworks;
91
+ if (scoresAll100 && !disableFireworks) {
90
92
  this._enableFireworks();
91
- }
92
-
93
- if (turnOffTheLights) {
94
- toggleDarkTheme(this._dom, true);
93
+ // If dark mode is allowed, force it on because it looks so much better.
94
+ if (!disableDarkMode) toggleDarkTheme(this._dom, true);
95
95
  }
96
96
 
97
97
  // Show the metric descriptions by default when there is an error.
@@ -112,6 +112,15 @@ export class ReportUIFeatures {
112
112
  });
113
113
  }
114
114
 
115
+ if (this._opts.onViewTrace) {
116
+ this.addButton({
117
+ text: lhr.configSettings.throttlingMethod === 'simulate' ?
118
+ Util.i18n.strings.viewOriginalTraceLabel :
119
+ Util.i18n.strings.viewTraceLabel,
120
+ onClick: () => this._opts.onViewTrace?.(),
121
+ });
122
+ }
123
+
115
124
  if (this._opts.getStandaloneReportHTML) {
116
125
  this._dom.find('a[data-action="save-html"]', this._dom.rootEl).classList.remove('lh-hidden');
117
126
  }
@@ -180,9 +189,6 @@ export class ReportUIFeatures {
180
189
  _enableFireworks() {
181
190
  const scoresContainer = this._dom.find('.lh-scores-container', this._dom.rootEl);
182
191
  scoresContainer.classList.add('lh-score100');
183
- scoresContainer.addEventListener('click', _ => {
184
- scoresContainer.classList.toggle('lh-fireworks-paused');
185
- });
186
192
  }
187
193
 
188
194
  _setupMediaQueryListeners() {
@@ -339,13 +345,15 @@ export class ReportUIFeatures {
339
345
  }
340
346
 
341
347
  /**
342
- * DevTools uses its own file manager to download files, so it redefines this function.
343
- * Wrapper is necessary so DevTools can still override this function.
344
- *
345
348
  * @param {Blob|File} blob
346
349
  */
347
350
  _saveFile(blob) {
348
- const filename = getLhrFilenamePrefix(this.json);
349
- this._dom.saveFile(blob, filename);
351
+ const ext = blob.type.match('json') ? '.json' : '.html';
352
+ const filename = getLhrFilenamePrefix(this.json) + ext;
353
+ if (this._opts.onSaveFileOverride) {
354
+ this._opts.onSaveFileOverride(blob, filename);
355
+ } else {
356
+ this._dom.saveFile(blob, filename);
357
+ }
350
358
  }
351
359
  }
@@ -199,7 +199,11 @@ export class TopbarFeatures {
199
199
  }
200
200
 
201
201
  _print() {
202
- self.print();
202
+ if (this._reportUIFeatures._opts.onPrintOverride) {
203
+ this._reportUIFeatures._opts.onPrintOverride(this._dom.rootEl);
204
+ } else {
205
+ self.print();
206
+ }
203
207
  }
204
208
 
205
209
  /**
@@ -627,6 +627,10 @@ const UIStrings = {
627
627
  thirdPartyResourcesLabel: 'Show 3rd-party resources',
628
628
  /** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */
629
629
  viewTreemapLabel: 'View Treemap',
630
+ /** This label is for a button that will show the user a trace of the page. */
631
+ viewTraceLabel: 'View Trace',
632
+ /** This label is for a button that will show the user a trace of the page. */
633
+ viewOriginalTraceLabel: 'View Original Trace',
630
634
 
631
635
  /** Option in a dropdown menu that opens a small, summary report in a print dialog. */
632
636
  dropdownPrintSummary: 'Print Summary',
@@ -0,0 +1,107 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ReportRendererAxe with aXe renders without axe violations 2`] = `
4
+ Array [
5
+ Object {
6
+ "description": "Ensures every id attribute value is unique",
7
+ "help": "id attribute value must be unique",
8
+ "helpUrl": "https://dequeuniversity.com/rules/axe/4.3/duplicate-id?application=axeAPI",
9
+ "id": "duplicate-id",
10
+ "impact": "minor",
11
+ "nodes": Array [
12
+ Object {
13
+ "all": Array [],
14
+ "any": Array [
15
+ Object {
16
+ "data": "viewport",
17
+ "id": "duplicate-id",
18
+ "impact": "minor",
19
+ "message": "Document has multiple static elements with the same id attribute: viewport",
20
+ "relatedNodes": Array [
21
+ Object {
22
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"viewport\\">",
23
+ "target": Array [
24
+ "#seo > .lh-audit-group:nth-child(4) > .lh-clump--passed.lh-clump > .lh-audit--binary.lh-audit--pass.lh-audit:nth-child(2)",
25
+ ],
26
+ },
27
+ Object {
28
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"viewport\\">",
29
+ "target": Array [
30
+ ".lh-audit-group--pwa-optimized > .lh-audit--binary.lh-audit--pass.lh-audit:nth-child(6)",
31
+ ],
32
+ },
33
+ ],
34
+ },
35
+ ],
36
+ "failureSummary": "Fix any of the following:
37
+ Document has multiple static elements with the same id attribute: viewport",
38
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"viewport\\">",
39
+ "impact": "minor",
40
+ "none": Array [],
41
+ "target": Array [
42
+ ".lh-audit--binary.lh-audit--pass.lh-audit:nth-child(20)",
43
+ ],
44
+ },
45
+ Object {
46
+ "all": Array [],
47
+ "any": Array [
48
+ Object {
49
+ "data": "image-alt",
50
+ "id": "duplicate-id",
51
+ "impact": "minor",
52
+ "message": "Document has multiple static elements with the same id attribute: image-alt",
53
+ "relatedNodes": Array [
54
+ Object {
55
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--fail\\" id=\\"image-alt\\">",
56
+ "target": Array [
57
+ ".lh-audit-group--seo-content > .lh-audit--fail.lh-audit--binary.lh-audit:nth-child(3)",
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ "failureSummary": "Fix any of the following:
64
+ Document has multiple static elements with the same id attribute: image-alt",
65
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--fail\\" id=\\"image-alt\\">",
66
+ "impact": "minor",
67
+ "none": Array [],
68
+ "target": Array [
69
+ ".lh-audit-group--a11y-names-labels > .lh-audit--fail.lh-audit--binary.lh-audit:nth-child(2)",
70
+ ],
71
+ },
72
+ Object {
73
+ "all": Array [],
74
+ "any": Array [
75
+ Object {
76
+ "data": "document-title",
77
+ "id": "duplicate-id",
78
+ "impact": "minor",
79
+ "message": "Document has multiple static elements with the same id attribute: document-title",
80
+ "relatedNodes": Array [
81
+ Object {
82
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"document-title\\">",
83
+ "target": Array [
84
+ "#seo > .lh-audit-group:nth-child(4) > .lh-clump--passed.lh-clump > .lh-audit--binary.lh-audit--pass.lh-audit:nth-child(3)",
85
+ ],
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ "failureSummary": "Fix any of the following:
91
+ Document has multiple static elements with the same id attribute: document-title",
92
+ "html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"document-title\\">",
93
+ "impact": "minor",
94
+ "none": Array [],
95
+ "target": Array [
96
+ ".lh-audit--binary.lh-audit--pass.lh-audit:nth-child(13)",
97
+ ],
98
+ },
99
+ ],
100
+ "tags": Array [
101
+ "cat.parsing",
102
+ "wcag2a",
103
+ "wcag411",
104
+ ],
105
+ },
106
+ ]
107
+ `;
@@ -53,23 +53,44 @@ describe('ReportRendererAxe', () => {
53
53
  // eslint-disable-next-line no-undef
54
54
  const axeResults = await page.evaluate(config => axe.run(config), config);
55
55
 
56
- expect(axeResults.violations).toMatchObject([
57
- // Color contrast failure only pops up if this pptr is run headfully.
58
- // There are currently 27 problematic nodes, primarily audit display text and explanations.
59
- // TODO: fix these failures, regardless.
60
- // {
61
- // id: 'color-contrast',
62
- // },
63
- {
64
- id: 'duplicate-id',
65
- nodes: [
66
- // We use these audits in multiple categories. Makes sense.
67
- {html: '<div class="lh-audit lh-audit--binary lh-audit--pass" id="viewport">'},
68
- {html: '<div class="lh-audit lh-audit--binary lh-audit--fail" id="image-alt">'},
69
- {html: '<div class="lh-audit lh-audit--binary lh-audit--pass" id="document-title">'},
70
- ],
71
- },
72
- ]);
56
+ // Color contrast failure only pops up if this pptr is run headfully.
57
+ // There are currently 27 problematic nodes, primarily audit display text and explanations.
58
+ // TODO: fix these failures, regardless.
59
+ // {
60
+ // id: 'color-contrast',
61
+ // },
62
+
63
+ expect(axeResults.violations.find(v => v.id === 'duplicate-id')).toMatchObject({
64
+ id: 'duplicate-id',
65
+ nodes: [
66
+ // We use these audits in multiple categories. Makes sense.
67
+ {html: '<div class="lh-audit lh-audit--binary lh-audit--pass" id="viewport">'},
68
+ {html: '<div class="lh-audit lh-audit--binary lh-audit--fail" id="image-alt">'},
69
+ {html: '<div class="lh-audit lh-audit--binary lh-audit--pass" id="document-title">'},
70
+ ],
71
+ });
72
+
73
+ const axeSummary = axeResults.violations.map((v) => {
74
+ return {
75
+ id: v.id,
76
+ message: v.nodes.map((n) => n.failureSummary).join('\n'),
77
+ };
78
+ });
79
+ expect(axeSummary).toMatchInlineSnapshot(`
80
+ Array [
81
+ Object {
82
+ "id": "duplicate-id",
83
+ "message": "Fix any of the following:
84
+ Document has multiple static elements with the same id attribute: viewport
85
+ Fix any of the following:
86
+ Document has multiple static elements with the same id attribute: image-alt
87
+ Fix any of the following:
88
+ Document has multiple static elements with the same id attribute: document-title",
89
+ },
90
+ ]
91
+ `);
92
+
93
+ expect(axeResults.violations).toMatchSnapshot();
73
94
  },
74
95
  // This test takes 10s on fast hardware, but can take longer in CI.
75
96
  // https://github.com/dequelabs/axe-core/tree/b573b1c1/doc/examples/jest_react#timeout-issues
@@ -46,7 +46,8 @@ async function renderLHReport() {
46
46
 
47
47
  const reportRootEl = lighthouseRenderer.renderReport(lhr, {
48
48
  omitTopbar: true,
49
- disableAutoDarkModeAndFireworks: true,
49
+ disableFireworks: true,
50
+ disableDarkMode: true,
50
51
  });
51
52
  // TODO: display warnings if appropriate.
52
53
  for (const el of reportRootEl.querySelectorAll('.lh-warnings--toplevel')) {