lighthouse 9.5.0-dev.20221003 → 9.5.0-dev.20221004

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 (50) hide show
  1. package/cli/test/smokehouse/core-tests.js +2 -0
  2. package/cli/test/smokehouse/readme.md +1 -1
  3. package/cli/test/smokehouse/report-assert-test.js +4 -4
  4. package/cli/test/smokehouse/report-assert.js +1 -1
  5. package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
  6. package/core/audits/byte-efficiency/modern-image-formats.js +1 -1
  7. package/core/audits/byte-efficiency/uses-optimized-images.js +1 -1
  8. package/core/audits/font-display.js +1 -1
  9. package/core/audits/redirects.js +3 -3
  10. package/core/audits/seo/crawlable-anchors.js +1 -1
  11. package/core/audits/seo/font-size.js +1 -1
  12. package/core/audits/seo/link-text.js +2 -2
  13. package/core/audits/service-worker.js +1 -1
  14. package/core/audits/third-party-summary.js +1 -1
  15. package/core/audits/valid-source-maps.js +1 -1
  16. package/core/computed/page-dependency-graph.js +1 -1
  17. package/core/computed/resource-summary.js +1 -1
  18. package/core/gather/base-artifacts.js +4 -6
  19. package/core/gather/gatherers/full-page-screenshot.js +18 -5
  20. package/core/gather/gatherers/link-elements.js +4 -4
  21. package/core/gather/gatherers/seo/robots-txt.js +2 -2
  22. package/core/gather/gatherers/web-app-manifest.js +2 -2
  23. package/core/gather/navigation-runner.js +2 -4
  24. package/core/gather/snapshot-runner.js +1 -2
  25. package/core/gather/timespan-runner.js +2 -6
  26. package/core/legacy/gather/gather-runner.js +3 -4
  27. package/core/runner.js +3 -1
  28. package/core/user-flow.js +2 -2
  29. package/core/util.cjs +25 -0
  30. package/dist/report/bundle.esm.js +48 -13
  31. package/dist/report/flow.js +7 -7
  32. package/dist/report/standalone.js +8 -8
  33. package/flow-report/src/summary/category.tsx +3 -3
  34. package/flow-report/src/summary/summary.tsx +2 -2
  35. package/package.json +1 -1
  36. package/report/generator/file-namer.js +2 -2
  37. package/report/generator/report-generator.js +1 -1
  38. package/report/renderer/element-screenshot-renderer.js +6 -1
  39. package/report/renderer/open-tab.js +3 -1
  40. package/report/renderer/report-renderer.js +3 -3
  41. package/report/renderer/report-ui-features.js +9 -6
  42. package/report/renderer/util.js +25 -0
  43. package/report/test/generator/file-namer-test.js +1 -1
  44. package/report/test/generator/report-generator-test.js +1 -1
  45. package/report/test/renderer/report-renderer-test.js +4 -1
  46. package/report/test/renderer/report-ui-features-test.js +1 -1
  47. package/types/artifacts.d.ts +2 -8
  48. package/types/lhr/lhr.d.ts +9 -2
  49. package/types/lhr/treemap.d.ts +3 -1
  50. package/types/smokehouse.d.ts +1 -1
@@ -62,6 +62,7 @@ import seoStatus403 from './test-definitions/seo-status-403.js';
62
62
  import seoTapTargets from './test-definitions/seo-tap-targets.js';
63
63
  import sourceMaps from './test-definitions/source-maps.js';
64
64
  import timing from './test-definitions/timing.js';
65
+ import fpsScaled from './test-definitions/fps-scaled.js';
65
66
 
66
67
  /** @type {ReadonlyArray<Smokehouse.TestDfn>} */
67
68
  const smokeTests = [
@@ -123,6 +124,7 @@ const smokeTests = [
123
124
  seoTapTargets,
124
125
  sourceMaps,
125
126
  timing,
127
+ fpsScaled,
126
128
  ];
127
129
 
128
130
  export default smokeTests;
@@ -188,7 +188,7 @@ Smokehouse comes with a core set of test definitions, but it can run any set of
188
188
  const expectations = [{
189
189
  lhr: {
190
190
  requestedUrl: 'http://localhost:8080/index.html',
191
- finalUrl: 'http://localhost:8080/index.html',
191
+ finalDisplayedUrl: 'http://localhost:8080/index.html',
192
192
  audits: {
193
193
  'preload-as': {
194
194
  score: 1,
@@ -235,7 +235,7 @@ describe('getAssertionReport', () => {
235
235
  lhr: {
236
236
  audits: {},
237
237
  requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
238
- finalUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
238
+ finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
239
239
  },
240
240
  });
241
241
  expect(report).toMatchObject({passed: 3, failed: 0, log: ''});
@@ -252,7 +252,7 @@ describe('getAssertionReport', () => {
252
252
  },
253
253
  },
254
254
  requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
255
- finalUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
255
+ finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
256
256
  },
257
257
  });
258
258
  expect(report).toMatchObject({passed: 3, failed: 1});
@@ -270,7 +270,7 @@ describe('getAssertionReport', () => {
270
270
  },
271
271
  },
272
272
  requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
273
- finalUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
273
+ finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
274
274
  },
275
275
  });
276
276
  expect(report).toMatchObject({passed: 3, failed: 1});
@@ -289,7 +289,7 @@ describe('getAssertionReport', () => {
289
289
  },
290
290
  },
291
291
  requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
292
- finalUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
292
+ finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
293
293
  },
294
294
  });
295
295
  expect(report).toMatchObject({passed: 3, failed: 1});
@@ -408,7 +408,7 @@ function collateResults(localConsole, actual, expected) {
408
408
  }
409
409
 
410
410
  return [
411
- makeComparison('final url', actual.lhr.finalUrl, expected.lhr.finalUrl),
411
+ makeComparison('final url', actual.lhr.finalDisplayedUrl, expected.lhr.finalDisplayedUrl),
412
412
  runtimeErrorAssertion,
413
413
  runWarningsAssertion,
414
414
  ...requestCountAssertion,
@@ -401,7 +401,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
401
401
  * @return {Promise<ByteEfficiencyProduct>}
402
402
  */
403
403
  static async audit_(artifacts, networkRecords, context) {
404
- const mainDocumentEntity = thirdPartyWeb.getEntity(artifacts.URL.finalUrl);
404
+ const mainDocumentEntity = thirdPartyWeb.getEntity(artifacts.URL.finalDisplayedUrl);
405
405
  const bundles = await JSBundles.request(artifacts, context);
406
406
 
407
407
  /** @type {Item[]} */
@@ -92,7 +92,7 @@ class ModernImageFormats extends ByteEfficiencyAudit {
92
92
  * @return {import('./byte-efficiency-audit.js').ByteEfficiencyProduct}
93
93
  */
94
94
  static audit_(artifacts) {
95
- const pageURL = artifacts.URL.finalUrl;
95
+ const pageURL = artifacts.URL.finalDisplayedUrl;
96
96
  const images = artifacts.OptimizedImages;
97
97
  const imageElements = artifacts.ImageElements;
98
98
  /** @type {Map<string, LH.Artifacts.ImageElement>} */
@@ -69,7 +69,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
69
69
  * @return {import('./byte-efficiency-audit.js').ByteEfficiencyProduct}
70
70
  */
71
71
  static audit_(artifacts) {
72
- const pageURL = artifacts.URL.finalUrl;
72
+ const pageURL = artifacts.URL.finalDisplayedUrl;
73
73
  const images = artifacts.OptimizedImages;
74
74
  const imageElements = artifacts.ImageElements;
75
75
  /** @type {Map<string, LH.Artifacts.ImageElement>} */
@@ -100,7 +100,7 @@ class FontDisplay extends Audit {
100
100
  for (const relativeURL of relativeURLs) {
101
101
  try {
102
102
  const relativeRoot = UrlUtils.isValid(stylesheet.header.sourceURL) ?
103
- stylesheet.header.sourceURL : artifacts.URL.finalUrl;
103
+ stylesheet.header.sourceURL : artifacts.URL.finalDisplayedUrl;
104
104
  const absoluteURL = new URL(relativeURL, relativeRoot);
105
105
  targetURLSet.add(absoluteURL.href);
106
106
  } catch (err) {
@@ -42,10 +42,10 @@ class Redirects extends Audit {
42
42
  * Example:
43
43
  * GET /initialUrl => 302 /firstRedirect
44
44
  * GET /firstRedirect => 200 /firstRedirect, window.location = '/secondRedirect'
45
- * GET /secondRedirect => 302 /finalUrl
46
- * GET /finalUrl => 200 /finalUrl
45
+ * GET /secondRedirect => 302 /thirdRedirect
46
+ * GET /thirdRedirect => 200 /mainDocumentUrl
47
47
  *
48
- * Returns network records [/initialUrl, /firstRedirect, /secondRedirect, /thirdRedirect, /finalUrl]
48
+ * Returns network records [/initialUrl, /firstRedirect, /secondRedirect, /thirdRedirect, /mainDocumentUrl]
49
49
  *
50
50
  * @param {LH.Artifacts.NetworkRequest} mainResource
51
51
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@@ -62,7 +62,7 @@ class CrawlableAnchors extends Audit {
62
62
 
63
63
  // checking if rawHref is a valid
64
64
  try {
65
- new URL(rawHref, url.finalUrl);
65
+ new URL(rawHref, url.finalDisplayedUrl);
66
66
  } catch (e) {
67
67
  return true;
68
68
  }
@@ -280,7 +280,7 @@ class FontSize extends Audit {
280
280
  const failingRules = getUniqueFailingRules(analyzedFailingNodesData);
281
281
  const percentageOfPassingText =
282
282
  (totalTextLength - failingTextLength) / totalTextLength * 100;
283
- const pageUrl = artifacts.URL.finalUrl;
283
+ const pageUrl = artifacts.URL.finalDisplayedUrl;
284
284
 
285
285
  /** @type {LH.Audit.Details.Table['headings']} */
286
286
  const headings = [
@@ -120,8 +120,8 @@ class LinkText extends Audit {
120
120
  href.startsWith('javascript:') ||
121
121
  href.startsWith('mailto:') ||
122
122
  // This line prevents the audit from flagging anchor links.
123
- // In this case it is better to use `finalUrl` than `mainDocumentUrl`.
124
- UrlUtils.equalWithExcludedFragments(link.href, artifacts.URL.finalUrl)
123
+ // In this case it is better to use `finalDisplayedUrl` than `mainDocumentUrl`.
124
+ UrlUtils.equalWithExcludedFragments(link.href, artifacts.URL.finalDisplayedUrl)
125
125
  ) {
126
126
  return false;
127
127
  }
@@ -170,7 +170,7 @@ class ServiceWorker extends Audit {
170
170
  };
171
171
  }
172
172
 
173
- // SW controls both finalUrl and start_url.
173
+ // SW controls both finalDisplayedUrl and start_url.
174
174
  return {
175
175
  score: 1,
176
176
  details,
@@ -197,7 +197,7 @@ class ThirdPartySummary extends Audit {
197
197
  const trace = artifacts.traces[Audit.DEFAULT_PASS];
198
198
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
199
199
  const networkRecords = await NetworkRecords.request(devtoolsLog, context);
200
- const mainEntity = thirdPartyWeb.getEntity(artifacts.URL.finalUrl);
200
+ const mainEntity = thirdPartyWeb.getEntity(artifacts.URL.finalDisplayedUrl);
201
201
  const tasks = await MainThreadTasks.request(trace, context);
202
202
  const multiplier = settings.throttlingMethod === 'simulate' ?
203
203
  settings.throttling.cpuSlowdownMultiplier : 1;
@@ -77,7 +77,7 @@ class ValidSourceMaps extends Audit {
77
77
  for (const script of artifacts.Scripts) {
78
78
  const sourceMap = SourceMaps.find(m => m.scriptId === script.scriptId);
79
79
  const errors = [];
80
- const isLargeFirstParty = this.isLargeFirstPartyJS(script, artifacts.URL.finalUrl);
80
+ const isLargeFirstParty = this.isLargeFirstPartyJS(script, artifacts.URL.finalDisplayedUrl);
81
81
 
82
82
  if (isLargeFirstParty && (!sourceMap || !sourceMap.map)) {
83
83
  missingMapsForLargeFirstPartyFile = true;
@@ -14,7 +14,7 @@ import {NetworkRecords} from './network-records.js';
14
14
  import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
15
15
 
16
16
  /** @typedef {import('../lib/dependency-graph/base-node.js').Node} Node */
17
- /** @typedef {Omit<LH.Artifacts['URL'], 'initialUrl'|'finalUrl'>} URLArtifact */
17
+ /** @typedef {Omit<LH.Artifacts['URL'], 'finalDisplayedUrl'>} URLArtifact */
18
18
 
19
19
  /**
20
20
  * @typedef {Object} NetworkNodeOutput
@@ -56,7 +56,7 @@ class ResourceSummary {
56
56
  if (budget?.options?.firstPartyHostnames) {
57
57
  firstPartyHosts = budget.options.firstPartyHostnames;
58
58
  } else {
59
- const rootDomain = Util.getRootDomain(URLArtifact.finalUrl);
59
+ const rootDomain = Util.getRootDomain(URLArtifact.finalDisplayedUrl);
60
60
  firstPartyHosts = [`*.${rootDomain}`];
61
61
  }
62
62
 
@@ -34,8 +34,7 @@ async function getBaseArtifacts(config, driver, context) {
34
34
  'mobile' : 'desktop',
35
35
  // Contextual artifacts whose collection changes based on gather mode.
36
36
  URL: {
37
- initialUrl: '',
38
- finalUrl: '',
37
+ finalDisplayedUrl: '',
39
38
  },
40
39
  PageLoadError: null,
41
40
  GatherContext: context,
@@ -83,13 +82,12 @@ function finalizeArtifacts(baseArtifacts, gathererArtifacts) {
83
82
  artifacts.Timing = log.getTimeEntries();
84
83
  artifacts.LighthouseRunWarnings = deduplicateWarnings(warnings);
85
84
 
86
- if (artifacts.PageLoadError && !artifacts.URL.finalUrl) {
87
- artifacts.URL.finalUrl = artifacts.URL.requestedUrl || artifacts.URL.initialUrl;
85
+ if (artifacts.PageLoadError && !artifacts.URL.finalDisplayedUrl) {
86
+ artifacts.URL.finalDisplayedUrl = artifacts.URL.requestedUrl || '';
88
87
  }
89
88
 
90
89
  // Check that the runner remembered to mutate the special-case URL artifact.
91
- if (!artifacts.URL.initialUrl) throw new Error('Runner did not set initialUrl');
92
- if (!artifacts.URL.finalUrl) throw new Error('Runner did not set finalUrl');
90
+ if (!artifacts.URL.finalDisplayedUrl) throw new Error('Runner did not set finalDisplayedUrl');
93
91
 
94
92
  return artifacts;
95
93
  }
@@ -4,7 +4,7 @@
4
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
5
  */
6
6
 
7
- /* globals window document getBoundingClientRect requestAnimationFrame */
7
+ /* globals window getBoundingClientRect requestAnimationFrame */
8
8
 
9
9
  import FRGatherer from '../base-gatherer.js';
10
10
  import * as emulation from '../../lib/emulation.js';
@@ -31,8 +31,8 @@ function getObservedDeviceMetrics() {
31
31
  // Convert the Web API's kebab case (landscape-primary) to camel case (landscapePrimary).
32
32
  const screenOrientationType = kebabCaseToCamelCase(window.screen.orientation.type);
33
33
  return {
34
- width: document.documentElement.clientWidth,
35
- height: document.documentElement.clientHeight,
34
+ width: window.outerWidth,
35
+ height: window.outerHeight,
36
36
  screenOrientation: {
37
37
  type: screenOrientationType,
38
38
  angle: window.screen.orientation.angle,
@@ -41,6 +41,13 @@ function getObservedDeviceMetrics() {
41
41
  };
42
42
  }
43
43
 
44
+ function getScreenshotAreaSize() {
45
+ return {
46
+ width: window.innerWidth,
47
+ height: window.innerHeight,
48
+ };
49
+ }
50
+
44
51
  function waitForDoubleRaf() {
45
52
  return new Promise((resolve) => {
46
53
  requestAnimationFrame(() => requestAnimationFrame(resolve));
@@ -123,10 +130,16 @@ class FullPageScreenshot extends FRGatherer {
123
130
  });
124
131
  const data = 'data:image/webp;base64,' + result.data;
125
132
 
133
+ const screenshotAreaSize =
134
+ await context.driver.executionContext.evaluate(getScreenshotAreaSize, {
135
+ args: [],
136
+ useIsolation: true,
137
+ deps: [kebabCaseToCamelCase],
138
+ });
126
139
  return {
127
140
  data,
128
- width: deviceMetrics.width,
129
- height,
141
+ width: screenshotAreaSize.width,
142
+ height: screenshotAreaSize.height,
130
143
  };
131
144
  }
132
145
 
@@ -22,12 +22,12 @@ import {MainResource} from '../../computed/main-resource.js';
22
22
  /**
23
23
  *
24
24
  * @param {string} url
25
- * @param {string} finalUrl
25
+ * @param {string} finalDisplayedUrl
26
26
  * @return {string|null}
27
27
  */
28
- function normalizeUrlOrNull(url, finalUrl) {
28
+ function normalizeUrlOrNull(url, finalDisplayedUrl) {
29
29
  try {
30
- return new URL(url, finalUrl).href;
30
+ return new URL(url, finalDisplayedUrl).href;
31
31
  } catch (_) {
32
32
  return null;
33
33
  }
@@ -128,7 +128,7 @@ class LinkElements extends FRGatherer {
128
128
  for (const link of LinkHeader.parse(header.value).refs) {
129
129
  linkElements.push({
130
130
  rel: link.rel || '',
131
- href: normalizeUrlOrNull(link.uri, context.baseArtifacts.URL.finalUrl),
131
+ href: normalizeUrlOrNull(link.uri, context.baseArtifacts.URL.finalDisplayedUrl),
132
132
  hrefRaw: link.uri || '',
133
133
  hreflang: link.hreflang || '',
134
134
  as: link.as || '',
@@ -17,8 +17,8 @@ class RobotsTxt extends FRGatherer {
17
17
  * @return {Promise<LH.Artifacts['RobotsTxt']>}
18
18
  */
19
19
  async getArtifact(passContext) {
20
- const {finalUrl} = passContext.baseArtifacts.URL;
21
- const robotsUrl = new URL('/robots.txt', finalUrl).href;
20
+ const {finalDisplayedUrl} = passContext.baseArtifacts.URL;
21
+ const robotsUrl = new URL('/robots.txt', finalDisplayedUrl).href;
22
22
  return passContext.driver.fetcher.fetchResource(robotsUrl)
23
23
  .catch(err => ({status: null, content: null, errorMessage: err.message}));
24
24
  }
@@ -94,8 +94,8 @@ class WebAppManifest extends FRGatherer {
94
94
  */
95
95
  getArtifact(context) {
96
96
  const driver = context.driver;
97
- const {finalUrl} = context.baseArtifacts.URL;
98
- return WebAppManifest.getWebAppManifest(driver.defaultSession, finalUrl);
97
+ const {finalDisplayedUrl} = context.baseArtifacts.URL;
98
+ return WebAppManifest.getWebAppManifest(driver.defaultSession, finalDisplayedUrl);
99
99
  }
100
100
  }
101
101
 
@@ -214,9 +214,8 @@ async function _computeNavigationResult(
214
214
  */
215
215
  async function _navigation(navigationContext) {
216
216
  const artifactState = getEmptyArtifactState();
217
- const initialUrl = await navigationContext.driver.url();
218
217
  const phaseState = {
219
- url: initialUrl,
218
+ url: await navigationContext.driver.url(),
220
219
  gatherMode: /** @type {const} */ ('navigation'),
221
220
  driver: navigationContext.driver,
222
221
  page: navigationContext.page,
@@ -236,10 +235,9 @@ async function _navigation(navigationContext) {
236
235
  // If we haven't set all the required urls yet, set them here.
237
236
  if (!Object.values(phaseState.baseArtifacts).every(Boolean)) {
238
237
  phaseState.baseArtifacts.URL = {
239
- initialUrl,
240
238
  requestedUrl: navigateResult.requestedUrl,
241
239
  mainDocumentUrl: navigateResult.mainDocumentUrl,
242
- finalUrl: navigateResult.mainDocumentUrl,
240
+ finalDisplayedUrl: await navigationContext.driver.url(),
243
241
  };
244
242
  }
245
243
  phaseState.url = navigateResult.mainDocumentUrl;
@@ -33,8 +33,7 @@ async function snapshotGather(options) {
33
33
  async () => {
34
34
  const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'snapshot'});
35
35
  baseArtifacts.URL = {
36
- initialUrl: url,
37
- finalUrl: url,
36
+ finalDisplayedUrl: url,
38
37
  };
39
38
 
40
39
  const artifactDefinitions = config.artifacts || [];
@@ -28,7 +28,6 @@ async function startTimespanGather(options) {
28
28
  /** @type {Map<string, LH.ArbitraryEqualityMap>} */
29
29
  const computedCache = new Map();
30
30
  const artifactDefinitions = config.artifacts || [];
31
- const initialUrl = await driver.url();
32
31
  const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'timespan'});
33
32
  const artifactState = getEmptyArtifactState();
34
33
  /** @type {Omit<import('./runner-helpers.js').CollectPhaseArtifactOptions, 'phase'>} */
@@ -49,15 +48,12 @@ async function startTimespanGather(options) {
49
48
 
50
49
  return {
51
50
  async endTimespanGather() {
52
- const finalUrl = await driver.url();
51
+ const finalDisplayedUrl = await driver.url();
53
52
 
54
53
  const runnerOptions = {config, computedCache};
55
54
  const artifacts = await Runner.gather(
56
55
  async () => {
57
- baseArtifacts.URL = {
58
- initialUrl,
59
- finalUrl,
60
- };
56
+ baseArtifacts.URL = {finalDisplayedUrl};
61
57
 
62
58
  await collectPhaseArtifacts({phase: 'stopSensitiveInstrumentation', ...phaseOptions});
63
59
  await collectPhaseArtifacts({phase: 'stopInstrumentation', ...phaseOptions});
@@ -82,9 +82,9 @@ class GatherRunner {
82
82
  });
83
83
  passContext.url = mainDocumentUrl;
84
84
  const {URL} = passContext.baseArtifacts;
85
- if (!URL.finalUrl || !URL.mainDocumentUrl) {
86
- URL.finalUrl = mainDocumentUrl;
85
+ if (!URL.finalDisplayedUrl || !URL.mainDocumentUrl) {
87
86
  URL.mainDocumentUrl = mainDocumentUrl;
87
+ URL.finalDisplayedUrl = await passContext.driver.url();
88
88
  }
89
89
  if (passContext.passConfig.loadFailureMode === 'fatal') {
90
90
  passContext.LighthouseRunWarnings.push(...warnings);
@@ -409,10 +409,9 @@ class GatherRunner {
409
409
  settings: options.settings,
410
410
  GatherContext: {gatherMode: 'navigation'},
411
411
  URL: {
412
- initialUrl: await options.driver.url(),
413
412
  requestedUrl: options.requestedUrl,
414
413
  mainDocumentUrl: '',
415
- finalUrl: '',
414
+ finalDisplayedUrl: '',
416
415
  },
417
416
  Timing: [],
418
417
  PageLoadError: null,
package/core/runner.js CHANGED
@@ -90,7 +90,9 @@ class Runner {
90
90
  const i18nLhr = {
91
91
  lighthouseVersion,
92
92
  requestedUrl: artifacts.URL.requestedUrl,
93
- finalUrl: artifacts.URL.finalUrl,
93
+ mainDocumentUrl: artifacts.URL.mainDocumentUrl,
94
+ finalDisplayedUrl: artifacts.URL.finalDisplayedUrl,
95
+ finalUrl: artifacts.URL.mainDocumentUrl,
94
96
  fetchTime: artifacts.fetchTime,
95
97
  gatherMode: artifacts.GatherContext.gatherMode,
96
98
  runtimeError: Runner.getArtifactRuntimeError(artifacts),
package/core/user-flow.js CHANGED
@@ -46,7 +46,7 @@ class UserFlow {
46
46
  * @return {string}
47
47
  */
48
48
  _getDefaultStepName(artifacts) {
49
- const shortUrl = this._shortenUrl(artifacts.URL.finalUrl);
49
+ const shortUrl = this._shortenUrl(artifacts.URL.finalDisplayedUrl);
50
50
  switch (artifacts.GatherContext.gatherMode) {
51
51
  case 'navigation':
52
52
  return `Navigation report (${shortUrl})`;
@@ -264,7 +264,7 @@ async function auditGatherSteps(gatherSteps, options) {
264
264
  steps.push({lhr: result.lhr, name});
265
265
  }
266
266
 
267
- const url = new URL(gatherSteps[0].artifacts.URL.finalUrl);
267
+ const url = new URL(gatherSteps[0].artifacts.URL.finalDisplayedUrl);
268
268
  const flowName = options.name || `User flow (${url.hostname})`;
269
269
  return {steps, name: flowName};
270
270
  }
package/core/util.cjs CHANGED
@@ -54,6 +54,28 @@ class Util {
54
54
  return `%10d${NBSP}ms`;
55
55
  }
56
56
 
57
+ /**
58
+ * If LHR is older than 10.0 it will not have the `finalDisplayedUrl` property.
59
+ * Old LHRs should have the `finalUrl` property which will work fine for the report.
60
+ *
61
+ * @param {LH.Result} lhr
62
+ */
63
+ static getFinalDisplayedUrl(lhr) {
64
+ if (lhr.finalDisplayedUrl) return lhr.finalDisplayedUrl;
65
+ if (lhr.finalUrl) return lhr.finalUrl;
66
+ throw new Error('Could not determine final displayed URL');
67
+ }
68
+
69
+ /**
70
+ * If LHR is older than 10.0 it will not have the `mainDocumentUrl` property.
71
+ * Old LHRs should have the `finalUrl` property which is the same as `mainDocumentUrl`.
72
+ *
73
+ * @param {LH.Result} lhr
74
+ */
75
+ static getMainDocumentUrl(lhr) {
76
+ return lhr.mainDocumentUrl || lhr.finalUrl;
77
+ }
78
+
57
79
  /**
58
80
  * Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer.
59
81
  * Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be
@@ -76,6 +98,9 @@ class Util {
76
98
  clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor;
77
99
  }
78
100
 
101
+ clone.finalDisplayedUrl = this.getFinalDisplayedUrl(clone);
102
+ clone.mainDocumentUrl = this.getMainDocumentUrl(clone);
103
+
79
104
  for (const audit of Object.values(clone.audits)) {
80
105
  // Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions)
81
106
  // into 'notApplicable' (LHR ≥4.0).
@@ -50,6 +50,28 @@ class Util {
50
50
  return `%10d${NBSP}ms`;
51
51
  }
52
52
 
53
+ /**
54
+ * If LHR is older than 10.0 it will not have the `finalDisplayedUrl` property.
55
+ * Old LHRs should have the `finalUrl` property which will work fine for the report.
56
+ *
57
+ * @param {LH.Result} lhr
58
+ */
59
+ static getFinalDisplayedUrl(lhr) {
60
+ if (lhr.finalDisplayedUrl) return lhr.finalDisplayedUrl;
61
+ if (lhr.finalUrl) return lhr.finalUrl;
62
+ throw new Error('Could not determine final displayed URL');
63
+ }
64
+
65
+ /**
66
+ * If LHR is older than 10.0 it will not have the `mainDocumentUrl` property.
67
+ * Old LHRs should have the `finalUrl` property which is the same as `mainDocumentUrl`.
68
+ *
69
+ * @param {LH.Result} lhr
70
+ */
71
+ static getMainDocumentUrl(lhr) {
72
+ return lhr.mainDocumentUrl || lhr.finalUrl;
73
+ }
74
+
53
75
  /**
54
76
  * Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer.
55
77
  * Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be
@@ -72,6 +94,9 @@ class Util {
72
94
  clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor;
73
95
  }
74
96
 
97
+ clone.finalDisplayedUrl = this.getFinalDisplayedUrl(clone);
98
+ clone.mainDocumentUrl = this.getMainDocumentUrl(clone);
99
+
75
100
  for (const audit of Object.values(clone.audits)) {
76
101
  // Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions)
77
102
  // into 'notApplicable' (LHR ≥4.0).
@@ -2779,7 +2804,12 @@ class ElementScreenshotRenderer {
2779
2804
  width: maxRenderSizeDC.width / zoomFactor,
2780
2805
  height: maxRenderSizeDC.height / zoomFactor,
2781
2806
  };
2782
- elementPreviewSizeSC.width = Math.min(screenshot.width, elementPreviewSizeSC.width);
2807
+
2808
+ if (screenshot.width < elementPreviewSizeSC.width) {
2809
+ elementPreviewSizeSC.width = screenshot.width;
2810
+ elementPreviewSizeSC.height = screenshot.height;
2811
+ }
2812
+
2783
2813
  /* This preview size is either the size of the thumbnail or size of the Lightbox */
2784
2814
  const elementPreviewSizeDC = {
2785
2815
  width: elementPreviewSizeSC.width * zoomFactor,
@@ -4313,9 +4343,9 @@ class ReportRenderer {
4313
4343
  _renderReportTopbar(report) {
4314
4344
  const el = this._dom.createComponent('topbar');
4315
4345
  const metadataUrl = this._dom.find('a.lh-topbar__url', el);
4316
- metadataUrl.textContent = report.finalUrl;
4317
- metadataUrl.title = report.finalUrl;
4318
- this._dom.safelySetHref(metadataUrl, report.finalUrl);
4346
+ metadataUrl.textContent = report.finalDisplayedUrl;
4347
+ metadataUrl.title = report.finalDisplayedUrl;
4348
+ this._dom.safelySetHref(metadataUrl, report.finalDisplayedUrl);
4319
4349
  return el;
4320
4350
  }
4321
4351
 
@@ -4713,7 +4743,7 @@ function computeWindowNameSuffix(json) {
4713
4743
  // @ts-expect-error - If this is a v2 LHR, use old `generatedTime`.
4714
4744
  const fallbackFetchTime = /** @type {string} */ (json.generatedTime);
4715
4745
  const fetchTime = json.fetchTime || fallbackFetchTime;
4716
- return `${json.lighthouseVersion}-${json.finalUrl}-${fetchTime}`;
4746
+ return `${json.lighthouseVersion}-${json.finalDisplayedUrl}-${fetchTime}`;
4717
4747
  }
4718
4748
 
4719
4749
  /**
@@ -4795,7 +4825,9 @@ function openTreemap(json) {
4795
4825
  /** @type {LH.Treemap.Options} */
4796
4826
  const treemapOptions = {
4797
4827
  lhr: {
4828
+ mainDocumentUrl: json.mainDocumentUrl,
4798
4829
  finalUrl: json.finalUrl,
4830
+ finalDisplayedUrl: json.finalDisplayedUrl,
4799
4831
  audits: {
4800
4832
  'script-treemap-data': json.audits['script-treemap-data'],
4801
4833
  },
@@ -5369,11 +5401,11 @@ function getFilenamePrefix(name, fetchTime) {
5369
5401
 
5370
5402
  /**
5371
5403
  * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS.
5372
- * @param {{finalUrl: string, fetchTime: string}} lhr
5404
+ * @param {{finalDisplayedUrl: string, fetchTime: string}} lhr
5373
5405
  * @return {string}
5374
5406
  */
5375
5407
  function getLhrFilenamePrefix(lhr) {
5376
- const hostname = new URL(lhr.finalUrl).hostname;
5408
+ const hostname = new URL(lhr.finalDisplayedUrl).hostname;
5377
5409
  return getFilenamePrefix(hostname, lhr.fetchTime);
5378
5410
  }
5379
5411
 
@@ -5607,7 +5639,7 @@ class ReportUIFeatures {
5607
5639
 
5608
5640
  tablesWithUrls.forEach((tableEl) => {
5609
5641
  const rowEls = getTableRows(tableEl);
5610
- const thirdPartyRows = this._getThirdPartyRows(rowEls, this.json.finalUrl);
5642
+ const thirdPartyRows = this._getThirdPartyRows(rowEls, Util.getFinalDisplayedUrl(this.json));
5611
5643
 
5612
5644
  // create input box
5613
5645
  const filterTemplate = this._dom.createComponent('3pFilter');
@@ -5683,13 +5715,13 @@ class ReportUIFeatures {
5683
5715
  * From a table with URL entries, finds the rows containing third-party URLs
5684
5716
  * and returns them.
5685
5717
  * @param {HTMLElement[]} rowEls
5686
- * @param {string} finalUrl
5718
+ * @param {string} finalDisplayedUrl
5687
5719
  * @return {Array<HTMLElement>}
5688
5720
  */
5689
- _getThirdPartyRows(rowEls, finalUrl) {
5721
+ _getThirdPartyRows(rowEls, finalDisplayedUrl) {
5690
5722
  /** @type {Array<HTMLElement>} */
5691
5723
  const thirdPartyRows = [];
5692
- const finalUrlRootDomain = Util.getRootDomain(finalUrl);
5724
+ const finalDisplayedUrlRootDomain = Util.getRootDomain(finalDisplayedUrl);
5693
5725
 
5694
5726
  for (const rowEl of rowEls) {
5695
5727
  if (rowEl.classList.contains('lh-sub-item-row')) continue;
@@ -5699,7 +5731,7 @@ class ReportUIFeatures {
5699
5731
 
5700
5732
  const datasetUrl = urlItem.dataset.url;
5701
5733
  if (!datasetUrl) continue;
5702
- const isThirdParty = Util.getRootDomain(datasetUrl) !== finalUrlRootDomain;
5734
+ const isThirdParty = Util.getRootDomain(datasetUrl) !== finalDisplayedUrlRootDomain;
5703
5735
  if (!isThirdParty) continue;
5704
5736
 
5705
5737
  thirdPartyRows.push(rowEl);
@@ -5713,7 +5745,10 @@ class ReportUIFeatures {
5713
5745
  */
5714
5746
  _saveFile(blob) {
5715
5747
  const ext = blob.type.match('json') ? '.json' : '.html';
5716
- const filename = getLhrFilenamePrefix(this.json) + ext;
5748
+ const filename = getLhrFilenamePrefix({
5749
+ finalDisplayedUrl: Util.getFinalDisplayedUrl(this.json),
5750
+ fetchTime: this.json.fetchTime,
5751
+ }) + ext;
5717
5752
  if (this._opts.onSaveFileOverride) {
5718
5753
  this._opts.onSaveFileOverride(blob, filename);
5719
5754
  } else {