lighthouse 8.6.0 → 9.0.0-dev.20211118
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/changelog.md +121 -0
- package/dist/report/bundle.esm.js +6009 -0
- package/dist/report/flow.js +98 -25
- package/dist/report/standalone.js +41 -35
- package/flow-report/.eslintrc.cjs +2 -0
- package/flow-report/assets/standalone-flow-template.html +3 -4
- package/flow-report/assets/styles.css +108 -31
- package/flow-report/src/app.tsx +25 -28
- package/flow-report/src/common.tsx +28 -15
- package/flow-report/src/header.tsx +12 -13
- package/flow-report/src/help-dialog.tsx +3 -4
- package/flow-report/src/i18n/i18n.tsx +57 -16
- package/flow-report/src/i18n/ui-strings.js +50 -0
- package/flow-report/src/icons.tsx +36 -8
- package/flow-report/src/sidebar/flow.tsx +3 -3
- package/flow-report/src/sidebar/sidebar.tsx +35 -14
- package/flow-report/src/summary/category.tsx +97 -24
- package/flow-report/src/summary/summary.tsx +21 -14
- package/flow-report/src/topbar.tsx +13 -12
- package/flow-report/src/util.ts +61 -27
- package/flow-report/src/wrappers/category-score.tsx +9 -27
- package/flow-report/src/wrappers/markdown.tsx +18 -0
- package/flow-report/src/wrappers/report.tsx +26 -37
- package/flow-report/standalone-flow.tsx +5 -4
- package/flow-report/test/common-test.tsx +32 -17
- package/flow-report/test/flow-report-pptr-test.ts +46 -0
- package/flow-report/test/header-test.tsx +6 -6
- package/flow-report/test/setup/env-setup.ts +11 -5
- package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
- package/flow-report/test/summary/category-test.tsx +114 -21
- package/flow-report/test/summary/summary-test.tsx +4 -7
- package/flow-report/test/topbar-test.tsx +15 -14
- package/flow-report/test/util-test.tsx +69 -11
- package/flow-report/test/wrappers/category-score-test.tsx +84 -0
- package/flow-report/test/wrappers/markdown-test.tsx +17 -0
- package/flow-report/tsconfig.json +1 -0
- package/jest.config.js +4 -2
- package/lighthouse-cli/.eslintrc.cjs +2 -0
- package/lighthouse-cli/cli-flags.js +1 -1
- package/lighthouse-cli/run.js +1 -1
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
- package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
- package/lighthouse-core/audits/accessibility/axe-audit.js +7 -1
- package/lighthouse-core/audits/audit.js +1 -1
- package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -0
- package/lighthouse-core/audits/byte-efficiency/modern-image-formats.js +4 -2
- package/lighthouse-core/audits/byte-efficiency/offscreen-images.js +11 -10
- package/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js +5 -3
- package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +2 -1
- package/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +2 -1
- package/lighthouse-core/audits/deprecations.js +33 -11
- package/lighthouse-core/audits/dobetterweb/uses-http2.js +27 -11
- package/lighthouse-core/audits/errors-in-console.js +0 -2
- package/lighthouse-core/audits/image-aspect-ratio.js +5 -5
- package/lighthouse-core/audits/image-size-responsive.js +6 -6
- package/lighthouse-core/audits/installable-manifest.js +16 -7
- package/lighthouse-core/audits/preload-lcp-image.js +7 -5
- package/lighthouse-core/audits/script-treemap-data.js +1 -1
- package/lighthouse-core/audits/unsized-images.js +2 -3
- package/lighthouse-core/computed/image-records.js +4 -3
- 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 +21 -10
- package/lighthouse-core/config/constants.js +3 -4
- package/lighthouse-core/config/default-config.js +61 -71
- package/lighthouse-core/fraggle-rock/config/config.js +1 -1
- package/lighthouse-core/fraggle-rock/config/default-config.js +4 -5
- package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
- package/lighthouse-core/fraggle-rock/gather/session.js +3 -4
- package/lighthouse-core/gather/connections/cri.js +1 -1
- package/lighthouse-core/gather/driver/storage.js +0 -1
- package/lighthouse-core/gather/driver.js +3 -7
- package/lighthouse-core/gather/fetcher.js +4 -7
- package/lighthouse-core/gather/gather-runner.js +29 -6
- package/lighthouse-core/gather/gatherers/accessibility.js +27 -0
- package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
- package/lighthouse-core/gather/gatherers/image-elements.js +0 -3
- package/lighthouse-core/gather/gatherers/inspector-issues.js +5 -0
- package/lighthouse-core/lib/asset-saver.js +7 -4
- package/lighthouse-core/lib/emulation.js +45 -1
- package/lighthouse-core/lib/i18n/i18n.js +19 -8
- package/lighthouse-core/lib/lh-env.js +2 -0
- package/lighthouse-core/lib/page-functions.js +2 -1
- package/lighthouse-core/lib/proto-preprocessor.js +14 -2
- package/lighthouse-core/lib/statistics.js +26 -39
- 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 +65 -46
- package/package.json +27 -17
- package/readme.md +3 -3
- package/report/.eslintrc.cjs +2 -0
- package/report/README.md +1 -1
- package/report/assets/standalone-template.html +2 -3
- package/report/assets/styles.css +362 -256
- package/report/assets/templates.html +21 -76
- package/report/clients/bundle.js +1 -0
- package/report/clients/standalone.js +6 -15
- package/report/generator/README.md +1 -1
- package/report/renderer/api.js +66 -0
- package/report/renderer/category-renderer.js +76 -22
- package/report/renderer/components.js +59 -110
- package/report/renderer/crc-details-renderer.js +15 -12
- package/report/renderer/dom.js +16 -1
- package/report/renderer/drop-down-menu.js +2 -2
- package/report/renderer/element-screenshot-renderer.js +8 -8
- package/report/renderer/features-util.js +1 -1
- package/report/renderer/open-tab.js +9 -3
- package/report/renderer/performance-category-renderer.js +55 -34
- package/report/renderer/pwa-category-renderer.js +0 -10
- package/report/renderer/report-renderer.js +92 -44
- package/report/renderer/report-ui-features.js +36 -33
- package/report/renderer/swap-locale-feature.js +3 -3
- package/report/renderer/topbar-features.js +53 -48
- package/report/renderer/util.js +66 -46
- package/report/test/clients/bundle-test.js +70 -0
- package/report/test/renderer/category-renderer-test.js +76 -3
- package/report/test/renderer/i18n-test.js +0 -7
- package/report/test/renderer/performance-category-renderer-test.js +51 -14
- package/report/test/renderer/pwa-category-renderer-test.js +6 -6
- package/report/test/renderer/report-renderer-axe-test.js +11 -13
- package/report/test/renderer/report-renderer-test.js +52 -22
- package/report/test/renderer/report-ui-features-test.js +20 -10
- package/report/test/renderer/text-encoding-test.js +1 -1
- package/report/test/renderer/util-test.js +35 -31
- package/report/test-assets/faux-psi-template.html +3 -11
- package/report/test-assets/faux-psi.js +26 -22
- package/report/types/html-renderer.d.ts +2 -0
- package/report/types/report-renderer.d.ts +28 -0
- package/shared/localization/format.js +64 -30
- package/shared/localization/locales/ar-XB.json +72 -75
- package/shared/localization/locales/ar.json +72 -75
- package/shared/localization/locales/bg.json +72 -75
- package/shared/localization/locales/ca.json +72 -75
- package/shared/localization/locales/cs.json +89 -92
- package/shared/localization/locales/da.json +72 -75
- package/shared/localization/locales/de.json +89 -92
- package/shared/localization/locales/el.json +72 -75
- package/shared/localization/locales/en-GB.json +72 -75
- package/shared/localization/locales/en-US.json +78 -75
- package/shared/localization/locales/en-XA.json +72 -75
- package/shared/localization/locales/en-XL.json +78 -75
- package/shared/localization/locales/es-419.json +90 -93
- package/shared/localization/locales/es.json +90 -93
- package/shared/localization/locales/fi.json +72 -75
- package/shared/localization/locales/fil.json +72 -75
- package/shared/localization/locales/fr.json +89 -92
- package/shared/localization/locales/he.json +73 -76
- package/shared/localization/locales/hi.json +72 -75
- package/shared/localization/locales/hr.json +72 -75
- package/shared/localization/locales/hu.json +89 -92
- package/shared/localization/locales/id.json +89 -92
- package/shared/localization/locales/it.json +89 -92
- package/shared/localization/locales/ja.json +89 -92
- package/shared/localization/locales/ko.json +89 -92
- package/shared/localization/locales/lt.json +89 -92
- package/shared/localization/locales/lv.json +95 -98
- package/shared/localization/locales/nl.json +89 -92
- package/shared/localization/locales/no.json +72 -75
- package/shared/localization/locales/pl.json +72 -75
- package/shared/localization/locales/pt-PT.json +72 -75
- package/shared/localization/locales/pt.json +89 -92
- package/shared/localization/locales/ro.json +90 -93
- package/shared/localization/locales/ru.json +89 -92
- package/shared/localization/locales/sk.json +89 -92
- package/shared/localization/locales/sl.json +89 -92
- package/shared/localization/locales/sr-Latn.json +89 -92
- package/shared/localization/locales/sr.json +89 -92
- package/shared/localization/locales/sv.json +72 -75
- package/shared/localization/locales/ta.json +89 -92
- package/shared/localization/locales/te.json +89 -92
- package/shared/localization/locales/th.json +89 -92
- package/shared/localization/locales/tr.json +89 -92
- package/shared/localization/locales/uk.json +92 -95
- package/shared/localization/locales/vi.json +89 -92
- package/shared/localization/locales/zh-HK.json +89 -92
- package/shared/localization/locales/zh-TW.json +89 -92
- package/shared/localization/locales/zh.json +89 -92
- package/shared/localization/locales.js +6 -0
- package/shared/test/localization/format-test.js +45 -9
- package/shared/test/localization/swap-locale-test.js +0 -12
- package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +19 -11
- package/third-party/chromium-synchronization/installability-errors-test.js +3 -2
- package/third-party/download-content-shell/download-content-shell.js +2 -2
- package/third-party/download-content-shell/utils.js +0 -24
- package/third-party/snyk/snapshot.json +4 -1
- package/tsconfig-all.json +2 -2
- package/tsconfig.json +0 -1
- package/types/artifacts.d.ts +7 -7
- package/types/enquirer.d.ts +3 -1
- package/types/lhr/audit-details.d.ts +6 -5
- package/types/lhr/flow.d.ts +3 -2
- package/types/lhr/lhr.d.ts +1 -1
- package/types/node.d.ts +5 -5
- package/types/rollup-plugin-postprocess.d.ts +10 -0
- package/flow-report/src/wrappers/report-renderer.tsx +0 -46
- package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
- package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
- package/lighthouse-core/audits/redirects-http.js +0 -59
- package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
- package/lighthouse-core/gather/gatherers/http-redirect.js +0 -46
- package/lighthouse-core/lib/i18n/locales/en-US.ctc.json +0 -6699
- package/lighthouse-core/lib/i18n/locales/en-XL.ctc.json +0 -6672
- package/report/clients/psi.js +0 -158
- package/report/test/clients/psi-test.js +0 -143
- package/shared/localization/locales/en-US.ctc.json +0 -6877
- package/shared/localization/locales/en-XL.ctc.json +0 -6852
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
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
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
|
|
11
|
+
import jsdom from 'jsdom';
|
|
12
|
+
import {jest} from '@jest/globals';
|
|
13
|
+
|
|
14
|
+
import * as lighthouseRenderer from '../../clients/bundle.js';
|
|
15
|
+
import {LH_ROOT} from '../../../root.js';
|
|
16
|
+
|
|
17
|
+
const sampleResultsStr =
|
|
18
|
+
fs.readFileSync(LH_ROOT + '/lighthouse-core/test/results/sample_v2.json', 'utf-8');
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/* eslint-env jest */
|
|
22
|
+
|
|
23
|
+
describe('lighthouseRenderer bundle', () => {
|
|
24
|
+
let document;
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
global.console.warn = jest.fn();
|
|
27
|
+
|
|
28
|
+
const {window} = new jsdom.JSDOM();
|
|
29
|
+
document = window.document;
|
|
30
|
+
|
|
31
|
+
global.window = global.self = window;
|
|
32
|
+
global.window.requestAnimationFrame = fn => fn();
|
|
33
|
+
// Stub out matchMedia for Node.
|
|
34
|
+
global.self.matchMedia = function() {
|
|
35
|
+
return {
|
|
36
|
+
addListener: function() {},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
global.window.ResizeObserver = class ResizeObserver {
|
|
40
|
+
observe() { }
|
|
41
|
+
unobserve() { }
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
global.window = global.self = undefined;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
it('renders an LHR to DOM', () => {
|
|
51
|
+
const lhr = /** @type {LH.Result} */ JSON.parse(sampleResultsStr);
|
|
52
|
+
const reportContainer = document.body;
|
|
53
|
+
reportContainer.classList.add('lh-vars', 'lh-root');
|
|
54
|
+
|
|
55
|
+
const dom = new lighthouseRenderer.DOM(reportContainer.ownerDocument);
|
|
56
|
+
const renderer = new lighthouseRenderer.ReportRenderer(dom);
|
|
57
|
+
renderer.renderReport(lhr, reportContainer);
|
|
58
|
+
const features = new lighthouseRenderer.ReportUIFeatures(renderer._dom);
|
|
59
|
+
features.initFeatures(lhr);
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
// Check that the report exists and has some content.
|
|
63
|
+
expect(reportContainer instanceof document.defaultView.Element).toBeTruthy();
|
|
64
|
+
expect(reportContainer.outerHTML.length).toBeGreaterThan(50000);
|
|
65
|
+
|
|
66
|
+
const title = reportContainer.querySelector('.lh-audit-group--metrics')
|
|
67
|
+
.querySelector('.lh-audit-group__title');
|
|
68
|
+
expect(title.textContent).toEqual('Metrics');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -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');
|
|
@@ -409,6 +409,10 @@ describe('CategoryRenderer', () => {
|
|
|
409
409
|
const passingRefs = categoryClone.auditRefs.filter(ref => ref.result.score === 1);
|
|
410
410
|
passingRefs[0].result.warnings = ['Some warning'];
|
|
411
411
|
passingRefs[1].result.warnings = ['Some warning'];
|
|
412
|
+
// Make one audit n/a
|
|
413
|
+
const audit = categoryClone.auditRefs.find(ref => ref.id === 'themed-omnibox');
|
|
414
|
+
audit.result.scoreDisplayMode = 'notApplicable';
|
|
415
|
+
audit.result.score = null;
|
|
412
416
|
|
|
413
417
|
const elem = renderer.render(categoryClone, sampleResults.categoryGroups);
|
|
414
418
|
const passedAudits = elem.querySelectorAll('.lh-clump--passed .lh-audit');
|
|
@@ -418,7 +422,7 @@ describe('CategoryRenderer', () => {
|
|
|
418
422
|
const naAudits = elem.querySelectorAll('.lh-clump--notapplicable .lh-audit');
|
|
419
423
|
|
|
420
424
|
assert.equal(passedAudits.length, 0);
|
|
421
|
-
assert.equal(failedAudits.length,
|
|
425
|
+
assert.equal(failedAudits.length, 5);
|
|
422
426
|
assert.equal(warningAudits.length, 2);
|
|
423
427
|
assert.equal(manualAudits.length, 3);
|
|
424
428
|
assert.equal(naAudits.length, 1);
|
|
@@ -495,4 +499,73 @@ describe('CategoryRenderer', () => {
|
|
|
495
499
|
assert.ok(shouldBeWarning[0].textContent.includes(passingWarning));
|
|
496
500
|
});
|
|
497
501
|
});
|
|
502
|
+
|
|
503
|
+
describe('renderCategoryScore', () => {
|
|
504
|
+
it('removes label if omitLabel is true', () => {
|
|
505
|
+
const options = {omitLabel: true};
|
|
506
|
+
const categoryScore = renderer.renderCategoryScore(
|
|
507
|
+
sampleResults.categories.performance,
|
|
508
|
+
{},
|
|
509
|
+
options
|
|
510
|
+
);
|
|
511
|
+
const label = categoryScore.querySelector('.lh-gauge__label,.lh-fraction__label');
|
|
512
|
+
assert.ok(!label);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('uses custom callback if present', () => {
|
|
516
|
+
const options = {
|
|
517
|
+
onPageAnchorRendered: link => {
|
|
518
|
+
link.href = '#index=0&anchor=performance';
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
const categoryScore = renderer.renderCategoryScore(
|
|
522
|
+
sampleResults.categories.performance,
|
|
523
|
+
{},
|
|
524
|
+
options
|
|
525
|
+
);
|
|
526
|
+
const link = categoryScore.querySelector('a');
|
|
527
|
+
assert.equal(link.hash, '#index=0&anchor=performance');
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('renders audits by weight', () => {
|
|
532
|
+
const defaultAuditRef = {
|
|
533
|
+
title: '',
|
|
534
|
+
description: '',
|
|
535
|
+
scoreDisplayMode: 'numeric',
|
|
536
|
+
score: 0,
|
|
537
|
+
warnings: [],
|
|
538
|
+
};
|
|
539
|
+
const category = {
|
|
540
|
+
id: 'test',
|
|
541
|
+
title: 'Test',
|
|
542
|
+
score: 0,
|
|
543
|
+
auditRefs: [{
|
|
544
|
+
id: 'audit-1',
|
|
545
|
+
weight: 0,
|
|
546
|
+
result: {
|
|
547
|
+
id: 'audit-1',
|
|
548
|
+
...defaultAuditRef,
|
|
549
|
+
},
|
|
550
|
+
}, {
|
|
551
|
+
id: 'audit-2',
|
|
552
|
+
weight: 1,
|
|
553
|
+
result: {
|
|
554
|
+
id: 'audit-2',
|
|
555
|
+
...defaultAuditRef,
|
|
556
|
+
},
|
|
557
|
+
}, {
|
|
558
|
+
id: 'audit-3',
|
|
559
|
+
weight: 0.5,
|
|
560
|
+
result: {
|
|
561
|
+
id: 'audit-3',
|
|
562
|
+
...defaultAuditRef,
|
|
563
|
+
},
|
|
564
|
+
}],
|
|
565
|
+
};
|
|
566
|
+
const categoryDOM = renderer.render(category);
|
|
567
|
+
|
|
568
|
+
const auditEls = [...categoryDOM.querySelectorAll('.lh-audit')];
|
|
569
|
+
expect(auditEls.map(el => el.id)).toEqual(['audit-2', 'audit-3', 'audit-1']);
|
|
570
|
+
});
|
|
498
571
|
});
|
|
@@ -9,7 +9,6 @@ import {strict as assert} from 'assert';
|
|
|
9
9
|
|
|
10
10
|
import {Util} from '../../renderer/util.js';
|
|
11
11
|
import {I18n} from '../../renderer/i18n.js';
|
|
12
|
-
import {isNode12SmallIcu} from '../../../lighthouse-core/test/test-utils.js';
|
|
13
12
|
|
|
14
13
|
// Require i18n to make sure Intl is polyfilled in Node without full-icu for testing.
|
|
15
14
|
// When Util is run in a browser, Intl will be supplied natively (IE11+).
|
|
@@ -108,9 +107,6 @@ describe('util helpers', () => {
|
|
|
108
107
|
});
|
|
109
108
|
|
|
110
109
|
it('formats numbers based on locale', () => {
|
|
111
|
-
// COMPAT: Node 12 only has 'en' by default.
|
|
112
|
-
if (isNode12SmallIcu()) return;
|
|
113
|
-
|
|
114
110
|
// Requires full-icu or Intl polyfill.
|
|
115
111
|
const number = 12346.858558;
|
|
116
112
|
|
|
@@ -122,9 +118,6 @@ describe('util helpers', () => {
|
|
|
122
118
|
});
|
|
123
119
|
|
|
124
120
|
it('uses decimal comma with en-XA test locale', () => {
|
|
125
|
-
// COMPAT: Node 12 only has 'en' by default.
|
|
126
|
-
if (isNode12SmallIcu()) return;
|
|
127
|
-
|
|
128
121
|
// Requires full-icu or Intl polyfill.
|
|
129
122
|
const number = 12346.858558;
|
|
130
123
|
|
|
@@ -67,6 +67,17 @@ describe('PerfCategoryRenderer', () => {
|
|
|
67
67
|
const timelineElements = metricsSection.querySelectorAll('.lh-metric');
|
|
68
68
|
const nontimelineElements = metricsSection.querySelectorAll('.lh-audit');
|
|
69
69
|
assert.equal(timelineElements.length + nontimelineElements.length, metricAudits.length);
|
|
70
|
+
assert.deepStrictEqual(
|
|
71
|
+
Array.from(timelineElements).map(el => el.id),
|
|
72
|
+
[
|
|
73
|
+
'first-contentful-paint',
|
|
74
|
+
'interactive',
|
|
75
|
+
'speed-index',
|
|
76
|
+
'total-blocking-time',
|
|
77
|
+
'largest-contentful-paint',
|
|
78
|
+
'cumulative-layout-shift',
|
|
79
|
+
]
|
|
80
|
+
);
|
|
70
81
|
});
|
|
71
82
|
|
|
72
83
|
it('does not render metrics section if no metric group audits', () => {
|
|
@@ -84,7 +95,7 @@ describe('PerfCategoryRenderer', () => {
|
|
|
84
95
|
it('renders the metrics variance disclaimer as markdown', () => {
|
|
85
96
|
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
86
97
|
const disclaimerEl =
|
|
87
|
-
categoryDOM.querySelector('.lh-
|
|
98
|
+
categoryDOM.querySelector('.lh-category-header__description > .lh-metrics__disclaimer');
|
|
88
99
|
|
|
89
100
|
assert.ok(disclaimerEl.textContent.includes('Values are estimated'));
|
|
90
101
|
const disclamerLink = disclaimerEl.querySelector('a');
|
|
@@ -96,11 +107,23 @@ describe('PerfCategoryRenderer', () => {
|
|
|
96
107
|
assert.strictEqual(new URL(calcLink.href).hostname, 'googlechrome.github.io');
|
|
97
108
|
});
|
|
98
109
|
|
|
110
|
+
it('ignores hidden audits', () => {
|
|
111
|
+
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
112
|
+
|
|
113
|
+
const hiddenAudits = category.auditRefs.filter(audit => audit.group === 'hidden');
|
|
114
|
+
const auditElements = [...categoryDOM.querySelectorAll('.lh-audit')];
|
|
115
|
+
const matchingElements = auditElements
|
|
116
|
+
.filter(el => hiddenAudits.some(audit => audit.id === el.id));
|
|
117
|
+
expect(matchingElements).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
99
120
|
it('renders the failing performance opportunities', () => {
|
|
100
121
|
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
101
122
|
|
|
102
|
-
const oppAudits = category.auditRefs.filter(audit =>
|
|
103
|
-
|
|
123
|
+
const oppAudits = category.auditRefs.filter(audit =>
|
|
124
|
+
audit.result.details &&
|
|
125
|
+
audit.result.details.type === 'opportunity' &&
|
|
126
|
+
!Util.showAsPassed(audit.result));
|
|
104
127
|
const oppElements = [...categoryDOM.querySelectorAll('.lh-audit--load-opportunity')];
|
|
105
128
|
expect(oppElements.map(e => e.id).sort()).toEqual(oppAudits.map(a => a.id).sort());
|
|
106
129
|
expect(oppElements.length).toBeGreaterThan(0);
|
|
@@ -120,10 +143,14 @@ describe('PerfCategoryRenderer', () => {
|
|
|
120
143
|
it('renders performance opportunities with an errorMessage', () => {
|
|
121
144
|
const auditWithError = {
|
|
122
145
|
score: 0,
|
|
123
|
-
group: 'load-opportunities',
|
|
124
146
|
result: {
|
|
125
147
|
score: null, scoreDisplayMode: 'error', errorMessage: 'Yikes!!', title: 'Bug #2',
|
|
126
148
|
description: '',
|
|
149
|
+
details: {
|
|
150
|
+
overallSavingsMs: 0,
|
|
151
|
+
items: [],
|
|
152
|
+
type: 'opportunity',
|
|
153
|
+
},
|
|
127
154
|
},
|
|
128
155
|
};
|
|
129
156
|
|
|
@@ -137,10 +164,14 @@ describe('PerfCategoryRenderer', () => {
|
|
|
137
164
|
it('renders performance opportunities\' explanation', () => {
|
|
138
165
|
const auditWithExplanation = {
|
|
139
166
|
score: 0,
|
|
140
|
-
group: 'load-opportunities',
|
|
141
167
|
result: {
|
|
142
168
|
score: 0, scoreDisplayMode: 'numeric',
|
|
143
169
|
numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2', description: '',
|
|
170
|
+
details: {
|
|
171
|
+
overallSavingsMs: 0,
|
|
172
|
+
items: [],
|
|
173
|
+
type: 'opportunity',
|
|
174
|
+
},
|
|
144
175
|
},
|
|
145
176
|
};
|
|
146
177
|
|
|
@@ -159,7 +190,9 @@ describe('PerfCategoryRenderer', () => {
|
|
|
159
190
|
'.lh-category > .lh-audit-group.lh-audit-group--diagnostics');
|
|
160
191
|
|
|
161
192
|
const diagnosticAuditIds = category.auditRefs.filter(audit => {
|
|
162
|
-
return audit.group
|
|
193
|
+
return !audit.group &&
|
|
194
|
+
!(audit.result.details && audit.result.details.type === 'opportunity') &&
|
|
195
|
+
!Util.showAsPassed(audit.result);
|
|
163
196
|
}).map(audit => audit.id).sort();
|
|
164
197
|
assert.ok(diagnosticAuditIds.length > 0);
|
|
165
198
|
|
|
@@ -170,11 +203,11 @@ describe('PerfCategoryRenderer', () => {
|
|
|
170
203
|
|
|
171
204
|
it('renders the passed audits', () => {
|
|
172
205
|
const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
|
|
173
|
-
const passedSection = categoryDOM.querySelector('.lh-
|
|
206
|
+
const passedSection = categoryDOM.querySelector('.lh-clump--passed');
|
|
174
207
|
|
|
175
208
|
const passedAudits = category.auditRefs.filter(audit =>
|
|
176
|
-
audit.group &&
|
|
177
|
-
|
|
209
|
+
!audit.group &&
|
|
210
|
+
Util.showAsPassed(audit.result));
|
|
178
211
|
const passedElements = passedSection.querySelectorAll('.lh-audit');
|
|
179
212
|
assert.equal(passedElements.length, passedAudits.length);
|
|
180
213
|
});
|
|
@@ -187,10 +220,14 @@ describe('PerfCategoryRenderer', () => {
|
|
|
187
220
|
it('handles erroring opportunities', () => {
|
|
188
221
|
const auditWithDebug = {
|
|
189
222
|
score: 0,
|
|
190
|
-
group: 'load-opportunities',
|
|
191
223
|
result: {
|
|
192
224
|
error: true, score: 0,
|
|
193
225
|
numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
|
|
226
|
+
details: {
|
|
227
|
+
overallSavingsMs: 0,
|
|
228
|
+
items: [],
|
|
229
|
+
type: 'opportunity',
|
|
230
|
+
},
|
|
194
231
|
},
|
|
195
232
|
};
|
|
196
233
|
const wastedMs = renderer._getWastedMs(auditWithDebug);
|
|
@@ -256,10 +293,10 @@ describe('PerfCategoryRenderer', () => {
|
|
|
256
293
|
expect(url.hash.split('&')).toMatchInlineSnapshot(`
|
|
257
294
|
Array [
|
|
258
295
|
"#FCP=6844",
|
|
259
|
-
"SI=8114",
|
|
260
|
-
"LCP=6844",
|
|
261
296
|
"TTI=8191",
|
|
297
|
+
"SI=8114",
|
|
262
298
|
"TBT=1221",
|
|
299
|
+
"LCP=6844",
|
|
263
300
|
"CLS=0",
|
|
264
301
|
"FMP=6844",
|
|
265
302
|
]
|
|
@@ -277,10 +314,10 @@ Array [
|
|
|
277
314
|
expect(url.hash.split('&')).toMatchInlineSnapshot(`
|
|
278
315
|
Array [
|
|
279
316
|
"#FCP=6844",
|
|
280
|
-
"SI=8114",
|
|
281
|
-
"LCP=6844",
|
|
282
317
|
"TTI=8191",
|
|
318
|
+
"SI=8114",
|
|
283
319
|
"TBT=1221",
|
|
320
|
+
"LCP=6844",
|
|
284
321
|
"CLS=0.14",
|
|
285
322
|
"FMP=6844",
|
|
286
323
|
"device=mobile",
|
|
@@ -107,12 +107,12 @@ describe('PwaCategoryRenderer', () => {
|
|
|
107
107
|
const clone = JSON.parse(JSON.stringify(sampleResults));
|
|
108
108
|
const category = clone.categories.pwa;
|
|
109
109
|
|
|
110
|
-
// Set everything to passing, except
|
|
110
|
+
// Set everything to passing, except for one. (themed-omnibox chosen randomly)
|
|
111
111
|
for (const auditRef of category.auditRefs) {
|
|
112
112
|
auditRef.result.score = 1;
|
|
113
113
|
auditRef.result.scoreDisplayMode = 'binary';
|
|
114
114
|
}
|
|
115
|
-
const audit = category.auditRefs.find(ref => ref.id === '
|
|
115
|
+
const audit = category.auditRefs.find(ref => ref.id === 'themed-omnibox');
|
|
116
116
|
audit.result.scoreDisplayMode = 'notApplicable';
|
|
117
117
|
audit.result.score = null;
|
|
118
118
|
|
|
@@ -249,10 +249,10 @@ describe('PwaCategoryRenderer', () => {
|
|
|
249
249
|
});
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
describe('#
|
|
252
|
+
describe('#renderCategoryScore', () => {
|
|
253
253
|
it('renders an error score gauge in case of category error', () => {
|
|
254
254
|
category.score = null;
|
|
255
|
-
const badgeGauge = pwaRenderer.
|
|
255
|
+
const badgeGauge = pwaRenderer.renderCategoryScore(category, sampleResults.categoryGroups);
|
|
256
256
|
|
|
257
257
|
// Not a PWA gauge.
|
|
258
258
|
assert.strictEqual(badgeGauge.querySelector('.lh-gauge--pwa__wrapper'), null);
|
|
@@ -263,11 +263,11 @@ describe('PwaCategoryRenderer', () => {
|
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
it('renders score gauges with unique ids for items in <defs>', () => {
|
|
266
|
-
const gauge1 = pwaRenderer.
|
|
266
|
+
const gauge1 = pwaRenderer.renderCategoryScore(category, sampleResults.categoryGroups);
|
|
267
267
|
const gauge1Ids = [...gauge1.querySelectorAll('defs [id]')].map(el => el.id);
|
|
268
268
|
assert.ok(gauge1Ids.length > 2);
|
|
269
269
|
|
|
270
|
-
const gauge2 = pwaRenderer.
|
|
270
|
+
const gauge2 = pwaRenderer.renderCategoryScore(category, sampleResults.categoryGroups);
|
|
271
271
|
const gauge2Ids = [...gauge2.querySelectorAll('defs [id]')].map(el => el.id);
|
|
272
272
|
assert.ok(gauge2Ids.length === gauge1Ids.length);
|
|
273
273
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
/* eslint-env jest */
|
|
9
9
|
|
|
10
10
|
import jsdom from 'jsdom';
|
|
11
|
+
import {jest} from '@jest/globals';
|
|
11
12
|
|
|
12
13
|
import {Util} from '../../renderer/util.js';
|
|
13
14
|
import {DOM} from '../../renderer/dom.js';
|
|
@@ -23,6 +24,8 @@ describe('ReportRendererAxe', () => {
|
|
|
23
24
|
let sampleResults;
|
|
24
25
|
|
|
25
26
|
beforeAll(async () => {
|
|
27
|
+
global.console.warn = jest.fn();
|
|
28
|
+
|
|
26
29
|
const {window} = new jsdom.JSDOM();
|
|
27
30
|
const dom = new DOM(window.document);
|
|
28
31
|
const detailsRenderer = new DetailsRenderer(dom);
|
|
@@ -46,11 +49,11 @@ describe('ReportRendererAxe', () => {
|
|
|
46
49
|
global.Element = undefined;
|
|
47
50
|
});
|
|
48
51
|
|
|
49
|
-
it('renders without axe violations', () => {
|
|
50
|
-
const container = renderer._dom.
|
|
52
|
+
it('renders without axe violations', async () => {
|
|
53
|
+
const container = renderer._dom.document().createElement('main');
|
|
51
54
|
const output = renderer.renderReport(sampleResults, container);
|
|
52
55
|
|
|
53
|
-
renderer._dom.
|
|
56
|
+
renderer._dom.document().body.appendChild(container);
|
|
54
57
|
|
|
55
58
|
const config = {
|
|
56
59
|
rules: {
|
|
@@ -70,17 +73,12 @@ describe('ReportRendererAxe', () => {
|
|
|
70
73
|
},
|
|
71
74
|
};
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
expect(error).toBeNull();
|
|
76
|
-
expect(violations).toEqual([]);
|
|
77
|
-
resolve();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
// This test takes 40s on fast hardware, and 50-60s on GHA.
|
|
81
|
-
// https://github.com/dequelabs/axe-core/tree/b573b1c1/doc/examples/jest_react#timeout-issues
|
|
76
|
+
const axeResults = await axe.run(output, config);
|
|
77
|
+
expect(axeResults.violations).toEqual([]);
|
|
82
78
|
},
|
|
83
|
-
|
|
79
|
+
// This test takes 40s on fast hardware, and 50-60s on GHA (but can take longer).
|
|
80
|
+
// https://github.com/dequelabs/axe-core/tree/b573b1c1/doc/examples/jest_react#timeout-issues
|
|
81
|
+
/* timeout= */ process.env.CI ? 200_000 : 60_000
|
|
84
82
|
);
|
|
85
83
|
});
|
|
86
84
|
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {strict as assert} from 'assert';
|
|
11
11
|
|
|
12
12
|
import jsdom from 'jsdom';
|
|
13
|
+
import {jest} from '@jest/globals';
|
|
13
14
|
|
|
14
15
|
import {Util} from '../../renderer/util.js';
|
|
15
16
|
import URL from '../../../lighthouse-core/lib/url-shim.js';
|
|
@@ -26,6 +27,8 @@ describe('ReportRenderer', () => {
|
|
|
26
27
|
let sampleResults;
|
|
27
28
|
|
|
28
29
|
beforeAll(() => {
|
|
30
|
+
global.console.warn = jest.fn();
|
|
31
|
+
|
|
29
32
|
// Stub out matchMedia for Node.
|
|
30
33
|
global.matchMedia = function() {
|
|
31
34
|
return {
|
|
@@ -50,7 +53,7 @@ describe('ReportRenderer', () => {
|
|
|
50
53
|
|
|
51
54
|
describe('renderReport', () => {
|
|
52
55
|
it('should render a report', () => {
|
|
53
|
-
const container = renderer._dom.
|
|
56
|
+
const container = renderer._dom.document().body;
|
|
54
57
|
const output = renderer.renderReport(sampleResults, container);
|
|
55
58
|
assert.ok(output.querySelector('.lh-header-container'), 'has a header');
|
|
56
59
|
assert.ok(output.querySelector('.lh-report'), 'has report body');
|
|
@@ -60,7 +63,7 @@ describe('ReportRenderer', () => {
|
|
|
60
63
|
});
|
|
61
64
|
|
|
62
65
|
it('renders additional reports by replacing the existing one', () => {
|
|
63
|
-
const container = renderer._dom.
|
|
66
|
+
const container = renderer._dom.document().body;
|
|
64
67
|
const oldReport = Array.from(renderer.renderReport(sampleResults, container).children);
|
|
65
68
|
const newReport = Array.from(renderer.renderReport(sampleResults, container).children);
|
|
66
69
|
assert.ok(!oldReport.find(node => container.contains(node)), 'old report was removed');
|
|
@@ -86,11 +89,11 @@ describe('ReportRenderer', () => {
|
|
|
86
89
|
auditRefs: [],
|
|
87
90
|
};
|
|
88
91
|
|
|
89
|
-
const container = renderer._dom.
|
|
92
|
+
const container = renderer._dom.document().body;
|
|
90
93
|
const output = renderer.renderReport(sampleResultsCopy, container);
|
|
91
94
|
|
|
92
95
|
function isPWAGauge(el) {
|
|
93
|
-
return el.querySelector('.lh-gauge__label').textContent === '
|
|
96
|
+
return el.querySelector('.lh-gauge__label').textContent === 'PWA';
|
|
94
97
|
}
|
|
95
98
|
function isPluginGauge(el) {
|
|
96
99
|
return el.querySelector('.lh-gauge__label').textContent === 'Some Plugin';
|
|
@@ -119,6 +122,35 @@ describe('ReportRenderer', () => {
|
|
|
119
122
|
}
|
|
120
123
|
});
|
|
121
124
|
|
|
125
|
+
it('renders score gauges with custom callback', () => {
|
|
126
|
+
const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
|
|
127
|
+
|
|
128
|
+
const opts = {
|
|
129
|
+
onPageAnchorRendered: link => {
|
|
130
|
+
const id = link.hash.substring(1);
|
|
131
|
+
link.hash = `#index=0&anchor=${id}`;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const container = renderer._dom.document().body;
|
|
135
|
+
const output = renderer.renderReport(sampleResultsCopy, container, opts);
|
|
136
|
+
const anchors = output.querySelectorAll('a.lh-gauge__wrapper, a.lh-fraction__wrapper');
|
|
137
|
+
const hashes = Array.from(anchors).map(anchor => anchor.hash).filter(hash => hash);
|
|
138
|
+
|
|
139
|
+
// One set for the sticky header, on set for the gauges at the top.
|
|
140
|
+
assert.deepStrictEqual(hashes, [
|
|
141
|
+
'#index=0&anchor=performance',
|
|
142
|
+
'#index=0&anchor=accessibility',
|
|
143
|
+
'#index=0&anchor=best-practices',
|
|
144
|
+
'#index=0&anchor=seo',
|
|
145
|
+
'#index=0&anchor=pwa',
|
|
146
|
+
'#index=0&anchor=performance',
|
|
147
|
+
'#index=0&anchor=accessibility',
|
|
148
|
+
'#index=0&anchor=best-practices',
|
|
149
|
+
'#index=0&anchor=seo',
|
|
150
|
+
'#index=0&anchor=pwa',
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
122
154
|
it('renders plugin score gauge', () => {
|
|
123
155
|
const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
|
|
124
156
|
sampleResultsCopy.categories['lighthouse-plugin-someplugin'] = {
|
|
@@ -126,7 +158,7 @@ describe('ReportRenderer', () => {
|
|
|
126
158
|
title: 'Some Plugin',
|
|
127
159
|
auditRefs: [],
|
|
128
160
|
};
|
|
129
|
-
const container = renderer._dom.
|
|
161
|
+
const container = renderer._dom.document().body;
|
|
130
162
|
const output = renderer.renderReport(sampleResultsCopy, container);
|
|
131
163
|
const scoresHeaderElem = output.querySelector('.lh-scores-header');
|
|
132
164
|
|
|
@@ -140,7 +172,7 @@ describe('ReportRenderer', () => {
|
|
|
140
172
|
});
|
|
141
173
|
|
|
142
174
|
it('should not mutate a report object', () => {
|
|
143
|
-
const container = renderer._dom.
|
|
175
|
+
const container = renderer._dom.document().body;
|
|
144
176
|
const originalResults = JSON.parse(JSON.stringify(sampleResults));
|
|
145
177
|
renderer.renderReport(sampleResults, container);
|
|
146
178
|
assert.deepStrictEqual(sampleResults, originalResults);
|
|
@@ -148,13 +180,13 @@ describe('ReportRenderer', () => {
|
|
|
148
180
|
|
|
149
181
|
it('renders no warning section when no lighthouseRunWarnings occur', () => {
|
|
150
182
|
const warningResults = Object.assign({}, sampleResults, {runWarnings: []});
|
|
151
|
-
const container = renderer._dom.
|
|
183
|
+
const container = renderer._dom.document().body;
|
|
152
184
|
const output = renderer.renderReport(warningResults, container);
|
|
153
185
|
assert.strictEqual(output.querySelector('.lh-warnings--toplevel'), null);
|
|
154
186
|
});
|
|
155
187
|
|
|
156
188
|
it('renders a warning section', () => {
|
|
157
|
-
const container = renderer._dom.
|
|
189
|
+
const container = renderer._dom.document().body;
|
|
158
190
|
const output = renderer.renderReport(sampleResults, container);
|
|
159
191
|
|
|
160
192
|
const warningEls = output.querySelectorAll('.lh-warnings--toplevel > ul > li');
|
|
@@ -165,7 +197,7 @@ describe('ReportRenderer', () => {
|
|
|
165
197
|
const warningResults = Object.assign({}, sampleResults, {
|
|
166
198
|
runWarnings: ['[I am a link](https://example.com/)'],
|
|
167
199
|
});
|
|
168
|
-
const container = renderer._dom.
|
|
200
|
+
const container = renderer._dom.document().body;
|
|
169
201
|
const output = renderer.renderReport(warningResults, container);
|
|
170
202
|
|
|
171
203
|
const warningEls = output.querySelectorAll('.lh-warnings--toplevel ul li a');
|
|
@@ -179,17 +211,15 @@ describe('ReportRenderer', () => {
|
|
|
179
211
|
assert.ok(/Generated by Lighthouse \d/.test(footerContent), 'includes lh version');
|
|
180
212
|
assert.ok(footerContent.match(TIMESTAMP_REGEX), 'includes timestamp');
|
|
181
213
|
|
|
182
|
-
// Check
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(
|
|
190
|
-
expect(
|
|
191
|
-
expect(descriptionsTxt).toMatch(/\dx/);
|
|
192
|
-
expect(descriptionsTxt).toContain(sampleResults.userAgent);
|
|
214
|
+
// Check env items were populated.
|
|
215
|
+
const items = Array.from(footer.querySelectorAll('.lh-meta__item'));
|
|
216
|
+
expect(items.length).toBeGreaterThanOrEqual(6);
|
|
217
|
+
|
|
218
|
+
const itemsTxt = items.map(el => `${el.textContent} ${el.title}`).join('\n');
|
|
219
|
+
expect(itemsTxt).toContain('Moto G4');
|
|
220
|
+
expect(itemsTxt).toContain('RTT');
|
|
221
|
+
expect(itemsTxt).toMatch(/\dx/);
|
|
222
|
+
expect(itemsTxt).toContain(sampleResults.environment.networkUserAgent);
|
|
193
223
|
});
|
|
194
224
|
});
|
|
195
225
|
|
|
@@ -198,7 +228,7 @@ describe('ReportRenderer', () => {
|
|
|
198
228
|
// Make sure we have a channel in the LHR.
|
|
199
229
|
assert.ok(lhrChannel.length > 2);
|
|
200
230
|
|
|
201
|
-
const container = renderer._dom.
|
|
231
|
+
const container = renderer._dom.document().body;
|
|
202
232
|
const output = renderer.renderReport(sampleResults, container);
|
|
203
233
|
|
|
204
234
|
const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev'];
|
|
@@ -227,7 +257,7 @@ describe('ReportRenderer', () => {
|
|
|
227
257
|
|
|
228
258
|
assert.ok(notApplicableCount > 20); // Make sure something's being tested.
|
|
229
259
|
|
|
230
|
-
const container = renderer._dom.
|
|
260
|
+
const container = renderer._dom.document().body;
|
|
231
261
|
const reportElement = renderer.renderReport(sampleResults, container);
|
|
232
262
|
const notApplicableElementCount = reportElement
|
|
233
263
|
.querySelectorAll('.lh-audit--notapplicable').length;
|