lighthouse 9.5.0-dev.20221018 → 9.5.0-dev.20221020
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.
- package/core/api.js +14 -10
- package/core/audits/accessibility/axe-audit.js +1 -1
- package/core/audits/accessibility/bypass.js +1 -0
- package/core/audits/accessibility/th-has-data-cells.js +1 -0
- package/core/audits/accessibility/video-caption.js +1 -0
- package/core/audits/autocomplete.js +3 -3
- package/core/audits/bootup-time.js +5 -5
- package/core/audits/byte-efficiency/duplicated-javascript.js +1 -1
- package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
- package/core/audits/byte-efficiency/total-byte-weight.js +2 -2
- package/core/audits/byte-efficiency/uses-long-cache-ttl.js +3 -3
- package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +4 -4
- package/core/audits/csp-xss.js +3 -3
- package/core/audits/deprecations.js +2 -2
- package/core/audits/dobetterweb/dom-size.js +3 -3
- package/core/audits/dobetterweb/geolocation-on-start.js +1 -1
- package/core/audits/dobetterweb/inspector-issues.js +1 -1
- package/core/audits/dobetterweb/js-libraries.js +2 -2
- package/core/audits/dobetterweb/no-document-write.js +1 -1
- package/core/audits/dobetterweb/notification-on-start.js +1 -1
- package/core/audits/dobetterweb/password-inputs-can-be-pasted-into.js +1 -1
- package/core/audits/dobetterweb/uses-http2.js +2 -2
- package/core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
- package/core/audits/errors-in-console.js +4 -2
- package/core/audits/font-display.js +2 -2
- package/core/audits/image-aspect-ratio.js +4 -4
- package/core/audits/image-size-responsive.js +5 -5
- package/core/audits/installable-manifest.js +1 -1
- package/core/audits/is-on-https.js +2 -2
- package/core/audits/largest-contentful-paint-element.js +1 -1
- package/core/audits/layout-shift-elements.js +3 -3
- package/core/audits/lcp-lazy-loaded.js +1 -1
- package/core/audits/long-tasks.js +3 -3
- package/core/audits/main-thread-tasks.js +2 -2
- package/core/audits/mainthread-work-breakdown.js +4 -2
- package/core/audits/network-requests.js +11 -11
- package/core/audits/network-rtt.js +2 -2
- package/core/audits/network-server-latency.js +3 -3
- package/core/audits/no-unload-listeners.js +1 -1
- package/core/audits/non-composited-animations.js +2 -2
- package/core/audits/performance-budget.js +5 -5
- package/core/audits/preload-fonts.js +1 -1
- package/core/audits/resource-summary.js +3 -3
- package/core/audits/seo/crawlable-anchors.js +2 -2
- package/core/audits/seo/font-size.js +4 -4
- package/core/audits/seo/hreflang.js +3 -3
- package/core/audits/seo/is-crawlable.js +1 -1
- package/core/audits/seo/link-text.js +2 -2
- package/core/audits/seo/plugins.js +1 -1
- package/core/audits/seo/robots-txt.js +3 -3
- package/core/audits/seo/tap-targets.js +3 -3
- package/core/audits/themed-omnibox.js +6 -14
- package/core/audits/third-party-facades.js +3 -3
- package/core/audits/third-party-summary.js +3 -3
- package/core/audits/timing-budget.js +3 -3
- package/core/audits/unsized-images.js +2 -2
- package/core/audits/user-timings.js +6 -6
- package/core/audits/valid-source-maps.js +3 -3
- package/core/audits/work-during-interaction.js +6 -6
- package/core/gather/navigation-runner.js +3 -3
- package/core/gather/snapshot-runner.js +4 -3
- package/core/gather/timespan-runner.js +4 -3
- package/core/index.js +1 -1
- package/core/legacy/config/config.js +0 -26
- package/core/lib/bfcache-strings.js +655 -0
- package/core/lib/deprecations-strings.js +3 -2
- package/core/lib/manifest-parser.js +0 -16
- package/core/user-flow.js +135 -77
- package/core/util.cjs +27 -0
- package/dist/report/bundle.esm.js +38 -73
- package/dist/report/flow.js +2 -2
- package/dist/report/standalone.js +2 -2
- package/package.json +2 -3
- package/report/renderer/details-renderer.js +12 -73
- package/report/renderer/util.js +27 -0
- package/report/test/generator/report-generator-test.js +1 -1
- package/report/test/renderer/category-renderer-test.js +1 -1
- package/report/test/renderer/details-renderer-test.js +38 -38
- package/report/test/renderer/util-test.js +25 -0
- package/shared/localization/locales/en-US.json +352 -1
- package/shared/localization/locales/en-XL.json +352 -1
- package/types/lhr/audit-details.d.ts +4 -34
- package/types/user-flow.d.ts +10 -3
|
@@ -4,8 +4,6 @@
|
|
|
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
|
-
import cssParsers from 'cssstyle/lib/parsers.js';
|
|
8
|
-
|
|
9
7
|
const ALLOWED_DISPLAY_VALUES = [
|
|
10
8
|
'fullscreen',
|
|
11
9
|
'standalone',
|
|
@@ -29,14 +27,6 @@ const ALLOWED_ORIENTATION_VALUES = [
|
|
|
29
27
|
'landscape-secondary',
|
|
30
28
|
];
|
|
31
29
|
|
|
32
|
-
/**
|
|
33
|
-
* @param {string} color
|
|
34
|
-
* @return {boolean}
|
|
35
|
-
*/
|
|
36
|
-
function isValidColor(color) {
|
|
37
|
-
return cssParsers.valueType(color) === cssParsers.TYPES.COLOR;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
30
|
/**
|
|
41
31
|
* @param {*} raw
|
|
42
32
|
* @param {boolean=} trim
|
|
@@ -72,12 +62,6 @@ function parseColor(raw) {
|
|
|
72
62
|
return color;
|
|
73
63
|
}
|
|
74
64
|
|
|
75
|
-
// Use color parser to check CSS3 Color parsing.
|
|
76
|
-
if (!isValidColor(color.raw)) {
|
|
77
|
-
color.value = undefined;
|
|
78
|
-
color.warning = 'ERROR: color parsing failed.';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
65
|
return color;
|
|
82
66
|
}
|
|
83
67
|
|
package/core/user-flow.js
CHANGED
|
@@ -10,22 +10,56 @@ import {startTimespanGather} from './gather/timespan-runner.js';
|
|
|
10
10
|
import {navigationGather} from './gather/navigation-runner.js';
|
|
11
11
|
import {Runner} from './runner.js';
|
|
12
12
|
import {initializeConfig} from './config/config.js';
|
|
13
|
+
import {getFormatted} from '../shared/localization/format.js';
|
|
14
|
+
import * as i18n from './lib/i18n/i18n.js';
|
|
13
15
|
|
|
14
|
-
/** @typedef {Parameters<snapshotGather>[0]} FrOptions */
|
|
15
|
-
/** @typedef {Omit<FrOptions, 'page'> & {name?: string}} UserFlowOptions */
|
|
16
|
-
/** @typedef {Omit<FrOptions, 'page'> & {stepName?: string}} StepOptions */
|
|
17
16
|
/** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */
|
|
18
17
|
|
|
18
|
+
const UIStrings = {
|
|
19
|
+
/**
|
|
20
|
+
* @description Default name for a user flow on the given url. "User flow" refers to the series of page navigations and user interactions being tested on the page.
|
|
21
|
+
* @example {https://example.com} url
|
|
22
|
+
*/
|
|
23
|
+
defaultFlowName: 'User flow ({url})',
|
|
24
|
+
/**
|
|
25
|
+
* @description Default name for a user flow step that analyzes a page navigation.
|
|
26
|
+
* @example {https://example.com} url
|
|
27
|
+
*/
|
|
28
|
+
defaultNavigationName: 'Navigation report ({url})',
|
|
29
|
+
/**
|
|
30
|
+
* @description Default name for a user flow step that analyzes user interactions over a period of time.
|
|
31
|
+
* @example {https://example.com} url
|
|
32
|
+
*/
|
|
33
|
+
defaultTimespanName: 'Timespan report ({url})',
|
|
34
|
+
/**
|
|
35
|
+
* @description Default name for a user flow step that analyzes the page state at a point in time.
|
|
36
|
+
* @example {https://example.com} url
|
|
37
|
+
*/
|
|
38
|
+
defaultSnapshotName: 'Snapshot report ({url})',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} message
|
|
45
|
+
* @param {Record<string, string | number>} values
|
|
46
|
+
* @param {LH.Locale} locale
|
|
47
|
+
*/
|
|
48
|
+
function translate(message, values, locale) {
|
|
49
|
+
const icuMessage = str_(message, values);
|
|
50
|
+
return getFormatted(icuMessage, locale);
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
class UserFlow {
|
|
20
54
|
/**
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {
|
|
55
|
+
* @param {LH.Puppeteer.Page} page
|
|
56
|
+
* @param {LH.UserFlow.Options} [options]
|
|
23
57
|
*/
|
|
24
58
|
constructor(page, options) {
|
|
25
|
-
/** @type {
|
|
26
|
-
this.
|
|
27
|
-
/** @type {
|
|
28
|
-
this.
|
|
59
|
+
/** @type {LH.Puppeteer.Page} */
|
|
60
|
+
this._page = page;
|
|
61
|
+
/** @type {LH.UserFlow.Options|undefined} */
|
|
62
|
+
this._options = options;
|
|
29
63
|
/** @type {LH.UserFlow.GatherStep[]} */
|
|
30
64
|
this._gatherSteps = [];
|
|
31
65
|
/** @type {GatherStepRunnerOptions} */
|
|
@@ -33,67 +67,37 @@ class UserFlow {
|
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
/**
|
|
36
|
-
* @param {
|
|
37
|
-
* @
|
|
38
|
-
*/
|
|
39
|
-
_shortenUrl(longUrl) {
|
|
40
|
-
const url = new URL(longUrl);
|
|
41
|
-
return `${url.hostname}${url.pathname}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {LH.Artifacts} artifacts
|
|
46
|
-
* @return {string}
|
|
47
|
-
*/
|
|
48
|
-
_getDefaultStepName(artifacts) {
|
|
49
|
-
const shortUrl = this._shortenUrl(artifacts.URL.finalDisplayedUrl);
|
|
50
|
-
switch (artifacts.GatherContext.gatherMode) {
|
|
51
|
-
case 'navigation':
|
|
52
|
-
return `Navigation report (${shortUrl})`;
|
|
53
|
-
case 'timespan':
|
|
54
|
-
return `Timespan report (${shortUrl})`;
|
|
55
|
-
case 'snapshot':
|
|
56
|
-
return `Snapshot report (${shortUrl})`;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {StepOptions=} stepOptions
|
|
70
|
+
* @param {LH.UserFlow.StepFlags} [flags]
|
|
71
|
+
* @return {LH.UserFlow.StepFlags}
|
|
62
72
|
*/
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const flags = {...options.flags};
|
|
73
|
+
_getNextNavigationFlags(flags) {
|
|
74
|
+
const newStepFlags = {...flags};
|
|
66
75
|
|
|
67
|
-
if (
|
|
68
|
-
|
|
76
|
+
if (newStepFlags.skipAboutBlank === undefined) {
|
|
77
|
+
newStepFlags.skipAboutBlank = true;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
// On repeat navigations, we want to disable storage reset by default (i.e. it's not a cold load).
|
|
72
81
|
const isSubsequentNavigation = this._gatherSteps
|
|
73
82
|
.some(step => step.artifacts.GatherContext.gatherMode === 'navigation');
|
|
74
83
|
if (isSubsequentNavigation) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
84
|
+
if (newStepFlags.disableStorageReset === undefined) {
|
|
85
|
+
newStepFlags.disableStorageReset = true;
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return options;
|
|
89
|
+
return newStepFlags;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/**
|
|
86
93
|
*
|
|
87
94
|
* @param {LH.Gatherer.FRGatherResult} gatherResult
|
|
88
|
-
* @param {
|
|
95
|
+
* @param {LH.UserFlow.StepFlags} [flags]
|
|
89
96
|
*/
|
|
90
|
-
_addGatherStep(gatherResult,
|
|
91
|
-
const providedName = options?.stepName;
|
|
97
|
+
_addGatherStep(gatherResult, flags) {
|
|
92
98
|
const gatherStep = {
|
|
93
99
|
artifacts: gatherResult.artifacts,
|
|
94
|
-
|
|
95
|
-
config: options.config,
|
|
96
|
-
flags: options.flags,
|
|
100
|
+
flags,
|
|
97
101
|
};
|
|
98
102
|
this._gatherSteps.push(gatherStep);
|
|
99
103
|
this._gatherStepRunnerOptions.set(gatherStep, gatherResult.runnerOptions);
|
|
@@ -101,23 +105,26 @@ class UserFlow {
|
|
|
101
105
|
|
|
102
106
|
/**
|
|
103
107
|
* @param {LH.NavigationRequestor} requestor
|
|
104
|
-
* @param {
|
|
108
|
+
* @param {LH.UserFlow.StepFlags} [flags]
|
|
105
109
|
*/
|
|
106
|
-
async navigate(requestor,
|
|
110
|
+
async navigate(requestor, flags) {
|
|
107
111
|
if (this.currentTimespan) throw new Error('Timespan already in progress');
|
|
108
112
|
if (this.currentNavigation) throw new Error('Navigation already in progress');
|
|
109
113
|
|
|
110
|
-
const
|
|
111
|
-
const gatherResult = await navigationGather(requestor,
|
|
114
|
+
const newStepFlags = this._getNextNavigationFlags(flags);
|
|
115
|
+
const gatherResult = await navigationGather(this._page, requestor, {
|
|
116
|
+
config: this._options?.config,
|
|
117
|
+
flags: newStepFlags,
|
|
118
|
+
});
|
|
112
119
|
|
|
113
|
-
this._addGatherStep(gatherResult,
|
|
120
|
+
this._addGatherStep(gatherResult, newStepFlags);
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
/**
|
|
117
124
|
* This is an alternative to `navigate()` that can be used to analyze a navigation triggered by user interaction.
|
|
118
125
|
* For more on user triggered navigations, see https://github.com/GoogleChrome/lighthouse/blob/main/docs/user-flows.md#triggering-a-navigation-via-user-interactions.
|
|
119
126
|
*
|
|
120
|
-
* @param {
|
|
127
|
+
* @param {LH.UserFlow.StepFlags} [stepOptions]
|
|
121
128
|
*/
|
|
122
129
|
async startNavigation(stepOptions) {
|
|
123
130
|
/** @type {(value: () => void) => void} */
|
|
@@ -166,39 +173,43 @@ class UserFlow {
|
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
/**
|
|
169
|
-
* @param {
|
|
176
|
+
* @param {LH.UserFlow.StepFlags} [flags]
|
|
170
177
|
*/
|
|
171
|
-
async startTimespan(
|
|
178
|
+
async startTimespan(flags) {
|
|
172
179
|
if (this.currentTimespan) throw new Error('Timespan already in progress');
|
|
173
180
|
if (this.currentNavigation) throw new Error('Navigation already in progress');
|
|
174
181
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
const timespan = await startTimespanGather(this._page, {
|
|
183
|
+
config: this._options?.config,
|
|
184
|
+
flags: flags,
|
|
185
|
+
});
|
|
186
|
+
this.currentTimespan = {timespan, flags};
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
async endTimespan() {
|
|
181
190
|
if (!this.currentTimespan) throw new Error('No timespan in progress');
|
|
182
191
|
if (this.currentNavigation) throw new Error('Navigation already in progress');
|
|
183
192
|
|
|
184
|
-
const {timespan,
|
|
193
|
+
const {timespan, flags} = this.currentTimespan;
|
|
185
194
|
const gatherResult = await timespan.endTimespanGather();
|
|
186
195
|
this.currentTimespan = undefined;
|
|
187
196
|
|
|
188
|
-
this._addGatherStep(gatherResult,
|
|
197
|
+
this._addGatherStep(gatherResult, flags);
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
/**
|
|
192
|
-
* @param {
|
|
201
|
+
* @param {LH.UserFlow.StepFlags} [flags]
|
|
193
202
|
*/
|
|
194
|
-
async snapshot(
|
|
203
|
+
async snapshot(flags) {
|
|
195
204
|
if (this.currentTimespan) throw new Error('Timespan already in progress');
|
|
196
205
|
if (this.currentNavigation) throw new Error('Navigation already in progress');
|
|
197
206
|
|
|
198
|
-
const
|
|
199
|
-
|
|
207
|
+
const gatherResult = await snapshotGather(this._page, {
|
|
208
|
+
config: this._options?.config,
|
|
209
|
+
flags: flags,
|
|
210
|
+
});
|
|
200
211
|
|
|
201
|
-
this._addGatherStep(gatherResult,
|
|
212
|
+
this._addGatherStep(gatherResult, flags);
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
/**
|
|
@@ -206,8 +217,8 @@ class UserFlow {
|
|
|
206
217
|
*/
|
|
207
218
|
async createFlowResult() {
|
|
208
219
|
return auditGatherSteps(this._gatherSteps, {
|
|
209
|
-
name: this.name,
|
|
210
|
-
config: this.
|
|
220
|
+
name: this._options?.name,
|
|
221
|
+
config: this._options?.config,
|
|
211
222
|
gatherStepRunnerOptions: this._gatherStepRunnerOptions,
|
|
212
223
|
});
|
|
213
224
|
}
|
|
@@ -226,11 +237,56 @@ class UserFlow {
|
|
|
226
237
|
createArtifactsJson() {
|
|
227
238
|
return {
|
|
228
239
|
gatherSteps: this._gatherSteps,
|
|
229
|
-
name: this.name,
|
|
240
|
+
name: this._options?.name,
|
|
230
241
|
};
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
|
|
245
|
+
/**
|
|
246
|
+
* @param {string} longUrl
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
function shortenUrl(longUrl) {
|
|
250
|
+
const url = new URL(longUrl);
|
|
251
|
+
return `${url.hostname}${url.pathname}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {LH.UserFlow.StepFlags|undefined} flags
|
|
256
|
+
* @param {LH.Artifacts} artifacts
|
|
257
|
+
* @return {string}
|
|
258
|
+
*/
|
|
259
|
+
function getStepName(flags, artifacts) {
|
|
260
|
+
if (flags?.name) return flags.name;
|
|
261
|
+
|
|
262
|
+
const {locale} = artifacts.settings;
|
|
263
|
+
const shortUrl = shortenUrl(artifacts.URL.finalDisplayedUrl);
|
|
264
|
+
switch (artifacts.GatherContext.gatherMode) {
|
|
265
|
+
case 'navigation':
|
|
266
|
+
return translate(UIStrings.defaultNavigationName, {url: shortUrl}, locale);
|
|
267
|
+
case 'timespan':
|
|
268
|
+
return translate(UIStrings.defaultTimespanName, {url: shortUrl}, locale);
|
|
269
|
+
case 'snapshot':
|
|
270
|
+
return translate(UIStrings.defaultSnapshotName, {url: shortUrl}, locale);
|
|
271
|
+
default:
|
|
272
|
+
throw new Error('Unsupported gather mode');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {string|undefined} name
|
|
278
|
+
* @param {LH.UserFlow.GatherStep[]} gatherSteps
|
|
279
|
+
* @return {string}
|
|
280
|
+
*/
|
|
281
|
+
function getFlowName(name, gatherSteps) {
|
|
282
|
+
if (name) return name;
|
|
283
|
+
|
|
284
|
+
const firstArtifacts = gatherSteps[0].artifacts;
|
|
285
|
+
const {locale} = firstArtifacts.settings;
|
|
286
|
+
const url = new URL(firstArtifacts.URL.finalDisplayedUrl).hostname;
|
|
287
|
+
return translate(UIStrings.defaultFlowName, {url}, locale);
|
|
288
|
+
}
|
|
289
|
+
|
|
234
290
|
/**
|
|
235
291
|
* @param {Array<LH.UserFlow.GatherStep>} gatherSteps
|
|
236
292
|
* @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
|
|
@@ -243,14 +299,15 @@ async function auditGatherSteps(gatherSteps, options) {
|
|
|
243
299
|
/** @type {LH.FlowResult['steps']} */
|
|
244
300
|
const steps = [];
|
|
245
301
|
for (const gatherStep of gatherSteps) {
|
|
246
|
-
const {artifacts,
|
|
302
|
+
const {artifacts, flags} = gatherStep;
|
|
303
|
+
const name = getStepName(flags, artifacts);
|
|
247
304
|
|
|
248
305
|
let runnerOptions = options.gatherStepRunnerOptions?.get(gatherStep);
|
|
249
306
|
|
|
250
307
|
// If the gather step is not active, we must recreate the runner options.
|
|
251
308
|
if (!runnerOptions) {
|
|
252
309
|
// Step specific configs take precedence over a config for the entire flow.
|
|
253
|
-
const configJson =
|
|
310
|
+
const configJson = options.config;
|
|
254
311
|
const {gatherMode} = artifacts.GatherContext;
|
|
255
312
|
const {config} = await initializeConfig(gatherMode, configJson, flags);
|
|
256
313
|
runnerOptions = {
|
|
@@ -264,13 +321,14 @@ async function auditGatherSteps(gatherSteps, options) {
|
|
|
264
321
|
steps.push({lhr: result.lhr, name});
|
|
265
322
|
}
|
|
266
323
|
|
|
267
|
-
|
|
268
|
-
const flowName = options.name || `User flow (${url.hostname})`;
|
|
269
|
-
return {steps, name: flowName};
|
|
324
|
+
return {steps, name: getFlowName(options.name, gatherSteps)};
|
|
270
325
|
}
|
|
271
326
|
|
|
272
327
|
|
|
273
328
|
export {
|
|
274
329
|
UserFlow,
|
|
275
330
|
auditGatherSteps,
|
|
331
|
+
getStepName,
|
|
332
|
+
getFlowName,
|
|
333
|
+
UIStrings,
|
|
276
334
|
};
|
package/core/util.cjs
CHANGED
|
@@ -127,6 +127,33 @@ class Util {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
// Circa 10.0, table items were refactored.
|
|
132
|
+
if (audit.details.type === 'table') {
|
|
133
|
+
for (const heading of audit.details.headings) {
|
|
134
|
+
/** @type {{itemType: LH.Audit.Details.ItemValueType|undefined, text: string|undefined}} */
|
|
135
|
+
// @ts-expect-error
|
|
136
|
+
const {itemType, text} = heading;
|
|
137
|
+
if (itemType !== undefined) {
|
|
138
|
+
heading.valueType = itemType;
|
|
139
|
+
// @ts-expect-error
|
|
140
|
+
delete heading.itemType;
|
|
141
|
+
}
|
|
142
|
+
if (text !== undefined) {
|
|
143
|
+
heading.label = text;
|
|
144
|
+
// @ts-expect-error
|
|
145
|
+
delete heading.text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// @ts-expect-error
|
|
149
|
+
const subItemsItemType = heading.subItemsHeading?.itemType;
|
|
150
|
+
if (heading.subItemsHeading && subItemsItemType !== undefined) {
|
|
151
|
+
heading.subItemsHeading.valueType = subItemsItemType;
|
|
152
|
+
// @ts-expect-error
|
|
153
|
+
delete heading.subItemsHeading.itemType;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
130
157
|
}
|
|
131
158
|
}
|
|
132
159
|
|
|
@@ -123,6 +123,33 @@ class Util {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
// Circa 10.0, table items were refactored.
|
|
128
|
+
if (audit.details.type === 'table') {
|
|
129
|
+
for (const heading of audit.details.headings) {
|
|
130
|
+
/** @type {{itemType: LH.Audit.Details.ItemValueType|undefined, text: string|undefined}} */
|
|
131
|
+
// @ts-expect-error
|
|
132
|
+
const {itemType, text} = heading;
|
|
133
|
+
if (itemType !== undefined) {
|
|
134
|
+
heading.valueType = itemType;
|
|
135
|
+
// @ts-expect-error
|
|
136
|
+
delete heading.itemType;
|
|
137
|
+
}
|
|
138
|
+
if (text !== undefined) {
|
|
139
|
+
heading.label = text;
|
|
140
|
+
// @ts-expect-error
|
|
141
|
+
delete heading.text;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// @ts-expect-error
|
|
145
|
+
const subItemsItemType = heading.subItemsHeading?.itemType;
|
|
146
|
+
if (heading.subItemsHeading && subItemsItemType !== undefined) {
|
|
147
|
+
heading.subItemsHeading.valueType = subItemsItemType;
|
|
148
|
+
// @ts-expect-error
|
|
149
|
+
delete heading.subItemsHeading.itemType;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
|
|
@@ -2893,11 +2920,10 @@ class DetailsRenderer {
|
|
|
2893
2920
|
case 'list':
|
|
2894
2921
|
return this._renderList(details);
|
|
2895
2922
|
case 'table':
|
|
2923
|
+
case 'opportunity':
|
|
2896
2924
|
return this._renderTable(details);
|
|
2897
2925
|
case 'criticalrequestchain':
|
|
2898
2926
|
return CriticalRequestChainRenderer.render(this._dom, details, this);
|
|
2899
|
-
case 'opportunity':
|
|
2900
|
-
return this._renderTable(details);
|
|
2901
2927
|
|
|
2902
2928
|
// Internal-only details, not for rendering.
|
|
2903
2929
|
case 'screenshot':
|
|
@@ -3055,7 +3081,7 @@ class DetailsRenderer {
|
|
|
3055
3081
|
* based on the heading's valueType, unless the value itself has a `type`
|
|
3056
3082
|
* property to override it.
|
|
3057
3083
|
* @param {TableItemValue} value
|
|
3058
|
-
* @param {LH.Audit.Details.
|
|
3084
|
+
* @param {LH.Audit.Details.TableColumnHeading} heading
|
|
3059
3085
|
* @return {Element|null}
|
|
3060
3086
|
*/
|
|
3061
3087
|
_renderTableValue(value, heading) {
|
|
@@ -3140,73 +3166,14 @@ class DetailsRenderer {
|
|
|
3140
3166
|
}
|
|
3141
3167
|
}
|
|
3142
3168
|
|
|
3143
|
-
/**
|
|
3144
|
-
* Get the headings of a table-like details object, converted into the
|
|
3145
|
-
* OpportunityColumnHeading type until we have all details use the same
|
|
3146
|
-
* heading format.
|
|
3147
|
-
* @param {Table|OpportunityTable} tableLike
|
|
3148
|
-
* @return {OpportunityTable['headings']}
|
|
3149
|
-
*/
|
|
3150
|
-
_getCanonicalizedHeadingsFromTable(tableLike) {
|
|
3151
|
-
if (tableLike.type === 'opportunity') {
|
|
3152
|
-
return tableLike.headings;
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
return tableLike.headings.map(heading => this._getCanonicalizedHeading(heading));
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
/**
|
|
3159
|
-
* Get the headings of a table-like details object, converted into the
|
|
3160
|
-
* OpportunityColumnHeading type until we have all details use the same
|
|
3161
|
-
* heading format.
|
|
3162
|
-
* @param {Table['headings'][number]} heading
|
|
3163
|
-
* @return {OpportunityTable['headings'][number]}
|
|
3164
|
-
*/
|
|
3165
|
-
_getCanonicalizedHeading(heading) {
|
|
3166
|
-
let subItemsHeading;
|
|
3167
|
-
if (heading.subItemsHeading) {
|
|
3168
|
-
subItemsHeading = this._getCanonicalizedsubItemsHeading(heading.subItemsHeading, heading);
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
return {
|
|
3172
|
-
key: heading.key,
|
|
3173
|
-
valueType: heading.itemType,
|
|
3174
|
-
subItemsHeading,
|
|
3175
|
-
label: heading.text,
|
|
3176
|
-
displayUnit: heading.displayUnit,
|
|
3177
|
-
granularity: heading.granularity,
|
|
3178
|
-
};
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
/**
|
|
3182
|
-
* @param {Exclude<LH.Audit.Details.TableColumnHeading['subItemsHeading'], undefined>} subItemsHeading
|
|
3183
|
-
* @param {LH.Audit.Details.TableColumnHeading} parentHeading
|
|
3184
|
-
* @return {LH.Audit.Details.OpportunityColumnHeading['subItemsHeading']}
|
|
3185
|
-
*/
|
|
3186
|
-
_getCanonicalizedsubItemsHeading(subItemsHeading, parentHeading) {
|
|
3187
|
-
// Low-friction way to prevent committing a falsy key (which is never allowed for
|
|
3188
|
-
// a subItemsHeading) from passing in CI.
|
|
3189
|
-
if (!subItemsHeading.key) {
|
|
3190
|
-
// eslint-disable-next-line no-console
|
|
3191
|
-
console.warn('key should not be null');
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3194
|
-
return {
|
|
3195
|
-
key: subItemsHeading.key || '',
|
|
3196
|
-
valueType: subItemsHeading.itemType || parentHeading.itemType,
|
|
3197
|
-
granularity: subItemsHeading.granularity || parentHeading.granularity,
|
|
3198
|
-
displayUnit: subItemsHeading.displayUnit || parentHeading.displayUnit,
|
|
3199
|
-
};
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
3169
|
/**
|
|
3203
3170
|
* Returns a new heading where the values are defined first by `heading.subItemsHeading`,
|
|
3204
3171
|
* and secondly by `heading`. If there is no subItemsHeading, returns null, which will
|
|
3205
3172
|
* be rendered as an empty column.
|
|
3206
|
-
* @param {LH.Audit.Details.
|
|
3207
|
-
* @return {LH.Audit.Details.
|
|
3173
|
+
* @param {LH.Audit.Details.TableColumnHeading} heading
|
|
3174
|
+
* @return {LH.Audit.Details.TableColumnHeading | null}
|
|
3208
3175
|
*/
|
|
3209
|
-
|
|
3176
|
+
_getDerivedSubItemsHeading(heading) {
|
|
3210
3177
|
if (!heading.subItemsHeading) return null;
|
|
3211
3178
|
return {
|
|
3212
3179
|
key: heading.subItemsHeading.key || '',
|
|
@@ -3219,7 +3186,7 @@ class DetailsRenderer {
|
|
|
3219
3186
|
|
|
3220
3187
|
/**
|
|
3221
3188
|
* @param {TableItem} item
|
|
3222
|
-
* @param {(LH.Audit.Details.
|
|
3189
|
+
* @param {(LH.Audit.Details.TableColumnHeading | null)[]} headings
|
|
3223
3190
|
*/
|
|
3224
3191
|
_renderTableRow(item, headings) {
|
|
3225
3192
|
const rowElem = this._dom.createElement('tr');
|
|
@@ -3256,7 +3223,7 @@ class DetailsRenderer {
|
|
|
3256
3223
|
* Renders one or more rows from a details table item. A single table item can
|
|
3257
3224
|
* expand into multiple rows, if there is a subItemsHeading.
|
|
3258
3225
|
* @param {TableItem} item
|
|
3259
|
-
* @param {LH.Audit.Details.
|
|
3226
|
+
* @param {LH.Audit.Details.TableColumnHeading[]} headings
|
|
3260
3227
|
*/
|
|
3261
3228
|
_renderTableRowsFromItem(item, headings) {
|
|
3262
3229
|
const fragment = this._dom.createFragment();
|
|
@@ -3264,7 +3231,7 @@ class DetailsRenderer {
|
|
|
3264
3231
|
|
|
3265
3232
|
if (!item.subItems) return fragment;
|
|
3266
3233
|
|
|
3267
|
-
const subItemsHeadings = headings.map(this.
|
|
3234
|
+
const subItemsHeadings = headings.map(this._getDerivedSubItemsHeading);
|
|
3268
3235
|
if (!subItemsHeadings.some(Boolean)) return fragment;
|
|
3269
3236
|
|
|
3270
3237
|
for (const subItem of item.subItems.items) {
|
|
@@ -3277,7 +3244,7 @@ class DetailsRenderer {
|
|
|
3277
3244
|
}
|
|
3278
3245
|
|
|
3279
3246
|
/**
|
|
3280
|
-
* @param {
|
|
3247
|
+
* @param {{headings: TableColumnHeading[], items: TableItem[]}} details
|
|
3281
3248
|
* @return {Element}
|
|
3282
3249
|
*/
|
|
3283
3250
|
_renderTable(details) {
|
|
@@ -3287,9 +3254,7 @@ class DetailsRenderer {
|
|
|
3287
3254
|
const theadElem = this._dom.createChildOf(tableElem, 'thead');
|
|
3288
3255
|
const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
|
|
3289
3256
|
|
|
3290
|
-
const
|
|
3291
|
-
|
|
3292
|
-
for (const heading of headings) {
|
|
3257
|
+
for (const heading of details.headings) {
|
|
3293
3258
|
const valueType = heading.valueType || 'text';
|
|
3294
3259
|
const classes = `lh-table-column--${valueType}`;
|
|
3295
3260
|
const labelEl = this._dom.createElement('div', 'lh-text');
|
|
@@ -3300,7 +3265,7 @@ class DetailsRenderer {
|
|
|
3300
3265
|
const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
|
|
3301
3266
|
let even = true;
|
|
3302
3267
|
for (const item of details.items) {
|
|
3303
|
-
const rowsFragment = this._renderTableRowsFromItem(item, headings);
|
|
3268
|
+
const rowsFragment = this._renderTableRowsFromItem(item, details.headings);
|
|
3304
3269
|
for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
|
|
3305
3270
|
// For zebra styling.
|
|
3306
3271
|
rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
|