lighthouse 8.6.0-dev.20211029 → 8.6.0-dev.20211102
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/dist/report/flow.js +14 -14
- package/dist/report/standalone.js +12 -12
- package/flow-report/.eslintrc.cjs +2 -0
- package/flow-report/assets/styles.css +1 -1
- package/flow-report/src/app.tsx +10 -11
- package/flow-report/src/common.tsx +22 -9
- package/flow-report/src/header.tsx +3 -3
- package/flow-report/src/i18n/i18n.tsx +10 -6
- package/flow-report/src/icons.tsx +21 -9
- package/flow-report/src/sidebar/flow.tsx +3 -3
- package/flow-report/src/sidebar/sidebar.tsx +12 -6
- package/flow-report/src/summary/category.tsx +9 -4
- package/flow-report/src/summary/summary.tsx +9 -3
- package/flow-report/src/util.ts +31 -19
- package/flow-report/src/wrappers/report-renderer.tsx +9 -3
- package/flow-report/src/wrappers/report.tsx +17 -7
- package/flow-report/test/common-test.tsx +25 -10
- package/flow-report/test/header-test.tsx +6 -6
- package/flow-report/test/setup/env-setup.ts +1 -1
- package/flow-report/test/summary/category-test.tsx +5 -5
- package/flow-report/test/util-test.tsx +32 -11
- package/lighthouse-cli/.eslintrc.cjs +2 -0
- package/lighthouse-cli/cli-flags.js +1 -1
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
- package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
- package/lighthouse-core/audits/audit.js +1 -1
- package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
- package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
- package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
- package/lighthouse-core/computed/resource-summary.js +1 -1
- package/lighthouse-core/config/config-helpers.js +20 -10
- package/lighthouse-core/config/default-config.js +59 -55
- package/lighthouse-core/fraggle-rock/config/default-config.js +1 -1
- package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
- package/lighthouse-core/gather/connections/cri.js +1 -1
- package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
- package/lighthouse-core/lib/asset-saver.js +4 -1
- package/lighthouse-core/lib/lh-env.js +2 -0
- package/lighthouse-core/lib/proto-preprocessor.js +14 -2
- package/lighthouse-core/lib/tappable-rects.js +8 -15
- package/lighthouse-core/lib/url-shim.js +1 -1
- package/lighthouse-core/runner.js +1 -1
- package/lighthouse-core/util-commonjs.js +25 -11
- package/package.json +11 -8
- package/report/.eslintrc.cjs +2 -0
- package/report/assets/styles.css +226 -250
- package/report/assets/templates.html +19 -29
- package/report/clients/standalone.js +0 -2
- package/report/renderer/category-renderer.js +21 -17
- package/report/renderer/components.js +21 -54
- package/report/renderer/crc-details-renderer.js +15 -12
- package/report/renderer/element-screenshot-renderer.js +2 -2
- package/report/renderer/open-tab.js +9 -3
- package/report/renderer/performance-category-renderer.js +51 -24
- package/report/renderer/report-renderer.js +2 -1
- package/report/renderer/report-ui-features.js +2 -3
- package/report/renderer/util.js +26 -11
- package/report/test/renderer/category-renderer-test.js +2 -2
- package/report/test/renderer/performance-category-renderer-test.js +35 -9
- package/report/test/renderer/report-renderer-test.js +1 -1
- package/report/test/renderer/report-ui-features-test.js +10 -9
- package/report/test/renderer/util-test.js +13 -31
- package/report/test-assets/faux-psi.js +5 -5
- package/shared/localization/format.js +9 -4
- package/shared/localization/locales/ar-XB.json +0 -15
- package/shared/localization/locales/ar.json +0 -15
- package/shared/localization/locales/bg.json +0 -15
- package/shared/localization/locales/ca.json +0 -15
- package/shared/localization/locales/cs.json +0 -15
- package/shared/localization/locales/da.json +0 -15
- package/shared/localization/locales/de.json +0 -15
- package/shared/localization/locales/el.json +0 -15
- package/shared/localization/locales/en-GB.json +0 -15
- package/shared/localization/locales/en-US.json +19 -16
- package/shared/localization/locales/en-XA.json +0 -15
- package/shared/localization/locales/en-XL.json +19 -16
- package/shared/localization/locales/es-419.json +0 -15
- package/shared/localization/locales/es.json +0 -15
- package/shared/localization/locales/fi.json +0 -15
- package/shared/localization/locales/fil.json +0 -15
- package/shared/localization/locales/fr.json +0 -15
- package/shared/localization/locales/he.json +0 -15
- package/shared/localization/locales/hi.json +0 -15
- package/shared/localization/locales/hr.json +0 -15
- package/shared/localization/locales/hu.json +0 -15
- package/shared/localization/locales/id.json +0 -15
- package/shared/localization/locales/it.json +0 -15
- package/shared/localization/locales/ja.json +0 -15
- package/shared/localization/locales/ko.json +0 -15
- package/shared/localization/locales/lt.json +0 -15
- package/shared/localization/locales/lv.json +0 -15
- package/shared/localization/locales/nl.json +0 -15
- package/shared/localization/locales/no.json +0 -15
- package/shared/localization/locales/pl.json +0 -15
- package/shared/localization/locales/pt-PT.json +0 -15
- package/shared/localization/locales/pt.json +0 -15
- package/shared/localization/locales/ro.json +0 -15
- package/shared/localization/locales/ru.json +0 -15
- package/shared/localization/locales/sk.json +0 -15
- package/shared/localization/locales/sl.json +0 -15
- package/shared/localization/locales/sr-Latn.json +0 -15
- package/shared/localization/locales/sr.json +0 -15
- package/shared/localization/locales/sv.json +0 -15
- package/shared/localization/locales/ta.json +0 -15
- package/shared/localization/locales/te.json +0 -15
- package/shared/localization/locales/th.json +0 -15
- package/shared/localization/locales/tr.json +0 -15
- package/shared/localization/locales/uk.json +0 -15
- package/shared/localization/locales/vi.json +0 -15
- package/shared/localization/locales/zh-HK.json +0 -15
- package/shared/localization/locales/zh-TW.json +0 -15
- package/shared/localization/locales/zh.json +0 -15
- package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
- package/types/lhr/flow.d.ts +3 -2
- package/types/rollup-plugin-postprocess.d.ts +10 -0
- package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
|
@@ -152,6 +152,21 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
152
152
|
return url.href;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* For performance, audits with no group should be a diagnostic or opportunity.
|
|
157
|
+
* The audit details type will determine which of the two groups an audit is in.
|
|
158
|
+
*
|
|
159
|
+
* @param {LH.ReportResult.AuditRef} audit
|
|
160
|
+
* @return {'load-opportunity'|'diagnostic'|null}
|
|
161
|
+
*/
|
|
162
|
+
_classifyPerformanceAudit(audit) {
|
|
163
|
+
if (audit.group) return null;
|
|
164
|
+
if (audit.result.details && audit.result.details.type === 'opportunity') {
|
|
165
|
+
return 'load-opportunity';
|
|
166
|
+
}
|
|
167
|
+
return 'diagnostic';
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
/**
|
|
156
171
|
* @param {LH.ReportResult.Category} category
|
|
157
172
|
* @param {Object<string, LH.Result.ReportGroup>} groups
|
|
@@ -168,15 +183,26 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
168
183
|
// Metrics.
|
|
169
184
|
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
|
|
170
185
|
if (metricAudits.length) {
|
|
171
|
-
const
|
|
186
|
+
const [metricsGroupEl, metricsFooterEl] = this.renderAuditGroup(groups.metrics);
|
|
172
187
|
|
|
173
188
|
// Metric descriptions toggle.
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
const checkboxEl = this.dom.createElement('input', 'lh-metrics-toggle__input');
|
|
190
|
+
const checkboxId = `lh-metrics-toggle${Util.getUniqueSuffix()}`;
|
|
191
|
+
checkboxEl.setAttribute('aria-label', 'Toggle the display of metric descriptions');
|
|
192
|
+
checkboxEl.type = 'checkbox';
|
|
193
|
+
checkboxEl.id = checkboxId;
|
|
194
|
+
metricsGroupEl.prepend(checkboxEl);
|
|
195
|
+
const metricHeaderEl = this.dom.find('.lh-audit-group__header', metricsGroupEl);
|
|
196
|
+
const labelEl = this.dom.createChildOf(metricHeaderEl, 'label', 'lh-metrics-toggle__label');
|
|
197
|
+
labelEl.htmlFor = checkboxId;
|
|
198
|
+
const showEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--show');
|
|
199
|
+
const hideEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--hide');
|
|
200
|
+
showEl.textContent = Util.i18n.strings.expandView;
|
|
201
|
+
hideEl.textContent = Util.i18n.strings.collapseView;
|
|
202
|
+
|
|
203
|
+
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
|
|
204
|
+
const metricsBoxesEl = this.dom.createElement('div', 'lh-metrics-container');
|
|
205
|
+
metricsGroupEl.insertBefore(metricsBoxesEl, metricsFooterEl);
|
|
180
206
|
metricAudits.forEach(item => {
|
|
181
207
|
metricsBoxesEl.appendChild(this._renderMetric(item));
|
|
182
208
|
});
|
|
@@ -193,8 +219,8 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
193
219
|
this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
|
|
194
220
|
|
|
195
221
|
|
|
196
|
-
|
|
197
|
-
element.appendChild(
|
|
222
|
+
metricsGroupEl.classList.add('lh-audit-group--metrics');
|
|
223
|
+
element.appendChild(metricsGroupEl);
|
|
198
224
|
}
|
|
199
225
|
|
|
200
226
|
// Filmstrip
|
|
@@ -209,10 +235,10 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
209
235
|
|
|
210
236
|
// Opportunities
|
|
211
237
|
const opportunityAudits = category.auditRefs
|
|
212
|
-
.filter(audit => audit
|
|
238
|
+
.filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
|
|
239
|
+
.filter(audit => !Util.showAsPassed(audit.result))
|
|
213
240
|
.sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
|
|
214
241
|
|
|
215
|
-
|
|
216
242
|
const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
|
|
217
243
|
// TODO: only add if there are opportunities & diagnostics rendered.
|
|
218
244
|
if (filterableMetrics.length) {
|
|
@@ -225,7 +251,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
225
251
|
const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
|
|
226
252
|
const maxWaste = Math.max(...wastedMsValues);
|
|
227
253
|
const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
|
|
228
|
-
const groupEl = this.renderAuditGroup(groups['load-opportunities']);
|
|
254
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
|
|
229
255
|
const tmpl = this.dom.createComponent('opportunityHeader');
|
|
230
256
|
|
|
231
257
|
this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
|
|
@@ -234,15 +260,17 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
234
260
|
strings.opportunitySavingsColumnLabel;
|
|
235
261
|
|
|
236
262
|
const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
|
|
237
|
-
groupEl.
|
|
238
|
-
opportunityAudits.forEach(item =>
|
|
263
|
+
groupEl.insertBefore(headerEl, footerEl);
|
|
264
|
+
opportunityAudits.forEach(item =>
|
|
265
|
+
groupEl.insertBefore(this._renderOpportunity(item, scale), footerEl));
|
|
239
266
|
groupEl.classList.add('lh-audit-group--load-opportunities');
|
|
240
267
|
element.appendChild(groupEl);
|
|
241
268
|
}
|
|
242
269
|
|
|
243
270
|
// Diagnostics
|
|
244
271
|
const diagnosticAudits = category.auditRefs
|
|
245
|
-
.filter(audit => audit
|
|
272
|
+
.filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
|
|
273
|
+
.filter(audit => !Util.showAsPassed(audit.result))
|
|
246
274
|
.sort((a, b) => {
|
|
247
275
|
const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
|
|
248
276
|
const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
|
|
@@ -250,16 +278,15 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
250
278
|
});
|
|
251
279
|
|
|
252
280
|
if (diagnosticAudits.length) {
|
|
253
|
-
const groupEl = this.renderAuditGroup(groups['diagnostics']);
|
|
254
|
-
diagnosticAudits.forEach(item => groupEl.
|
|
281
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
|
|
282
|
+
diagnosticAudits.forEach(item => groupEl.insertBefore(this.renderAudit(item), footerEl));
|
|
255
283
|
groupEl.classList.add('lh-audit-group--diagnostics');
|
|
256
284
|
element.appendChild(groupEl);
|
|
257
285
|
}
|
|
258
286
|
|
|
259
287
|
// Passed audits
|
|
260
288
|
const passedAudits = category.auditRefs
|
|
261
|
-
.filter(audit => (audit
|
|
262
|
-
Util.showAsPassed(audit.result));
|
|
289
|
+
.filter(audit => this._classifyPerformanceAudit(audit) && Util.showAsPassed(audit.result));
|
|
263
290
|
|
|
264
291
|
if (!passedAudits.length) return element;
|
|
265
292
|
|
|
@@ -279,16 +306,16 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
279
306
|
const table = this.detailsRenderer.render(audit.result.details);
|
|
280
307
|
if (table) {
|
|
281
308
|
table.id = id;
|
|
282
|
-
table.classList.add('lh-audit');
|
|
309
|
+
table.classList.add('lh-details', 'lh-details--budget', 'lh-audit');
|
|
283
310
|
budgetTableEls.push(table);
|
|
284
311
|
}
|
|
285
312
|
}
|
|
286
313
|
});
|
|
287
314
|
if (budgetTableEls.length > 0) {
|
|
288
|
-
const
|
|
289
|
-
budgetTableEls.forEach(table =>
|
|
290
|
-
|
|
291
|
-
element.appendChild(
|
|
315
|
+
const [groupEl, footerEl] = this.renderAuditGroup(groups.budgets);
|
|
316
|
+
budgetTableEls.forEach(table => groupEl.insertBefore(table, footerEl));
|
|
317
|
+
groupEl.classList.add('lh-audit-group--budgets');
|
|
318
|
+
element.appendChild(groupEl);
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
return element;
|
|
@@ -268,6 +268,7 @@ export class ReportRenderer {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
const scoreScale = this._dom.createElement('div');
|
|
271
|
+
scoreScale.classList.add('lh-scorescale-wrap');
|
|
271
272
|
scoreScale.append(this._dom.createComponent('scorescale'));
|
|
272
273
|
if (scoreHeader) {
|
|
273
274
|
const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
|
|
@@ -287,7 +288,7 @@ export class ReportRenderer {
|
|
|
287
288
|
for (const category of Object.values(report.categories)) {
|
|
288
289
|
const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
|
|
289
290
|
// .lh-category-wrapper is full-width and provides horizontal rules between categories.
|
|
290
|
-
// .lh-category within has the max-width: var(--report-width);
|
|
291
|
+
// .lh-category within has the max-width: var(--report-content-width);
|
|
291
292
|
const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
|
|
292
293
|
wrapper.appendChild(renderer.render(
|
|
293
294
|
category,
|
|
@@ -256,10 +256,9 @@ export class ReportUIFeatures {
|
|
|
256
256
|
const allThirdParty = thirdPartyRows.length === rowEls.length;
|
|
257
257
|
const allFirstParty = !thirdPartyRows.length;
|
|
258
258
|
|
|
259
|
-
// If all or none of the rows are 3rd party,
|
|
259
|
+
// If all or none of the rows are 3rd party, hide the control.
|
|
260
260
|
if (allThirdParty || allFirstParty) {
|
|
261
|
-
|
|
262
|
-
filterInput.checked = allThirdParty;
|
|
261
|
+
this._dom.find('div.lh-3p-filter', filterTemplate).hidden = true;
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
// Add checkbox to the DOM.
|
package/report/renderer/util.js
CHANGED
|
@@ -38,7 +38,11 @@ const listOfTlds = [
|
|
|
38
38
|
'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on',
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
class Util {
|
|
42
|
+
/** @type {I18n<typeof UIStrings>} */
|
|
43
|
+
// @ts-expect-error: Is set in report renderer.
|
|
44
|
+
static i18n = null;
|
|
45
|
+
|
|
42
46
|
static get PASS_THRESHOLD() {
|
|
43
47
|
return PASS_THRESHOLD;
|
|
44
48
|
}
|
|
@@ -417,7 +421,7 @@ export class Util {
|
|
|
417
421
|
throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
|
|
418
422
|
throttling.uploadThroughputKbps === 750 * 0.9;
|
|
419
423
|
};
|
|
420
|
-
summary =
|
|
424
|
+
summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
|
|
421
425
|
break;
|
|
422
426
|
}
|
|
423
427
|
case 'simulate': {
|
|
@@ -429,7 +433,7 @@ export class Util {
|
|
|
429
433
|
const isSlow4G = () => {
|
|
430
434
|
return rttMs === 150 && throughputKbps === 1.6 * 1024;
|
|
431
435
|
};
|
|
432
|
-
summary = isSlow4G() ?
|
|
436
|
+
summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
|
|
433
437
|
break;
|
|
434
438
|
}
|
|
435
439
|
default:
|
|
@@ -516,10 +520,9 @@ export class Util {
|
|
|
516
520
|
let totalWeight = 0;
|
|
517
521
|
for (const auditRef of category.auditRefs) {
|
|
518
522
|
const auditPassed = Util.showAsPassed(auditRef.result);
|
|
519
|
-
const notDisplayed = !auditRef.group && category.id === 'performance';
|
|
520
523
|
|
|
521
524
|
// Don't count the audit if it's manual, N/A, or isn't displayed.
|
|
522
|
-
if (
|
|
525
|
+
if (auditRef.group === 'hidden' ||
|
|
523
526
|
auditRef.result.scoreDisplayMode === 'manual' ||
|
|
524
527
|
auditRef.result.scoreDisplayMode === 'notApplicable') {
|
|
525
528
|
continue;
|
|
@@ -555,14 +558,10 @@ Util.getUniqueSuffix = (() => {
|
|
|
555
558
|
};
|
|
556
559
|
})();
|
|
557
560
|
|
|
558
|
-
/** @type {I18n<typeof Util['UIStrings']>} */
|
|
559
|
-
// @ts-expect-error: Is set in report renderer.
|
|
560
|
-
Util.i18n = null;
|
|
561
|
-
|
|
562
561
|
/**
|
|
563
562
|
* Report-renderer-specific strings.
|
|
564
563
|
*/
|
|
565
|
-
|
|
564
|
+
const UIStrings = {
|
|
566
565
|
/** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
|
|
567
566
|
varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
|
|
568
567
|
/** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
|
|
@@ -662,6 +661,22 @@ Util.UIStrings = {
|
|
|
662
661
|
|
|
663
662
|
/** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
|
|
664
663
|
throttlingProvided: 'Provided by environment',
|
|
664
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
665
|
+
show: 'Show',
|
|
666
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
667
|
+
hide: 'Hide',
|
|
668
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
669
|
+
expandView: 'Expand view',
|
|
670
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
671
|
+
collapseView: 'Collapse view',
|
|
672
|
+
/** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
|
|
673
|
+
runtimeSlow4g: 'Slow 4G throttling',
|
|
674
|
+
/** Label indicating that Lighthouse throttled the page using custom throttling settings. */
|
|
675
|
+
runtimeCustom: 'Custom throttling',
|
|
665
676
|
};
|
|
677
|
+
Util.UIStrings = UIStrings;
|
|
666
678
|
|
|
667
|
-
export
|
|
679
|
+
export {
|
|
680
|
+
Util,
|
|
681
|
+
UIStrings,
|
|
682
|
+
};
|
|
@@ -197,8 +197,8 @@ describe('CategoryRenderer', () => {
|
|
|
197
197
|
'score shows informative and dash icon');
|
|
198
198
|
|
|
199
199
|
assert.ok(pwaCategory.manualDescription);
|
|
200
|
-
const description = categoryDOM
|
|
201
|
-
.querySelector('.lh-
|
|
200
|
+
const description = categoryDOM.querySelector('.lh-clump--manual').closest('.lh-audit-group')
|
|
201
|
+
.querySelector('.lh-audit-group__description').textContent;
|
|
202
202
|
// may need to be adjusted if description includes a link at the beginning
|
|
203
203
|
assert.ok(description.startsWith(pwaCategory.manualDescription.substring(0, 20)),
|
|
204
204
|
'no manual description');
|
|
@@ -96,11 +96,23 @@ describe('PerfCategoryRenderer', () => {
|
|
|
96
96
|
assert.strictEqual(new URL(calcLink.href).hostname, 'googlechrome.github.io');
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it('ignores hidden audits', () => {
|
|
100
|
+
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
101
|
+
|
|
102
|
+
const hiddenAudits = category.auditRefs.filter(audit => audit.group === 'hidden');
|
|
103
|
+
const auditElements = [...categoryDOM.querySelectorAll('.lh-audit')];
|
|
104
|
+
const matchingElements = auditElements
|
|
105
|
+
.filter(el => hiddenAudits.some(audit => audit.id === el.id));
|
|
106
|
+
expect(matchingElements).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
99
109
|
it('renders the failing performance opportunities', () => {
|
|
100
110
|
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
101
111
|
|
|
102
|
-
const oppAudits = category.auditRefs.filter(audit =>
|
|
103
|
-
|
|
112
|
+
const oppAudits = category.auditRefs.filter(audit =>
|
|
113
|
+
audit.result.details &&
|
|
114
|
+
audit.result.details.type === 'opportunity' &&
|
|
115
|
+
!Util.showAsPassed(audit.result));
|
|
104
116
|
const oppElements = [...categoryDOM.querySelectorAll('.lh-audit--load-opportunity')];
|
|
105
117
|
expect(oppElements.map(e => e.id).sort()).toEqual(oppAudits.map(a => a.id).sort());
|
|
106
118
|
expect(oppElements.length).toBeGreaterThan(0);
|
|
@@ -120,10 +132,14 @@ describe('PerfCategoryRenderer', () => {
|
|
|
120
132
|
it('renders performance opportunities with an errorMessage', () => {
|
|
121
133
|
const auditWithError = {
|
|
122
134
|
score: 0,
|
|
123
|
-
group: 'load-opportunities',
|
|
124
135
|
result: {
|
|
125
136
|
score: null, scoreDisplayMode: 'error', errorMessage: 'Yikes!!', title: 'Bug #2',
|
|
126
137
|
description: '',
|
|
138
|
+
details: {
|
|
139
|
+
overallSavingsMs: 0,
|
|
140
|
+
items: [],
|
|
141
|
+
type: 'opportunity',
|
|
142
|
+
},
|
|
127
143
|
},
|
|
128
144
|
};
|
|
129
145
|
|
|
@@ -137,10 +153,14 @@ describe('PerfCategoryRenderer', () => {
|
|
|
137
153
|
it('renders performance opportunities\' explanation', () => {
|
|
138
154
|
const auditWithExplanation = {
|
|
139
155
|
score: 0,
|
|
140
|
-
group: 'load-opportunities',
|
|
141
156
|
result: {
|
|
142
157
|
score: 0, scoreDisplayMode: 'numeric',
|
|
143
158
|
numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2', description: '',
|
|
159
|
+
details: {
|
|
160
|
+
overallSavingsMs: 0,
|
|
161
|
+
items: [],
|
|
162
|
+
type: 'opportunity',
|
|
163
|
+
},
|
|
144
164
|
},
|
|
145
165
|
};
|
|
146
166
|
|
|
@@ -159,7 +179,9 @@ describe('PerfCategoryRenderer', () => {
|
|
|
159
179
|
'.lh-category > .lh-audit-group.lh-audit-group--diagnostics');
|
|
160
180
|
|
|
161
181
|
const diagnosticAuditIds = category.auditRefs.filter(audit => {
|
|
162
|
-
return audit.group
|
|
182
|
+
return !audit.group &&
|
|
183
|
+
!(audit.result.details && audit.result.details.type === 'opportunity') &&
|
|
184
|
+
!Util.showAsPassed(audit.result);
|
|
163
185
|
}).map(audit => audit.id).sort();
|
|
164
186
|
assert.ok(diagnosticAuditIds.length > 0);
|
|
165
187
|
|
|
@@ -170,11 +192,11 @@ describe('PerfCategoryRenderer', () => {
|
|
|
170
192
|
|
|
171
193
|
it('renders the passed audits', () => {
|
|
172
194
|
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
173
|
-
const passedSection = categoryDOM.querySelector('.lh-
|
|
195
|
+
const passedSection = categoryDOM.querySelector('.lh-clump--passed');
|
|
174
196
|
|
|
175
197
|
const passedAudits = category.auditRefs.filter(audit =>
|
|
176
|
-
audit.group &&
|
|
177
|
-
|
|
198
|
+
!audit.group &&
|
|
199
|
+
Util.showAsPassed(audit.result));
|
|
178
200
|
const passedElements = passedSection.querySelectorAll('.lh-audit');
|
|
179
201
|
assert.equal(passedElements.length, passedAudits.length);
|
|
180
202
|
});
|
|
@@ -187,10 +209,14 @@ describe('PerfCategoryRenderer', () => {
|
|
|
187
209
|
it('handles erroring opportunities', () => {
|
|
188
210
|
const auditWithDebug = {
|
|
189
211
|
score: 0,
|
|
190
|
-
group: 'load-opportunities',
|
|
191
212
|
result: {
|
|
192
213
|
error: true, score: 0,
|
|
193
214
|
numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
|
|
215
|
+
details: {
|
|
216
|
+
overallSavingsMs: 0,
|
|
217
|
+
items: [],
|
|
218
|
+
type: 'opportunity',
|
|
219
|
+
},
|
|
194
220
|
},
|
|
195
221
|
};
|
|
196
222
|
const wastedMs = renderer._getWastedMs(auditWithDebug);
|
|
@@ -90,7 +90,7 @@ describe('ReportRenderer', () => {
|
|
|
90
90
|
const output = renderer.renderReport(sampleResultsCopy, container);
|
|
91
91
|
|
|
92
92
|
function isPWAGauge(el) {
|
|
93
|
-
return el.querySelector('.lh-gauge__label').textContent === '
|
|
93
|
+
return el.querySelector('.lh-gauge__label').textContent === 'PWA';
|
|
94
94
|
}
|
|
95
95
|
function isPluginGauge(el) {
|
|
96
96
|
return el.querySelector('.lh-gauge__label').textContent === 'Some Plugin';
|
|
@@ -256,17 +256,15 @@ describe('ReportUIFeatures', () => {
|
|
|
256
256
|
.toThrowError('query #uses-rel-preconnect .lh-3p-filter-input not found');
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
-
it('filter is
|
|
260
|
-
const
|
|
261
|
-
dom.find('#render-blocking-resources .lh-3p-filter
|
|
262
|
-
expect(
|
|
263
|
-
expect(filterCheckbox.checked).toEqual(true);
|
|
259
|
+
it('filter is hidden when just third party resources', () => {
|
|
260
|
+
const filterControl =
|
|
261
|
+
dom.find('#render-blocking-resources .lh-3p-filter', container);
|
|
262
|
+
expect(filterControl.hidden).toEqual(true);
|
|
264
263
|
});
|
|
265
264
|
|
|
266
|
-
it('filter is
|
|
267
|
-
const
|
|
268
|
-
expect(
|
|
269
|
-
expect(filterCheckbox.checked).toEqual(false);
|
|
265
|
+
it('filter is hidden for just first party resources', () => {
|
|
266
|
+
const filterControl = dom.find('#uses-text-compression .lh-3p-filter', container);
|
|
267
|
+
expect(filterControl.hidden).toEqual(true);
|
|
270
268
|
});
|
|
271
269
|
});
|
|
272
270
|
});
|
|
@@ -515,6 +513,9 @@ describe('ReportUIFeatures', () => {
|
|
|
515
513
|
expect(render(lhr).querySelector('.lh-button.lh-report-icon--treemap')).toBeTruthy();
|
|
516
514
|
|
|
517
515
|
delete lhr.audits['script-treemap-data'];
|
|
516
|
+
const newAuditRefs = lhr.categories['performance'].auditRefs
|
|
517
|
+
.filter(a => a.id !== 'script-treemap-data');
|
|
518
|
+
lhr.categories['performance'].auditRefs = newAuditRefs;
|
|
518
519
|
expect(render(lhr).querySelector('.lh-button.lh-report-icon--treemap')).toBeNull();
|
|
519
520
|
});
|
|
520
521
|
});
|
|
@@ -401,10 +401,10 @@ describe('util helpers', () => {
|
|
|
401
401
|
const category = {
|
|
402
402
|
id: 'performance',
|
|
403
403
|
auditRefs: [
|
|
404
|
-
{weight: 3, result: {score: 1, scoreDisplayMode: 'binary'}, group: '
|
|
405
|
-
{weight: 2, result: {score: 1, scoreDisplayMode: 'binary'}, group: '
|
|
406
|
-
{weight: 0, result: {score: 1, scoreDisplayMode: 'binary'}, group: '
|
|
407
|
-
{weight: 1, result: {score: 0, scoreDisplayMode: 'binary'}, group: '
|
|
404
|
+
{weight: 3, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
405
|
+
{weight: 2, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
406
|
+
{weight: 0, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
407
|
+
{weight: 1, result: {score: 0, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
408
408
|
],
|
|
409
409
|
};
|
|
410
410
|
const fraction = Util.calculateCategoryFraction(category);
|
|
@@ -416,14 +416,14 @@ describe('util helpers', () => {
|
|
|
416
416
|
});
|
|
417
417
|
});
|
|
418
418
|
|
|
419
|
-
it('ignores manual audits, N/A audits, and
|
|
419
|
+
it('ignores manual audits, N/A audits, and hidden audits', () => {
|
|
420
420
|
const category = {
|
|
421
421
|
id: 'performance',
|
|
422
422
|
auditRefs: [
|
|
423
|
-
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: '
|
|
424
|
-
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}},
|
|
425
|
-
{weight: 1, result: {score: 0, scoreDisplayMode: 'manual'}, group: '
|
|
426
|
-
{weight: 1, result: {score: 0, scoreDisplayMode: 'notApplicable'}, group: '
|
|
423
|
+
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
424
|
+
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'hidden'},
|
|
425
|
+
{weight: 1, result: {score: 0, scoreDisplayMode: 'manual'}, group: 'metrics'},
|
|
426
|
+
{weight: 1, result: {score: 0, scoreDisplayMode: 'notApplicable'}, group: 'metrics'},
|
|
427
427
|
],
|
|
428
428
|
};
|
|
429
429
|
const fraction = Util.calculateCategoryFraction(category);
|
|
@@ -435,32 +435,14 @@ describe('util helpers', () => {
|
|
|
435
435
|
});
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
-
it('does not ignore audits with no group in non-performance category', () => {
|
|
439
|
-
const category = {
|
|
440
|
-
id: 'seo',
|
|
441
|
-
auditRefs: [
|
|
442
|
-
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
|
|
443
|
-
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}},
|
|
444
|
-
{weight: 1, result: {score: 0, scoreDisplayMode: 'manual'}, group: 'diagnostics'},
|
|
445
|
-
],
|
|
446
|
-
};
|
|
447
|
-
const fraction = Util.calculateCategoryFraction(category);
|
|
448
|
-
expect(fraction).toEqual({
|
|
449
|
-
numPassableAudits: 2,
|
|
450
|
-
numPassed: 2,
|
|
451
|
-
numInformative: 0,
|
|
452
|
-
totalWeight: 2,
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
|
|
456
438
|
it('tracks informative audits separately', () => {
|
|
457
439
|
const category = {
|
|
458
440
|
id: 'performance',
|
|
459
441
|
auditRefs: [
|
|
460
|
-
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: '
|
|
461
|
-
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: '
|
|
462
|
-
{weight: 0, result: {score: 1, scoreDisplayMode: 'informative'}, group: '
|
|
463
|
-
{weight: 1, result: {score: 0, scoreDisplayMode: 'informative'}, group: '
|
|
442
|
+
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
443
|
+
{weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
|
|
444
|
+
{weight: 0, result: {score: 1, scoreDisplayMode: 'informative'}, group: 'metrics'},
|
|
445
|
+
{weight: 1, result: {score: 0, scoreDisplayMode: 'informative'}, group: 'metrics'},
|
|
464
446
|
],
|
|
465
447
|
};
|
|
466
448
|
const fraction = Util.calculateCategoryFraction(category);
|
|
@@ -101,15 +101,15 @@ async function decorateScreenshot(datauri, tabId) {
|
|
|
101
101
|
img.src = datauri;
|
|
102
102
|
});
|
|
103
103
|
const c = document.createElement('canvas');
|
|
104
|
-
c.width = img.width;
|
|
105
|
-
c.height = img.height;
|
|
104
|
+
c.width = tabId === 'desktop' ? 280 : img.width;
|
|
105
|
+
c.height = tabId === 'desktop' ? 194 : img.height;
|
|
106
106
|
|
|
107
107
|
const ctx = c.getContext('2d');
|
|
108
108
|
if (!ctx) throw new Error();
|
|
109
|
-
ctx.drawImage(img, 0, 0);
|
|
110
|
-
ctx.font = `${
|
|
109
|
+
ctx.drawImage(img, 0, 0, c.width, c.height);
|
|
110
|
+
ctx.font = `${c.width / 2}px serif`;
|
|
111
111
|
ctx.textAlign = 'center';
|
|
112
112
|
ctx.globalAlpha = 0.7;
|
|
113
|
-
ctx.fillText(tabId === 'mobile' ? '📱' : '💻',
|
|
113
|
+
ctx.fillText(tabId === 'mobile' ? '📱' : '💻', c.width / 2, Math.min(c.height / 2, 700));
|
|
114
114
|
return c.toDataURL();
|
|
115
115
|
}
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
|
|
10
|
-
const MessageFormat = require('intl-messageformat')
|
|
10
|
+
const MessageFormat = require('intl-messageformat');
|
|
11
11
|
const {isObjectOfUnknownValues, isObjectOrArrayOfUnknownValues} = require('../type-verifiers.js');
|
|
12
12
|
|
|
13
13
|
/** Contains available locales with messages. May be an empty object if bundled. */
|
|
14
|
-
|
|
14
|
+
/** @type {import('./locales')} */
|
|
15
|
+
// @ts-expect-error TODO(esmodules): remove when file is es modules.
|
|
16
|
+
const LOCALE_MESSAGES = require('./locales.js').default || require('./locales.js');
|
|
15
17
|
|
|
16
18
|
const DEFAULT_LOCALE = 'en-US';
|
|
17
19
|
|
|
@@ -93,7 +95,7 @@ function collectAllCustomElementsFromICU(icuElements, seenElementsById = new Map
|
|
|
93
95
|
* Returns a copy of the `values` object, with the values formatted based on how
|
|
94
96
|
* they will be used in their icuMessage, e.g. KB or milliseconds. The original
|
|
95
97
|
* object is unchanged.
|
|
96
|
-
* @param {MessageFormat} messageFormatter
|
|
98
|
+
* @param {MessageFormat.IntlMessageFormat} messageFormatter
|
|
97
99
|
* @param {Readonly<Record<string, string | number>>} values
|
|
98
100
|
* @param {string} lhlMessage Used for clear error logging.
|
|
99
101
|
* @return {Record<string, string | number>}
|
|
@@ -171,7 +173,10 @@ function formatMessage(message, values = {}, locale) {
|
|
|
171
173
|
// When using accented english, force the use of a different locale for number formatting.
|
|
172
174
|
const localeForMessageFormat = (locale === 'en-XA' || locale === 'en-XL') ? 'de-DE' : locale;
|
|
173
175
|
|
|
174
|
-
|
|
176
|
+
// This package is not correctly bundled by Rollup.
|
|
177
|
+
/** @type {typeof MessageFormat.IntlMessageFormat} */
|
|
178
|
+
const MessageFormatCtor = MessageFormat.IntlMessageFormat || MessageFormat;
|
|
179
|
+
const formatter = new MessageFormatCtor(message, localeForMessageFormat, formats);
|
|
175
180
|
|
|
176
181
|
// Preformat values for the message format like KB and milliseconds.
|
|
177
182
|
const valuesForMessageFormat = _preformatValues(formatter, values, message);
|
|
@@ -800,21 +800,6 @@
|
|
|
800
800
|
"lighthouse-core/audits/dobetterweb/dom-size.js | title": {
|
|
801
801
|
"message": "Avoids an excessive DOM size"
|
|
802
802
|
},
|
|
803
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | columnFailingAnchors": {
|
|
804
|
-
"message": "Failing Anchors"
|
|
805
|
-
},
|
|
806
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | description": {
|
|
807
|
-
"message": "Add `rel=\"noopener\"` or `rel=\"noreferrer\"` to any external links to improve performance and prevent security vulnerabilities. [Learn more](https://web.dev/external-anchors-use-rel-noopener/)."
|
|
808
|
-
},
|
|
809
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | failureTitle": {
|
|
810
|
-
"message": "Links to cross-origin destinations are unsafe"
|
|
811
|
-
},
|
|
812
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | title": {
|
|
813
|
-
"message": "Links to cross-origin destinations are safe"
|
|
814
|
-
},
|
|
815
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | warning": {
|
|
816
|
-
"message": "Unable to determine the destination for anchor ({anchorHTML}). If not used as a hyperlink, consider removing target=_blank."
|
|
817
|
-
},
|
|
818
803
|
"lighthouse-core/audits/dobetterweb/geolocation-on-start.js | description": {
|
|
819
804
|
"message": "Users are mistrustful of or confused by sites that request their location without context. Consider tying the request to a user action instead. [Learn more](https://web.dev/geolocation-on-start/)."
|
|
820
805
|
},
|
|
@@ -800,21 +800,6 @@
|
|
|
800
800
|
"lighthouse-core/audits/dobetterweb/dom-size.js | title": {
|
|
801
801
|
"message": "تجنُب حجم DOM الزائد"
|
|
802
802
|
},
|
|
803
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | columnFailingAnchors": {
|
|
804
|
-
"message": "علامات الارتساء التي تعذّرت مراجعتها"
|
|
805
|
-
},
|
|
806
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | description": {
|
|
807
|
-
"message": "يمكنك إضافة `rel=\"noopener\"` أو `rel=\"noreferrer\"` إلى أي روابط خارجية لتحسين الأداء ومنع الثغرات الأمنية. [مزيد من المعلومات](https://web.dev/external-anchors-use-rel-noopener/)"
|
|
808
|
-
},
|
|
809
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | failureTitle": {
|
|
810
|
-
"message": "الروابط إلى وجهات مشتركة المصدر هي غير آمنة"
|
|
811
|
-
},
|
|
812
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | title": {
|
|
813
|
-
"message": "الروابط إلى وجهات مشتركة المصدر هي آمنة"
|
|
814
|
-
},
|
|
815
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | warning": {
|
|
816
|
-
"message": "يتعذّر تحديد الوجهة لإعلان ثابت {anchorHTML}). في حال عدم استخدامه كرابط تشعبي، يمكنك إزالة target=_blank."
|
|
817
|
-
},
|
|
818
803
|
"lighthouse-core/audits/dobetterweb/geolocation-on-start.js | description": {
|
|
819
804
|
"message": "لا يثق المستخدمون في المواقع الإلكترونية التي تطلب مواقعهم الجغرافية بدون سياق أو قد يؤدي ذلك إلى إرباكهم. يمكنك ربط الطلب بإجراء المستخدم بدلاً من ذلك. [مزيد من المعلومات](https://web.dev/geolocation-on-start/)"
|
|
820
805
|
},
|
|
@@ -800,21 +800,6 @@
|
|
|
800
800
|
"lighthouse-core/audits/dobetterweb/dom-size.js | title": {
|
|
801
801
|
"message": "Не се използва DOM с твърде голям размер"
|
|
802
802
|
},
|
|
803
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | columnFailingAnchors": {
|
|
804
|
-
"message": "Anchor маркери с грешки"
|
|
805
|
-
},
|
|
806
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | description": {
|
|
807
|
-
"message": "Добавете `rel=\"noopener\"` или `rel=\"noreferrer\"` към връзките към външни сайтове, за да подобрите ефективността и да избегнете уязвимости в сигурността. [Научете повече](https://web.dev/external-anchors-use-rel-noopener/)."
|
|
808
|
-
},
|
|
809
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | failureTitle": {
|
|
810
|
-
"message": "Връзките към външни дестинации не са безопасни"
|
|
811
|
-
},
|
|
812
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | title": {
|
|
813
|
-
"message": "Връзките към външни дестинации са безопасни"
|
|
814
|
-
},
|
|
815
|
-
"lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | warning": {
|
|
816
|
-
"message": "Не можахме да определим дестинацията за котвата ({anchorHTML}). Ако не се използва като хипервръзка, бихте могли да премахнете target=_blank."
|
|
817
|
-
},
|
|
818
803
|
"lighthouse-core/audits/dobetterweb/geolocation-on-start.js | description": {
|
|
819
804
|
"message": "Потребителите се объркват или нямат доверие на сайтове, които искат да узнаят местоположението им без контекст. Вместо това бихте могли да обвържете заявката към действие на потребителя. [Научете повече](https://web.dev/geolocation-on-start/)."
|
|
820
805
|
},
|