lighthouse 9.5.0-dev.20220626 → 9.5.0-dev.20220629

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 (54) hide show
  1. package/dist/report/bundle.esm.js +7 -6
  2. package/dist/report/flow.js +24 -24
  3. package/dist/report/standalone.js +19 -19
  4. package/flow-report/test/common-test.tsx +5 -4
  5. package/flow-report/test/flow-report-pptr-test.ts +4 -7
  6. package/flow-report/test/run-flow-report-tests.sh +20 -0
  7. package/flow-report/test/setup/env-setup.ts +44 -24
  8. package/flow-report/test/sidebar/sidebar-test.tsx +0 -1
  9. package/flow-report/test/topbar-test.tsx +5 -5
  10. package/flow-report/test/util-test.tsx +3 -3
  11. package/flow-report/tsconfig.json +4 -1
  12. package/lighthouse-cli/cli-flags.js +313 -305
  13. package/lighthouse-cli/test/smokehouse/report-assert-test.js +1 -1
  14. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +30 -5
  15. package/lighthouse-core/audits/network-requests.js +79 -62
  16. package/lighthouse-core/audits/preload-lcp-image.js +33 -7
  17. package/lighthouse-core/config/config-plugin.js +26 -0
  18. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +1 -0
  19. package/lighthouse-core/gather/gather-runner.js +1 -0
  20. package/lighthouse-core/gather/gatherers/main-document-content.js +0 -2
  21. package/lighthouse-core/lib/dependency-graph/network-node.js +1 -1
  22. package/lighthouse-core/lib/lh-env.js +1 -1
  23. package/lighthouse-core/lib/navigation-error.js +26 -7
  24. package/lighthouse-core/util-commonjs.js +7 -6
  25. package/package.json +24 -19
  26. package/report/renderer/util.js +7 -6
  27. package/report/test/.eslintrc.cjs +4 -1
  28. package/report/test/clients/bundle-test.js +4 -4
  29. package/report/test/generator/report-generator-test.js +3 -1
  30. package/report/test/renderer/category-renderer-test.js +3 -3
  31. package/report/test/renderer/components-test.js +36 -34
  32. package/report/test/renderer/crc-details-renderer-test.js +2 -2
  33. package/report/test/renderer/details-renderer-test.js +2 -2
  34. package/report/test/renderer/dom-test.js +4 -4
  35. package/report/test/renderer/element-screenshot-renderer-test.js +3 -2
  36. package/report/test/renderer/performance-category-renderer-test.js +4 -4
  37. package/report/test/renderer/pwa-category-renderer-test.js +3 -3
  38. package/report/test/renderer/report-renderer-axe-test.js +6 -8
  39. package/report/test/renderer/report-renderer-test.js +5 -5
  40. package/report/test/renderer/report-ui-features-test.js +8 -8
  41. package/report/test/renderer/snippet-renderer-test.js +2 -2
  42. package/report/test/renderer/text-encoding-test.js +2 -2
  43. package/report/test/renderer/util-test.js +1 -1
  44. package/root.js +0 -18
  45. package/shared/localization/locales/en-US.json +3 -0
  46. package/shared/localization/locales/en-XL.json +3 -0
  47. package/shared/test/localization/.eslintrc.cjs +4 -1
  48. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +1 -1
  49. package/third-party/chromium-synchronization/installability-errors-test.js +1 -3
  50. package/types/test.d.ts +53 -0
  51. package/flow-report/jest.config.js +0 -24
  52. package/flow-report/test/setup/global-setup.ts +0 -11
  53. package/jest.config.js +0 -30
  54. package/types/jest.d.ts +0 -25
@@ -6,8 +6,8 @@
6
6
 
7
7
  /* eslint-disable no-control-regex */
8
8
 
9
+ import {readJson} from '../../../lighthouse-core/test/test-utils.js';
9
10
  import {findDifferences, getAssertionReport} from './report-assert.js';
10
- import {readJson} from '../../../root.js';
11
11
 
12
12
  describe('findDiffersences', () => {
13
13
  const testCases = {
@@ -33,6 +33,9 @@ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
33
33
 
34
34
  const IGNORE_THRESHOLD_IN_BYTES = 4096;
35
35
 
36
+ // Ignore up to 12KB of waste if an effort was made with breakpoints.
37
+ const IGNORE_THRESHOLD_IN_BYTES_BREAKPOINTS_PRESENT = 12288;
38
+
36
39
  class UsesResponsiveImages extends ByteEfficiencyAudit {
37
40
  /**
38
41
  * @return {LH.Audit.Meta}
@@ -112,6 +115,18 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
112
115
  };
113
116
  }
114
117
 
118
+
119
+ /**
120
+ * @param {LH.Artifacts.ImageElement} image
121
+ * @return {number};
122
+ */
123
+ static determineAllowableWaste(image) {
124
+ if (image.srcset || image.isPicture) {
125
+ return IGNORE_THRESHOLD_IN_BYTES_BREAKPOINTS_PRESENT;
126
+ }
127
+ return IGNORE_THRESHOLD_IN_BYTES;
128
+ }
129
+
115
130
  /**
116
131
  * @param {LH.Artifacts} artifacts
117
132
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@@ -126,6 +141,8 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
126
141
  const ViewportDimensions = artifacts.ViewportDimensions;
127
142
  /** @type {Map<string, LH.Audit.ByteEfficiencyItem>} */
128
143
  const resultsMap = new Map();
144
+ /** @type {Array<string>} */
145
+ const passedImageList = [];
129
146
  for (const image of images) {
130
147
  // Give SVG a free pass because creating a "responsive" SVG is of questionable value.
131
148
  // Ignore CSS images because it's difficult to determine what is a spritesheet,
@@ -147,15 +164,23 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
147
164
  );
148
165
  if (!processed) continue;
149
166
 
150
- // Don't warn about an image that was later used appropriately
167
+ // Verify the image wastes more than the minimum.
168
+ const exceedsAllowableWaste = processed.wastedBytes > this.determineAllowableWaste(image);
169
+
151
170
  const existing = resultsMap.get(processed.url);
152
- if (!existing || existing.wastedBytes > processed.wastedBytes) {
153
- resultsMap.set(processed.url, processed);
171
+ // Don't warn about an image that was later used appropriately, or wastes a trivial amount of data.
172
+ if (exceedsAllowableWaste && !passedImageList.includes(processed.url)) {
173
+ if ((!existing || existing.wastedBytes > processed.wastedBytes)) {
174
+ resultsMap.set(processed.url, processed);
175
+ }
176
+ } else {
177
+ // Ensure this url passes for future tests.
178
+ resultsMap.delete(processed.url);
179
+ passedImageList.push(processed.url);
154
180
  }
155
181
  }
156
182
 
157
- const items = Array.from(resultsMap.values())
158
- .filter(item => item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES);
183
+ const items = Array.from(resultsMap.values());
159
184
 
160
185
  /** @type {LH.Audit.Details.Opportunity['headings']} */
161
186
  const headings = [
@@ -8,6 +8,7 @@
8
8
  const Audit = require('./audit.js');
9
9
  const URL = require('../lib/url-shim.js');
10
10
  const NetworkRecords = require('../computed/network-records.js');
11
+ const MainResource = require('../computed/main-resource.js');
11
12
 
12
13
  class NetworkRequests extends Audit {
13
14
  /**
@@ -19,7 +20,7 @@ class NetworkRequests extends Audit {
19
20
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
20
21
  title: 'Network Requests',
21
22
  description: 'Lists the network requests that were made during page load.',
22
- requiredArtifacts: ['devtoolsLogs'],
23
+ requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
23
24
  };
24
25
  }
25
26
 
@@ -28,75 +29,91 @@ class NetworkRequests extends Audit {
28
29
  * @param {LH.Audit.Context} context
29
30
  * @return {Promise<LH.Audit.Product>}
30
31
  */
31
- static audit(artifacts, context) {
32
+ static async audit(artifacts, context) {
32
33
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
33
- return NetworkRecords.request(devtoolsLog, context).then(records => {
34
- const earliestStartTime = records.reduce(
35
- (min, record) => Math.min(min, record.startTime),
36
- Infinity
37
- );
34
+ const records = await NetworkRecords.request(devtoolsLog, context);
35
+ const earliestStartTime = records.reduce(
36
+ (min, record) => Math.min(min, record.startTime),
37
+ Infinity
38
+ );
38
39
 
39
- /** @param {number} time */
40
- const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
41
- undefined : (time - earliestStartTime) * 1000;
40
+ // Optional mainFrameId check because the main resource is only available for
41
+ // navigations. TODO: https://github.com/GoogleChrome/lighthouse/issues/14157
42
+ // for the general solution to this.
43
+ /** @type {string|undefined} */
44
+ let mainFrameId;
45
+ if (artifacts.GatherContext.gatherMode === 'navigation') {
46
+ const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
47
+ mainFrameId = mainResource.frameId;
48
+ }
42
49
 
43
- const results = records.map(record => {
44
- const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
45
- const TCPMs = record.lrStatistics?.TCPMs;
46
- const requestMs = record.lrStatistics?.requestMs;
47
- const responseMs = record.lrStatistics?.responseMs;
50
+ /** @param {number} time */
51
+ const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
52
+ undefined : (time - earliestStartTime) * 1000;
48
53
 
49
- return {
50
- url: URL.elideDataURI(record.url),
51
- protocol: record.protocol,
52
- startTime: timeToMs(record.startTime),
53
- endTime: timeToMs(record.endTime),
54
- finished: record.finished,
55
- transferSize: record.transferSize,
56
- resourceSize: record.resourceSize,
57
- statusCode: record.statusCode,
58
- mimeType: record.mimeType,
59
- resourceType: record.resourceType,
60
- lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
61
- lrTCPMs: TCPMs, // Only exists on Lightrider runs
62
- lrRequestMs: requestMs, // Only exists on Lightrider runs
63
- lrResponseMs: responseMs, // Only exists on Lightrider runs
64
- };
65
- });
66
-
67
- // NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
68
- /** @type {LH.Audit.Details.Table['headings']} */
69
- const headings = [
70
- {key: 'url', itemType: 'url', text: 'URL'},
71
- {key: 'protocol', itemType: 'text', text: 'Protocol'},
72
- {key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
73
- {key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
74
- {
75
- key: 'transferSize',
76
- itemType: 'bytes',
77
- displayUnit: 'kb',
78
- granularity: 1,
79
- text: 'Transfer Size',
80
- },
81
- {
82
- key: 'resourceSize',
83
- itemType: 'bytes',
84
- displayUnit: 'kb',
85
- granularity: 1,
86
- text: 'Resource Size',
87
- },
88
- {key: 'statusCode', itemType: 'text', text: 'Status Code'},
89
- {key: 'mimeType', itemType: 'text', text: 'MIME Type'},
90
- {key: 'resourceType', itemType: 'text', text: 'Resource Type'},
91
- ];
92
-
93
- const tableDetails = Audit.makeTableDetails(headings, results);
54
+ const results = records.map(record => {
55
+ const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
56
+ const TCPMs = record.lrStatistics?.TCPMs;
57
+ const requestMs = record.lrStatistics?.requestMs;
58
+ const responseMs = record.lrStatistics?.responseMs;
59
+ // Default these to undefined so omitted from JSON in the negative case.
60
+ const isLinkPreload = record.isLinkPreload || undefined;
61
+ const experimentalFromMainFrame = mainFrameId ?
62
+ ((record.frameId === mainFrameId) || undefined) :
63
+ undefined;
94
64
 
95
65
  return {
96
- score: 1,
97
- details: tableDetails,
66
+ url: URL.elideDataURI(record.url),
67
+ protocol: record.protocol,
68
+ startTime: timeToMs(record.startTime),
69
+ endTime: timeToMs(record.endTime),
70
+ finished: record.finished,
71
+ transferSize: record.transferSize,
72
+ resourceSize: record.resourceSize,
73
+ statusCode: record.statusCode,
74
+ mimeType: record.mimeType,
75
+ resourceType: record.resourceType,
76
+ isLinkPreload,
77
+ experimentalFromMainFrame,
78
+ lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
79
+ lrTCPMs: TCPMs, // Only exists on Lightrider runs
80
+ lrRequestMs: requestMs, // Only exists on Lightrider runs
81
+ lrResponseMs: responseMs, // Only exists on Lightrider runs
98
82
  };
99
83
  });
84
+
85
+ // NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
86
+ /** @type {LH.Audit.Details.Table['headings']} */
87
+ const headings = [
88
+ {key: 'url', itemType: 'url', text: 'URL'},
89
+ {key: 'protocol', itemType: 'text', text: 'Protocol'},
90
+ {key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
91
+ {key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
92
+ {
93
+ key: 'transferSize',
94
+ itemType: 'bytes',
95
+ displayUnit: 'kb',
96
+ granularity: 1,
97
+ text: 'Transfer Size',
98
+ },
99
+ {
100
+ key: 'resourceSize',
101
+ itemType: 'bytes',
102
+ displayUnit: 'kb',
103
+ granularity: 1,
104
+ text: 'Resource Size',
105
+ },
106
+ {key: 'statusCode', itemType: 'text', text: 'Status Code'},
107
+ {key: 'mimeType', itemType: 'text', text: 'MIME Type'},
108
+ {key: 'resourceType', itemType: 'text', text: 'Resource Type'},
109
+ ];
110
+
111
+ const tableDetails = Audit.makeTableDetails(headings, results);
112
+
113
+ return {
114
+ score: 1,
115
+ details: tableDetails,
116
+ };
100
117
  }
101
118
  }
102
119
 
@@ -23,6 +23,10 @@ const UIStrings = {
23
23
 
24
24
  const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
25
25
 
26
+ /**
27
+ * @typedef {Array<{url: string, initiatorType: string}>} InitiatorPath
28
+ */
29
+
26
30
  class PreloadLCPImageAudit extends Audit {
27
31
  /**
28
32
  * @return {LH.Audit.Meta}
@@ -86,22 +90,33 @@ class PreloadLCPImageAudit extends Audit {
86
90
  * @param {LH.Gatherer.Simulation.GraphNode} graph
87
91
  * @param {LH.Artifacts.TraceElement|undefined} lcpElement
88
92
  * @param {Array<LH.Artifacts.ImageElement>} imageElements
89
- * @return {LH.Gatherer.Simulation.GraphNetworkNode|undefined}
93
+ * @return {{lcpNodeToPreload?: LH.Gatherer.Simulation.GraphNetworkNode, initiatorPath?: InitiatorPath}}
90
94
  */
91
95
  static getLCPNodeToPreload(mainResource, graph, lcpElement, imageElements) {
92
- if (!lcpElement) return undefined;
96
+ if (!lcpElement) return {};
93
97
 
94
98
  const lcpImageElement = imageElements.find(elem => {
95
99
  return elem.node.devtoolsNodePath === lcpElement.node.devtoolsNodePath;
96
100
  });
97
101
 
98
- if (!lcpImageElement) return undefined;
102
+ if (!lcpImageElement) return {};
99
103
  const lcpUrl = lcpImageElement.src;
100
104
  const {lcpNode, path} = PreloadLCPImageAudit.findLCPNode(graph, lcpUrl);
101
- if (!lcpNode || !path) return undefined;
105
+ if (!lcpNode || !path) return {};
106
+
102
107
  // eslint-disable-next-line max-len
103
108
  const shouldPreload = PreloadLCPImageAudit.shouldPreloadRequest(lcpNode.record, mainResource, path);
104
- return shouldPreload ? lcpNode : undefined;
109
+ const lcpNodeToPreload = shouldPreload ? lcpNode : undefined;
110
+
111
+ const initiatorPath = [
112
+ {url: lcpNode.record.url, initiatorType: lcpNode.initiatorType},
113
+ ...path.map(n => ({url: n.record.url, initiatorType: n.initiatorType})),
114
+ ];
115
+
116
+ return {
117
+ lcpNodeToPreload,
118
+ initiatorPath,
119
+ };
105
120
  }
106
121
 
107
122
  /**
@@ -215,10 +230,10 @@ class PreloadLCPImageAudit extends Audit {
215
230
 
216
231
  const graph = lanternLCP.pessimisticGraph;
217
232
  // eslint-disable-next-line max-len
218
- const lcpNode = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
233
+ const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
219
234
 
220
235
  const {results, wastedMs} =
221
- PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNode, graph, simulator);
236
+ PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
222
237
 
223
238
  /** @type {LH.Audit.Details.Opportunity['headings']} */
224
239
  const headings = [
@@ -228,6 +243,17 @@ class PreloadLCPImageAudit extends Audit {
228
243
  ];
229
244
  const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
230
245
 
246
+ // If LCP element was an image and had valid network records (regardless of
247
+ // if it should be preloaded), it will be found first in the `initiatorPath`.
248
+ // Otherwise path and length will be undefined.
249
+ if (initiatorPath) {
250
+ details.debugData = {
251
+ type: 'debugdata',
252
+ initiatorPath,
253
+ pathLength: initiatorPath.length,
254
+ };
255
+ }
256
+
231
257
  return {
232
258
  score: UnusedBytes.scoreForWastedMs(wastedMs),
233
259
  numericValue: wastedMs,
@@ -23,6 +23,24 @@ function isObjectOfUnknownProperties(val) {
23
23
  return typeof val === 'object' && val !== null && !Array.isArray(val);
24
24
  }
25
25
 
26
+ /**
27
+ * @param {unknown} str
28
+ * @return {str is LH.Gatherer.GatherMode}
29
+ */
30
+ function objectIsGatherMode(str) {
31
+ if (typeof str !== 'string') return false;
32
+ return str === 'navigation' || str === 'timespan' || str === 'snapshot';
33
+ }
34
+
35
+ /**
36
+ * @param {unknown} arr
37
+ * @return {arr is Array<LH.Gatherer.GatherMode>}
38
+ */
39
+ function isArrayOfGatherModes(arr) {
40
+ if (!Array.isArray(arr)) return false;
41
+ return arr.every(objectIsGatherMode);
42
+ }
43
+
26
44
  /**
27
45
  * Asserts that obj has no own properties, throwing a nice error message if it does.
28
46
  * Plugin and object name are included for nicer logging.
@@ -124,6 +142,7 @@ class ConfigPlugin {
124
142
  description,
125
143
  manualDescription,
126
144
  auditRefs: auditRefsJson,
145
+ supportedModes,
127
146
  ...invalidRest
128
147
  } = categoryJson;
129
148
 
@@ -138,6 +157,12 @@ class ConfigPlugin {
138
157
  if (!i18n.isStringOrIcuMessage(manualDescription) && manualDescription !== undefined) {
139
158
  throw new Error(`${pluginName} has an invalid category manualDescription.`);
140
159
  }
160
+ if (!isArrayOfGatherModes(supportedModes) && supportedModes !== undefined) {
161
+ throw new Error(
162
+ `${pluginName} supportedModes must be an array, ` +
163
+ `valid array values are "navigation", "timespan", and "snapshot".`
164
+ );
165
+ }
141
166
  const auditRefs = ConfigPlugin._parseAuditRefsList(auditRefsJson, pluginName);
142
167
 
143
168
  return {
@@ -145,6 +170,7 @@ class ConfigPlugin {
145
170
  auditRefs,
146
171
  description: description,
147
172
  manualDescription: manualDescription,
173
+ supportedModes,
148
174
  };
149
175
  }
150
176
 
@@ -178,6 +178,7 @@ async function _computeNavigationResult(
178
178
  url: mainDocumentUrl,
179
179
  loadFailureMode: navigationContext.navigation.loadFailureMode,
180
180
  networkRecords: debugData.records,
181
+ warnings,
181
182
  })
182
183
  : navigationError;
183
184
 
@@ -604,6 +604,7 @@ class GatherRunner {
604
604
  url: passContext.url,
605
605
  loadFailureMode: passConfig.loadFailureMode,
606
606
  networkRecords: loadData.networkRecords,
607
+ warnings: passContext.LighthouseRunWarnings,
607
608
  });
608
609
  if (pageLoadError) {
609
610
  const localizedMessage = format.getFormatted(pageLoadError.friendlyMessage,
@@ -21,7 +21,6 @@ class MainDocumentContent extends FRGatherer {
21
21
  };
22
22
 
23
23
  /**
24
- *
25
24
  * @param {LH.Gatherer.FRTransitionalContext} context
26
25
  * @param {LH.Artifacts['DevtoolsLog']} devtoolsLog
27
26
  * @return {Promise<LH.Artifacts['MainDocumentContent']>}
@@ -34,7 +33,6 @@ class MainDocumentContent extends FRGatherer {
34
33
  }
35
34
 
36
35
  /**
37
- *
38
36
  * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context
39
37
  * @return {Promise<LH.Artifacts['MainDocumentContent']>}
40
38
  */
@@ -44,7 +44,7 @@ class NetworkNode extends BaseNode {
44
44
  }
45
45
 
46
46
  /**
47
- * @return {?string}
47
+ * @return {string}
48
48
  */
49
49
  get initiatorType() {
50
50
  return this._record.initiator && this._record.initiator.type;
@@ -8,7 +8,7 @@
8
8
  const process = require('process');
9
9
 
10
10
  module.exports = {
11
- // NODE_ENV is set to test by jest and by smokehouse CLI runner
11
+ // NODE_ENV is set to test by mocha-setup.js and the smokehouse CLI runner
12
12
  // CI as a catchall for everything we do in GitHub Actions
13
13
  isUnderTest: !!process.env.CI || process.env.NODE_ENV === 'test',
14
14
  };
@@ -8,6 +8,22 @@
8
8
  const LHError = require('./lh-error.js');
9
9
  const NetworkAnalyzer = require('./dependency-graph/simulator/network-analyzer.js');
10
10
  const NetworkRequest = require('./network-request.js');
11
+ const i18n = require('./i18n/i18n.js');
12
+
13
+ const UIStrings = {
14
+ /**
15
+ * Warning shown in report when the page under test is an XHTML document, which Lighthouse does not directly support
16
+ * so we display a warning.
17
+ */
18
+ warningXhtml:
19
+ 'The page MIME type is XHTML: Lighthouse does not explicitly support this document type',
20
+ };
21
+
22
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
23
+
24
+ // MIME types are case-insensitive but Chrome normalizes MIME types to be lowercase.
25
+ const HTML_MIME_TYPE = 'text/html';
26
+ const XHTML_MIME_TYPE = 'application/xhtml+xml';
11
27
 
12
28
  /**
13
29
  * Returns an error if the original network request failed or wasn't found.
@@ -76,16 +92,15 @@ function getInterstitialError(mainRecord, networkRecords) {
76
92
  * @return {LH.LighthouseError|undefined}
77
93
  */
78
94
  function getNonHtmlError(finalRecord) {
79
- // MIME types are case-insenstive but Chrome normalizes MIME types to be lowercase.
80
- const HTML_MIME_TYPE = 'text/html';
81
-
82
95
  // If we never requested a document, there's no doctype error, let other cases handle it.
83
96
  if (!finalRecord) return undefined;
84
97
 
85
98
  // mimeType is determined by the browser, we assume Chrome is determining mimeType correctly,
86
99
  // independently of 'Content-Type' response headers, and always sending mimeType if well-formed.
87
- if (HTML_MIME_TYPE !== finalRecord.mimeType) {
88
- return new LHError(LHError.errors.NOT_HTML, {mimeType: finalRecord.mimeType});
100
+ if (finalRecord.mimeType !== HTML_MIME_TYPE && finalRecord.mimeType !== XHTML_MIME_TYPE) {
101
+ return new LHError(LHError.errors.NOT_HTML, {
102
+ mimeType: finalRecord.mimeType,
103
+ });
89
104
  }
90
105
 
91
106
  return undefined;
@@ -95,7 +110,7 @@ function getNonHtmlError(finalRecord) {
95
110
  * Returns an error if the page load should be considered failed, e.g. from a
96
111
  * main document request failure, a security issue, etc.
97
112
  * @param {LH.LighthouseError|undefined} navigationError
98
- * @param {{url: string, loadFailureMode: LH.Gatherer.PassContext['passConfig']['loadFailureMode'], networkRecords: Array<LH.Artifacts.NetworkRequest>}} context
113
+ * @param {{url: string, loadFailureMode: LH.Gatherer.PassContext['passConfig']['loadFailureMode'], networkRecords: Array<LH.Artifacts.NetworkRequest>, warnings: Array<string | LH.IcuMessage>}} context
99
114
  * @return {LH.LighthouseError|undefined}
100
115
  */
101
116
  function getPageLoadError(navigationError, context) {
@@ -120,6 +135,10 @@ function getPageLoadError(navigationError, context) {
120
135
  finalRecord = NetworkAnalyzer.resolveRedirects(mainRecord);
121
136
  }
122
137
 
138
+ if (finalRecord?.mimeType === XHTML_MIME_TYPE) {
139
+ context.warnings.push(str_(UIStrings.warningXhtml));
140
+ }
141
+
123
142
  const networkError = getNetworkError(mainRecord);
124
143
  const interstitialError = getInterstitialError(mainRecord, networkRecords);
125
144
  const nonHtmlError = getNonHtmlError(finalRecord);
@@ -145,10 +164,10 @@ function getPageLoadError(navigationError, context) {
145
164
  return navigationError;
146
165
  }
147
166
 
148
-
149
167
  module.exports = {
150
168
  getNetworkError,
151
169
  getInterstitialError,
152
170
  getPageLoadError,
153
171
  getNonHtmlError,
172
+ UIStrings,
154
173
  };
@@ -572,15 +572,16 @@ class Util {
572
572
  */
573
573
  Util.reportJson = null;
574
574
 
575
+ let svgSuffix = 0;
575
576
  /**
576
577
  * An always-increasing counter for making unique SVG ID suffixes.
577
578
  */
578
- Util.getUniqueSuffix = (() => {
579
- let svgSuffix = 0;
580
- return function() {
581
- return svgSuffix++;
582
- };
583
- })();
579
+ Util.getUniqueSuffix = () => {
580
+ return svgSuffix++;
581
+ };
582
+ Util.resetUniqueSuffix = () => {
583
+ svgSuffix = 0;
584
+ };
584
585
 
585
586
  /**
586
587
  * Report-renderer-specific strings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.5.0-dev.20220626",
3
+ "version": "9.5.0-dev.20220629",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -35,24 +35,24 @@
35
35
  "smoke": "node lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js",
36
36
  "debug": "node --inspect-brk ./lighthouse-cli/index.js",
37
37
  "start": "yarn build-report --standalone && node ./lighthouse-cli/index.js",
38
- "jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
38
+ "mocha": "node lighthouse-core/test/scripts/run-mocha-tests.js",
39
39
  "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check",
40
40
  "test-bundle": "yarn smoke --runner bundle --retries=2",
41
- "test-clients": "yarn jest \"$PWD/clients/\" && yarn jest --testMatch=\"**/clients/test/**/*-test-pptr.js\"",
42
- "test-viewer": "yarn unit-viewer && yarn jest --testMatch=\"**/viewer/**/*-test-pptr.js\"",
43
- "test-treemap": "yarn unit-treemap && yarn jest --testMatch=\"**/treemap/**/*-test-pptr.js\"",
41
+ "test-clients": "yarn mocha --testMatch clients/**/*-test.js && yarn mocha --testMatch clients/**/*-test-pptr.js",
42
+ "test-viewer": "yarn unit-viewer && yarn mocha --testMatch viewer/**/*-test-pptr.js --timeout 35000",
43
+ "test-treemap": "yarn unit-treemap && yarn mocha --testMatch treemap/**/*-test-pptr.js --timeout 35000",
44
44
  "test-lantern": "bash lighthouse-core/scripts/test-lantern.sh",
45
45
  "test-legacy-javascript": "bash lighthouse-core/scripts/test-legacy-javascript.sh",
46
46
  "test-docs": "yarn --cwd docs/recipes/ test",
47
47
  "test-proto": "yarn compile-proto && yarn build-proto-roundtrip",
48
- "unit-core": "yarn jest \"lighthouse-core\"",
49
- "unit-cli": "yarn jest \"lighthouse-cli/\"",
50
- "unit-report": "yarn jest \"report/\"",
51
- "unit-treemap": "yarn jest \"treemap/.*-test.js\"",
52
- "unit-viewer": "yarn jest \"viewer/.*-test.js\"",
53
- "unit-flow": "yarn jest \"flow-report/.*-test.[tj]s[x]?\"",
54
- "unit": "yarn jest",
55
- "unit:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run jest --ci .",
48
+ "unit-core": "yarn mocha lighthouse-core",
49
+ "unit-cli": "yarn mocha --testMatch lighthouse-cli/**/*-test.js",
50
+ "unit-report": "yarn mocha --testMatch report/**/*-test.js",
51
+ "unit-treemap": "yarn mocha --testMatch treemap/**/*-test.js",
52
+ "unit-viewer": "yarn mocha --testMatch viewer/**/*-test.js",
53
+ "unit-flow": "bash flow-report/test/run-flow-report-tests.sh",
54
+ "unit": "yarn unit-flow && yarn mocha",
55
+ "unit:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit --forbid-only",
56
56
  "core-unit": "yarn unit-core",
57
57
  "cli-unit": "yarn unit-cli",
58
58
  "viewer-unit": "yarn unit-viewer",
@@ -96,6 +96,8 @@
96
96
  },
97
97
  "devDependencies": {
98
98
  "@build-tracker/cli": "^1.0.0-beta.15",
99
+ "@esbuild-kit/esm-loader": "^2.1.1",
100
+ "@jest/fake-timers": "^28.1.0",
99
101
  "@rollup/plugin-alias": "^3.1.2",
100
102
  "@rollup/plugin-commonjs": "^20.0.0",
101
103
  "@rollup/plugin-dynamic-import-vars": "^1.1.1",
@@ -103,7 +105,7 @@
103
105
  "@rollup/plugin-node-resolve": "^13.0.4",
104
106
  "@rollup/plugin-typescript": "^8.2.5",
105
107
  "@stadtlandnetz/rollup-plugin-postprocess": "^1.1.0",
106
- "@testing-library/preact": "^2.0.1",
108
+ "@testing-library/preact": "^3.1.1",
107
109
  "@testing-library/preact-hooks": "^1.1.0",
108
110
  "@types/archiver": "^2.1.2",
109
111
  "@types/chrome": "^0.0.154",
@@ -113,10 +115,10 @@
113
115
  "@types/estree": "^0.0.50",
114
116
  "@types/gh-pages": "^2.0.0",
115
117
  "@types/google.analytics": "0.0.39",
116
- "@types/jest": "^27.0.3",
117
118
  "@types/jpeg-js": "^0.3.7",
118
119
  "@types/jsdom": "^16.2.13",
119
120
  "@types/lodash": "^4.14.178",
121
+ "@types/mocha": "^9.0.0",
120
122
  "@types/node": "*",
121
123
  "@types/pako": "^1.0.1",
122
124
  "@types/resize-observer-browser": "^0.1.1",
@@ -130,7 +132,7 @@
130
132
  "acorn": "^8.5.0",
131
133
  "angular": "^1.7.4",
132
134
  "archiver": "^3.0.0",
133
- "c8": "^7.4.0",
135
+ "c8": "^7.11.3",
134
136
  "chalk": "^2.4.1",
135
137
  "chrome-devtools-frontend": "1.0.1012379",
136
138
  "concurrently": "^6.4.0",
@@ -146,22 +148,25 @@
146
148
  "eslint-plugin-import": "^2.25.3",
147
149
  "eslint-plugin-local-rules": "1.1.0",
148
150
  "event-target-shim": "^6.0.2",
151
+ "expect": "^28.1.0",
149
152
  "firebase": "^9.0.2",
150
153
  "gh-pages": "^2.0.1",
151
154
  "glob": "^7.1.3",
152
155
  "idb-keyval": "2.2.0",
153
156
  "intl-messageformat-parser": "^1.8.1",
154
- "jest": "27.1.1",
157
+ "jest-mock": "^27.3.0",
158
+ "jest-snapshot": "^28.1.0",
155
159
  "jsdom": "^12.2.0",
156
160
  "jsonld": "^5.2.0",
157
161
  "jsonlint-mod": "^1.7.6",
158
162
  "lighthouse-plugin-publisher-ads": "^1.5.4",
159
163
  "magic-string": "^0.25.7",
160
164
  "mime-types": "^2.1.30",
165
+ "mocha": "^10.0.0",
161
166
  "node-fetch": "^2.6.1",
162
167
  "npm-run-posix-or-windows": "^2.0.2",
163
168
  "pako": "^2.0.3",
164
- "preact": "^10.5.14",
169
+ "preact": "^10.7.2",
165
170
  "pretty-json-stringify": "^0.0.2",
166
171
  "puppeteer": "13.7.0",
167
172
  "resolve": "^1.20.0",
@@ -173,7 +178,7 @@
173
178
  "rollup-plugin-terser": "^7.0.2",
174
179
  "tabulator-tables": "^4.9.3",
175
180
  "terser": "^5.3.8",
176
- "ts-jest": "^27.0.4",
181
+ "testdouble": "^3.16.5",
177
182
  "typed-query-selector": "^2.6.1",
178
183
  "typescript": "^4.7.3",
179
184
  "wait-for-expect": "^3.0.2",