lighthouse 10.2.0 → 10.3.0-dev.20230614
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/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +4 -6
- package/core/audits/accessibility/html-xml-lang-mismatch.d.ts +10 -0
- package/core/audits/accessibility/html-xml-lang-mismatch.js +47 -0
- package/core/audits/accessibility/input-button-name.d.ts +10 -0
- package/core/audits/accessibility/input-button-name.js +44 -0
- package/core/audits/accessibility/table-fake-caption.d.ts +10 -0
- package/core/audits/accessibility/table-fake-caption.js +47 -0
- package/core/audits/accessibility/td-has-header.d.ts +10 -0
- package/core/audits/accessibility/td-has-header.js +45 -0
- package/core/audits/audit.d.ts +2 -1
- package/core/audits/audit.js +5 -2
- package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +3 -3
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +14 -19
- package/core/audits/deprecations.js +1 -1
- package/core/audits/largest-contentful-paint-element.d.ts +14 -2
- package/core/audits/largest-contentful-paint-element.js +49 -31
- package/core/audits/layout-shift-elements.d.ts +3 -2
- package/core/audits/layout-shift-elements.js +10 -2
- package/core/audits/metrics.js +0 -2
- package/core/audits/network-requests.js +6 -0
- package/core/audits/seo/crawlable-anchors.js +4 -0
- package/core/audits/server-response-time.d.ts +2 -1
- package/core/audits/server-response-time.js +17 -3
- package/core/audits/uses-rel-preconnect.js +20 -4
- package/core/computed/entity-classification.d.ts +14 -1
- package/core/computed/entity-classification.js +61 -6
- package/core/computed/metrics/cumulative-layout-shift.d.ts +2 -9
- package/core/computed/metrics/cumulative-layout-shift.js +3 -16
- package/core/computed/metrics/time-to-first-byte.js +10 -7
- package/core/computed/metrics/timing-summary.js +0 -3
- package/core/config/default-config.js +8 -0
- package/core/gather/driver/execution-context.js +13 -5
- package/core/gather/driver/target-manager.d.ts +13 -0
- package/core/gather/driver/target-manager.js +49 -0
- package/core/gather/gatherers/accessibility.js +19 -20
- package/core/gather/gatherers/anchor-elements.js +2 -0
- package/core/gather/gatherers/global-listeners.js +38 -21
- package/core/gather/gatherers/inspector-issues.js +2 -1
- package/core/gather/gatherers/trace-elements.d.ts +3 -2
- package/core/gather/gatherers/trace-elements.js +10 -19
- package/core/legacy/config/config.d.ts +1 -1
- package/core/legacy/config/config.js +1 -1
- package/core/legacy/gather/driver.d.ts +1 -0
- package/core/legacy/gather/driver.js +11 -0
- package/core/lib/asset-saver.d.ts +4 -0
- package/core/lib/asset-saver.js +19 -0
- package/core/lib/bf-cache-strings.d.ts +3 -0
- package/core/lib/bf-cache-strings.js +248 -232
- package/core/lib/cdt/generated/SourceMap.d.ts +18 -31
- package/core/lib/cdt/generated/SourceMap.js +51 -101
- package/core/lib/dependency-graph/simulator/network-analyzer.d.ts +6 -2
- package/core/lib/dependency-graph/simulator/network-analyzer.js +45 -13
- package/core/lib/deprecation-description.d.ts +17 -0
- package/core/lib/deprecation-description.js +71 -0
- package/core/lib/deprecations-strings.d.ts +282 -63
- package/core/lib/deprecations-strings.js +282 -551
- package/core/lib/lh-error.d.ts +9 -4
- package/core/lib/lh-error.js +15 -11
- package/core/lib/network-request.d.ts +13 -6
- package/core/lib/network-request.js +39 -14
- package/core/lib/stack-packs.js +4 -0
- package/core/lib/tracehouse/trace-processor.js +1 -1
- package/core/lib/url-utils.js +4 -0
- package/core/runner.js +4 -2
- package/dist/report/bundle.esm.js +24 -7
- package/dist/report/flow.js +3 -3
- package/dist/report/standalone.js +8 -8
- package/package.json +13 -13
- package/readme.md +7 -0
- package/report/assets/styles.css +18 -2
- package/report/assets/templates.html +4 -0
- package/report/renderer/category-renderer.js +5 -2
- package/report/renderer/components.js +2 -2
- package/report/renderer/dom.d.ts +4 -1
- package/report/renderer/dom.js +3 -2
- package/shared/localization/locales/ar-XB.json +20 -170
- package/shared/localization/locales/ar.json +20 -170
- package/shared/localization/locales/bg.json +19 -169
- package/shared/localization/locales/ca.json +19 -169
- package/shared/localization/locales/cs.json +19 -169
- package/shared/localization/locales/da.json +19 -169
- package/shared/localization/locales/de.json +19 -169
- package/shared/localization/locales/el.json +19 -169
- package/shared/localization/locales/en-GB.json +9 -159
- package/shared/localization/locales/en-US.json +166 -70
- package/shared/localization/locales/en-XA.json +20 -170
- package/shared/localization/locales/en-XL.json +166 -70
- package/shared/localization/locales/es-419.json +19 -169
- package/shared/localization/locales/es.json +19 -169
- package/shared/localization/locales/fi.json +19 -169
- package/shared/localization/locales/fil.json +17 -167
- package/shared/localization/locales/fr.json +18 -168
- package/shared/localization/locales/he.json +19 -169
- package/shared/localization/locales/hi.json +20 -170
- package/shared/localization/locales/hr.json +19 -169
- package/shared/localization/locales/hu.json +19 -169
- package/shared/localization/locales/id.json +19 -169
- package/shared/localization/locales/it.json +19 -169
- package/shared/localization/locales/ja.json +19 -169
- package/shared/localization/locales/ko.json +19 -169
- package/shared/localization/locales/lt.json +20 -170
- package/shared/localization/locales/lv.json +20 -170
- package/shared/localization/locales/nl.json +18 -168
- package/shared/localization/locales/no.json +19 -169
- package/shared/localization/locales/pl.json +19 -169
- package/shared/localization/locales/pt-PT.json +19 -169
- package/shared/localization/locales/pt.json +19 -169
- package/shared/localization/locales/ro.json +19 -169
- package/shared/localization/locales/ru.json +19 -169
- package/shared/localization/locales/sk.json +20 -170
- package/shared/localization/locales/sl.json +19 -169
- package/shared/localization/locales/sr-Latn.json +19 -169
- package/shared/localization/locales/sr.json +19 -169
- package/shared/localization/locales/sv.json +19 -169
- package/shared/localization/locales/ta.json +19 -169
- package/shared/localization/locales/te.json +19 -169
- package/shared/localization/locales/th.json +19 -169
- package/shared/localization/locales/tr.json +19 -169
- package/shared/localization/locales/uk.json +19 -169
- package/shared/localization/locales/vi.json +20 -170
- package/shared/localization/locales/zh-HK.json +19 -169
- package/shared/localization/locales/zh-TW.json +19 -169
- package/shared/localization/locales/zh.json +19 -169
- package/shared/util.d.ts +6 -0
- package/shared/util.js +14 -1
- package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +2 -1
- package/tsconfig-base.json +1 -1
- package/types/artifacts.d.ts +10 -8
- package/types/audit.d.ts +5 -0
- package/types/gatherer.d.ts +1 -0
- package/types/lhr/audit-details.d.ts +1 -0
- package/types/lhr/audit-result.d.ts +10 -0
- package/types/internal/es-main.d.ts +0 -18
|
@@ -2,8 +2,9 @@ export default ServerResponseTime;
|
|
|
2
2
|
declare class ServerResponseTime extends Audit {
|
|
3
3
|
/**
|
|
4
4
|
* @param {LH.Artifacts.NetworkRequest} record
|
|
5
|
+
* @return {number|null}
|
|
5
6
|
*/
|
|
6
|
-
static calculateResponseTime(record: LH.Artifacts.NetworkRequest): number;
|
|
7
|
+
static calculateResponseTime(record: LH.Artifacts.NetworkRequest): number | null;
|
|
7
8
|
/**
|
|
8
9
|
* @param {LH.Artifacts} artifacts
|
|
9
10
|
* @param {LH.Audit.Context} context
|
|
@@ -43,10 +43,15 @@ class ServerResponseTime extends Audit {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @param {LH.Artifacts.NetworkRequest} record
|
|
46
|
+
* @return {number|null}
|
|
46
47
|
*/
|
|
47
48
|
static calculateResponseTime(record) {
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Lightrider does not have timings for sendEnd, but we do have this timing which should be
|
|
50
|
+
// close to the response time.
|
|
51
|
+
if (global.isLightrider && record.lrStatistics) return record.lrStatistics.requestMs;
|
|
52
|
+
|
|
53
|
+
if (!record.timing) return null;
|
|
54
|
+
return record.timing.receiveHeadersStart - record.timing.sendEnd;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -61,6 +66,10 @@ class ServerResponseTime extends Audit {
|
|
|
61
66
|
const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
62
67
|
|
|
63
68
|
const responseTime = ServerResponseTime.calculateResponseTime(mainResource);
|
|
69
|
+
if (responseTime === null) {
|
|
70
|
+
throw new Error('no timing found for main resource');
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
const passed = responseTime < TOO_SLOW_THRESHOLD_MS;
|
|
65
74
|
const displayValue = str_(UIStrings.displayValue, {timeInMs: responseTime});
|
|
66
75
|
|
|
@@ -70,10 +79,11 @@ class ServerResponseTime extends Audit {
|
|
|
70
79
|
{key: 'responseTime', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnTimeSpent)},
|
|
71
80
|
];
|
|
72
81
|
|
|
82
|
+
const overallSavingsMs = Math.max(responseTime - TARGET_MS, 0);
|
|
73
83
|
const details = Audit.makeOpportunityDetails(
|
|
74
84
|
headings,
|
|
75
85
|
[{url: mainResource.url, responseTime}],
|
|
76
|
-
{overallSavingsMs
|
|
86
|
+
{overallSavingsMs}
|
|
77
87
|
);
|
|
78
88
|
|
|
79
89
|
return {
|
|
@@ -82,6 +92,10 @@ class ServerResponseTime extends Audit {
|
|
|
82
92
|
score: Number(passed),
|
|
83
93
|
displayValue,
|
|
84
94
|
details,
|
|
95
|
+
metricSavings: {
|
|
96
|
+
FCP: overallSavingsMs,
|
|
97
|
+
LCP: overallSavingsMs,
|
|
98
|
+
},
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
}
|
|
@@ -72,7 +72,7 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
72
72
|
* @return {boolean}
|
|
73
73
|
*/
|
|
74
74
|
static hasValidTiming(record) {
|
|
75
|
-
return !!record.timing && record.timing.connectEnd
|
|
75
|
+
return !!record.timing && record.timing.connectEnd >= 0 && record.timing.connectStart >= 0;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -81,11 +81,27 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
81
81
|
* @return {boolean}
|
|
82
82
|
*/
|
|
83
83
|
static hasAlreadyConnectedToOrigin(record) {
|
|
84
|
-
return
|
|
85
|
-
|
|
84
|
+
if (!record.timing) return false;
|
|
85
|
+
|
|
86
|
+
// When these values are given as -1, that means the page has
|
|
87
|
+
// a connection for this origin and paid these costs already.
|
|
88
|
+
if (
|
|
89
|
+
record.timing.dnsStart === -1 && record.timing.dnsEnd === -1 &&
|
|
90
|
+
record.timing.connectStart === -1 && record.timing.connectEnd === -1
|
|
91
|
+
) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Less understood: if the connection setup took no time at all, consider
|
|
96
|
+
// it the same as the above. It is unclear if this is correct, or is even possible.
|
|
97
|
+
if (
|
|
86
98
|
record.timing.dnsEnd - record.timing.dnsStart === 0 &&
|
|
87
99
|
record.timing.connectEnd - record.timing.connectStart === 0
|
|
88
|
-
)
|
|
100
|
+
) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
/**
|
|
@@ -10,12 +10,25 @@ declare const EntityClassificationComputed: typeof EntityClassification & {
|
|
|
10
10
|
};
|
|
11
11
|
/** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */
|
|
12
12
|
declare class EntityClassification {
|
|
13
|
+
/**
|
|
14
|
+
* @param {EntityCache} entityCache
|
|
15
|
+
* @param {string} url
|
|
16
|
+
* @param {string=} extensionName
|
|
17
|
+
* @return {LH.Artifacts.Entity}
|
|
18
|
+
*/
|
|
19
|
+
static makeupChromeExtensionEntity_(entityCache: EntityCache, url: string, extensionName?: string | undefined): LH.Artifacts.Entity;
|
|
13
20
|
/**
|
|
14
21
|
* @param {EntityCache} entityCache
|
|
15
22
|
* @param {string} url
|
|
16
23
|
* @return {LH.Artifacts.Entity | undefined}
|
|
17
24
|
*/
|
|
18
|
-
static
|
|
25
|
+
static _makeUpAnEntity(entityCache: EntityCache, url: string): LH.Artifacts.Entity | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Preload Chrome extensions found in the devtoolsLog into cache.
|
|
28
|
+
* @param {EntityCache} entityCache
|
|
29
|
+
* @param {LH.DevtoolsLog} devtoolsLog
|
|
30
|
+
*/
|
|
31
|
+
static _preloadChromeExtensionsToCache(entityCache: EntityCache, devtoolsLog: import("../index.js").DevtoolsLog): void;
|
|
19
32
|
/**
|
|
20
33
|
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
|
|
21
34
|
* @param {LH.Artifacts.ComputedContext} context
|
|
@@ -13,16 +13,51 @@ import thirdPartyWeb from '../lib/third-party-web.js';
|
|
|
13
13
|
/** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */
|
|
14
14
|
|
|
15
15
|
class EntityClassification {
|
|
16
|
+
/**
|
|
17
|
+
* @param {EntityCache} entityCache
|
|
18
|
+
* @param {string} url
|
|
19
|
+
* @param {string=} extensionName
|
|
20
|
+
* @return {LH.Artifacts.Entity}
|
|
21
|
+
*/
|
|
22
|
+
static makeupChromeExtensionEntity_(entityCache, url, extensionName) {
|
|
23
|
+
const origin = Util.getChromeExtensionOrigin(url);
|
|
24
|
+
const host = new URL(origin).host;
|
|
25
|
+
const name = extensionName || host;
|
|
26
|
+
|
|
27
|
+
const cachedEntity = entityCache.get(origin);
|
|
28
|
+
if (cachedEntity) return cachedEntity;
|
|
29
|
+
|
|
30
|
+
const chromeExtensionEntity = {
|
|
31
|
+
name,
|
|
32
|
+
company: name,
|
|
33
|
+
category: 'Chrome Extension',
|
|
34
|
+
homepage: 'https://chromewebstore.google.com/detail/' + host,
|
|
35
|
+
categories: [],
|
|
36
|
+
domains: [],
|
|
37
|
+
averageExecutionTime: 0,
|
|
38
|
+
totalExecutionTime: 0,
|
|
39
|
+
totalOccurrences: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
entityCache.set(origin, chromeExtensionEntity);
|
|
43
|
+
return chromeExtensionEntity;
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
/**
|
|
17
47
|
* @param {EntityCache} entityCache
|
|
18
48
|
* @param {string} url
|
|
19
49
|
* @return {LH.Artifacts.Entity | undefined}
|
|
20
50
|
*/
|
|
21
|
-
static
|
|
51
|
+
static _makeUpAnEntity(entityCache, url) {
|
|
22
52
|
if (!UrlUtils.isValid(url)) return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
53
|
+
|
|
54
|
+
const parsedUrl = Util.createOrReturnURL(url);
|
|
55
|
+
if (parsedUrl.protocol === 'chrome-extension:') {
|
|
56
|
+
return EntityClassification.makeupChromeExtensionEntity_(entityCache, url);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Make up an entity only for valid http/https URLs.
|
|
60
|
+
if (!parsedUrl.protocol.startsWith('http')) return;
|
|
26
61
|
|
|
27
62
|
const rootDomain = Util.getRootDomain(url);
|
|
28
63
|
if (!rootDomain) return;
|
|
@@ -43,6 +78,24 @@ class EntityClassification {
|
|
|
43
78
|
return unrecognizedEntity;
|
|
44
79
|
}
|
|
45
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Preload Chrome extensions found in the devtoolsLog into cache.
|
|
83
|
+
* @param {EntityCache} entityCache
|
|
84
|
+
* @param {LH.DevtoolsLog} devtoolsLog
|
|
85
|
+
*/
|
|
86
|
+
static _preloadChromeExtensionsToCache(entityCache, devtoolsLog) {
|
|
87
|
+
for (const entry of devtoolsLog.values()) {
|
|
88
|
+
if (entry.method !== 'Runtime.executionContextCreated') continue;
|
|
89
|
+
|
|
90
|
+
const origin = entry.params.context.origin;
|
|
91
|
+
if (!origin.startsWith('chrome-extension:')) continue;
|
|
92
|
+
if (entityCache.has(origin)) continue;
|
|
93
|
+
|
|
94
|
+
EntityClassification.makeupChromeExtensionEntity_(entityCache, origin,
|
|
95
|
+
entry.params.context.name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
/**
|
|
47
100
|
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
|
|
48
101
|
* @param {LH.Artifacts.ComputedContext} context
|
|
@@ -57,12 +110,14 @@ class EntityClassification {
|
|
|
57
110
|
/** @type {Map<LH.Artifacts.Entity, Set<string>>} */
|
|
58
111
|
const urlsByEntity = new Map();
|
|
59
112
|
|
|
113
|
+
EntityClassification._preloadChromeExtensionsToCache(madeUpEntityCache, data.devtoolsLog);
|
|
114
|
+
|
|
60
115
|
for (const record of networkRecords) {
|
|
61
116
|
const {url} = record;
|
|
62
117
|
if (entityByUrl.has(url)) continue;
|
|
63
118
|
|
|
64
119
|
const entity = thirdPartyWeb.getEntity(url) ||
|
|
65
|
-
EntityClassification.
|
|
120
|
+
EntityClassification._makeUpAnEntity(madeUpEntityCache, url);
|
|
66
121
|
if (!entity) continue;
|
|
67
122
|
|
|
68
123
|
const entityURLs = urlsByEntity.get(entity) || new Set();
|
|
@@ -76,7 +131,7 @@ class EntityClassification {
|
|
|
76
131
|
// See https://github.com/GoogleChrome/lighthouse/issues/13706
|
|
77
132
|
const firstPartyUrl = data.URL.mainDocumentUrl || data.URL.finalDisplayedUrl;
|
|
78
133
|
const firstParty = thirdPartyWeb.getEntity(firstPartyUrl) ||
|
|
79
|
-
EntityClassification.
|
|
134
|
+
EntityClassification._makeUpAnEntity(madeUpEntityCache, firstPartyUrl);
|
|
80
135
|
|
|
81
136
|
/**
|
|
82
137
|
* Convenience function to check if a URL belongs to first party.
|
|
@@ -3,6 +3,7 @@ export type LayoutShiftEvent = {
|
|
|
3
3
|
ts: number;
|
|
4
4
|
isMainFrame: boolean;
|
|
5
5
|
weightedScore: number;
|
|
6
|
+
impactedNodes?: LH.Artifacts.TraceImpactedNode[];
|
|
6
7
|
};
|
|
7
8
|
declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
|
|
8
9
|
request: (dependencies: import("../../index.js").Trace, context: import("../../../types/utility-types.js").default.ImmutableObject<{
|
|
@@ -10,7 +11,6 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
|
|
|
10
11
|
}>) => Promise<{
|
|
11
12
|
cumulativeLayoutShift: number;
|
|
12
13
|
cumulativeLayoutShiftMainFrame: number;
|
|
13
|
-
totalCumulativeLayoutShift: number;
|
|
14
14
|
}>;
|
|
15
15
|
};
|
|
16
16
|
declare class CumulativeLayoutShift {
|
|
@@ -32,21 +32,14 @@ declare class CumulativeLayoutShift {
|
|
|
32
32
|
* @return {number}
|
|
33
33
|
*/
|
|
34
34
|
static calculate(layoutShiftEvents: Array<LayoutShiftEvent>): number;
|
|
35
|
-
/**
|
|
36
|
-
* Sum all layout shift events from the entire trace.
|
|
37
|
-
* @param {Array<LayoutShiftEvent>} layoutShiftEvents
|
|
38
|
-
* @return {number}
|
|
39
|
-
*/
|
|
40
|
-
static calculateTotalCumulativeLayoutShift(layoutShiftEvents: Array<LayoutShiftEvent>): number;
|
|
41
35
|
/**
|
|
42
36
|
* @param {LH.Trace} trace
|
|
43
37
|
* @param {LH.Artifacts.ComputedContext} context
|
|
44
|
-
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number
|
|
38
|
+
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
|
|
45
39
|
*/
|
|
46
40
|
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
|
|
47
41
|
cumulativeLayoutShift: number;
|
|
48
42
|
cumulativeLayoutShiftMainFrame: number;
|
|
49
|
-
totalCumulativeLayoutShift: number;
|
|
50
43
|
}>;
|
|
51
44
|
}
|
|
52
45
|
import { ProcessedTrace } from '../processed-trace.js';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {makeComputedArtifact} from '../computed-artifact.js';
|
|
8
8
|
import {ProcessedTrace} from '../processed-trace.js';
|
|
9
9
|
|
|
10
|
-
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
|
|
10
|
+
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number, impactedNodes?: LH.Artifacts.TraceImpactedNode[]}} LayoutShiftEvent */
|
|
11
11
|
|
|
12
12
|
const RECENT_INPUT_WINDOW = 500;
|
|
13
13
|
|
|
@@ -65,6 +65,7 @@ class CumulativeLayoutShift {
|
|
|
65
65
|
ts: event.ts,
|
|
66
66
|
isMainFrame: event.args.data.is_main_frame,
|
|
67
67
|
weightedScore: event.args.data.weighted_score_delta,
|
|
68
|
+
impactedNodes: event.args.data.impacted_nodes,
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -100,19 +101,10 @@ class CumulativeLayoutShift {
|
|
|
100
101
|
return maxScore;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
/**
|
|
104
|
-
* Sum all layout shift events from the entire trace.
|
|
105
|
-
* @param {Array<LayoutShiftEvent>} layoutShiftEvents
|
|
106
|
-
* @return {number}
|
|
107
|
-
*/
|
|
108
|
-
static calculateTotalCumulativeLayoutShift(layoutShiftEvents) {
|
|
109
|
-
return layoutShiftEvents.reduce((sum, e) => sum += e.weightedScore, 0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
104
|
/**
|
|
113
105
|
* @param {LH.Trace} trace
|
|
114
106
|
* @param {LH.Artifacts.ComputedContext} context
|
|
115
|
-
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number
|
|
107
|
+
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
|
|
116
108
|
*/
|
|
117
109
|
static async compute_(trace, context) {
|
|
118
110
|
const processedTrace = await ProcessedTrace.request(trace, context);
|
|
@@ -121,14 +113,9 @@ class CumulativeLayoutShift {
|
|
|
121
113
|
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
|
|
122
114
|
const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
|
|
123
115
|
|
|
124
|
-
// The original Cumulative Layout Shift metric, the sum of all main-frame shift events.
|
|
125
|
-
const totalCumulativeLayoutShift =
|
|
126
|
-
CumulativeLayoutShift.calculateTotalCumulativeLayoutShift(mainFrameShiftEvents);
|
|
127
|
-
|
|
128
116
|
return {
|
|
129
117
|
cumulativeLayoutShift: CumulativeLayoutShift.calculate(allFrameShiftEvents),
|
|
130
118
|
cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents),
|
|
131
|
-
totalCumulativeLayoutShift,
|
|
132
119
|
};
|
|
133
120
|
}
|
|
134
121
|
}
|
|
@@ -26,7 +26,8 @@ class TimeToFirstByte extends NavigationMetric {
|
|
|
26
26
|
|
|
27
27
|
// Estimate when the connection is not warm.
|
|
28
28
|
// TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
|
|
29
|
-
let roundTrips =
|
|
29
|
+
let roundTrips = 2;
|
|
30
|
+
if (!mainResource.protocol.startsWith('h3')) roundTrips += 1; // TCP
|
|
30
31
|
if (mainResource.parsedURL.scheme === 'https') roundTrips += 1;
|
|
31
32
|
const estimatedTTFB = data.settings.throttling.rttMs * roundTrips + observedResponseTime;
|
|
32
33
|
|
|
@@ -40,15 +41,17 @@ class TimeToFirstByte extends NavigationMetric {
|
|
|
40
41
|
* @return {Promise<LH.Artifacts.Metric>}
|
|
41
42
|
*/
|
|
42
43
|
static async computeObservedMetric(data, context) {
|
|
43
|
-
const {processedNavigation} = data;
|
|
44
|
-
const timeOriginTs = processedNavigation.timestamps.timeOrigin;
|
|
45
44
|
const mainResource = await MainResource.request(data, context);
|
|
45
|
+
if (!mainResource.timing) {
|
|
46
|
+
throw new Error('missing timing for main resource');
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
49
|
+
const {processedNavigation} = data;
|
|
50
|
+
const timeOriginTs = processedNavigation.timestamps.timeOrigin;
|
|
51
|
+
const timestampMs =
|
|
52
|
+
mainResource.timing.requestTime * 1000 + mainResource.timing.receiveHeadersStart;
|
|
53
|
+
const timestamp = timestampMs * 1000;
|
|
50
54
|
const timing = (timestamp - timeOriginTs) / 1000;
|
|
51
|
-
|
|
52
55
|
return {timing, timestamp};
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -65,7 +65,6 @@ class TimingSummary {
|
|
|
65
65
|
const {
|
|
66
66
|
cumulativeLayoutShift,
|
|
67
67
|
cumulativeLayoutShiftMainFrame,
|
|
68
|
-
totalCumulativeLayoutShift,
|
|
69
68
|
} = cumulativeLayoutShiftValues || {};
|
|
70
69
|
|
|
71
70
|
/** @type {LH.Artifacts.TimingSummary} */
|
|
@@ -89,7 +88,6 @@ class TimingSummary {
|
|
|
89
88
|
maxPotentialFID: maxPotentialFID?.timing,
|
|
90
89
|
cumulativeLayoutShift,
|
|
91
90
|
cumulativeLayoutShiftMainFrame,
|
|
92
|
-
totalCumulativeLayoutShift,
|
|
93
91
|
|
|
94
92
|
lcpLoadStart: lcpBreakdown?.loadStart,
|
|
95
93
|
lcpLoadEnd: lcpBreakdown?.loadEnd,
|
|
@@ -123,7 +121,6 @@ class TimingSummary {
|
|
|
123
121
|
observedDomContentLoadedTs: processedNavigation?.timestamps.domContentLoaded,
|
|
124
122
|
observedCumulativeLayoutShift: cumulativeLayoutShift,
|
|
125
123
|
observedCumulativeLayoutShiftMainFrame: cumulativeLayoutShiftMainFrame,
|
|
126
|
-
observedTotalCumulativeLayoutShift: totalCumulativeLayoutShift,
|
|
127
124
|
|
|
128
125
|
// Include some visual metrics from speedline
|
|
129
126
|
observedFirstVisualChange: speedline.first,
|
|
@@ -263,7 +263,9 @@ const defaultConfig = {
|
|
|
263
263
|
'accessibility/heading-order',
|
|
264
264
|
'accessibility/html-has-lang',
|
|
265
265
|
'accessibility/html-lang-valid',
|
|
266
|
+
'accessibility/html-xml-lang-mismatch',
|
|
266
267
|
'accessibility/image-alt',
|
|
268
|
+
'accessibility/input-button-name',
|
|
267
269
|
'accessibility/input-image-alt',
|
|
268
270
|
'accessibility/label',
|
|
269
271
|
'accessibility/link-name',
|
|
@@ -273,6 +275,8 @@ const defaultConfig = {
|
|
|
273
275
|
'accessibility/meta-viewport',
|
|
274
276
|
'accessibility/object-alt',
|
|
275
277
|
'accessibility/tabindex',
|
|
278
|
+
'accessibility/table-fake-caption',
|
|
279
|
+
'accessibility/td-has-header',
|
|
276
280
|
'accessibility/td-headers-attr',
|
|
277
281
|
'accessibility/th-has-data-cells',
|
|
278
282
|
'accessibility/valid-lang',
|
|
@@ -527,7 +531,9 @@ const defaultConfig = {
|
|
|
527
531
|
{id: 'heading-order', weight: 2, group: 'a11y-navigation'},
|
|
528
532
|
{id: 'html-has-lang', weight: 3, group: 'a11y-language'},
|
|
529
533
|
{id: 'html-lang-valid', weight: 3, group: 'a11y-language'},
|
|
534
|
+
{id: 'html-xml-lang-mismatch', weight: 2, group: 'a11y-language'},
|
|
530
535
|
{id: 'image-alt', weight: 10, group: 'a11y-names-labels'},
|
|
536
|
+
{id: 'input-button-name', weight: 10, group: 'a11y-names-labels'},
|
|
531
537
|
{id: 'input-image-alt', weight: 10, group: 'a11y-names-labels'},
|
|
532
538
|
{id: 'label', weight: 10, group: 'a11y-names-labels'},
|
|
533
539
|
{id: 'link-name', weight: 3, group: 'a11y-names-labels'},
|
|
@@ -537,6 +543,8 @@ const defaultConfig = {
|
|
|
537
543
|
{id: 'meta-viewport', weight: 10, group: 'a11y-best-practices'},
|
|
538
544
|
{id: 'object-alt', weight: 3, group: 'a11y-names-labels'},
|
|
539
545
|
{id: 'tabindex', weight: 3, group: 'a11y-navigation'},
|
|
546
|
+
{id: 'table-fake-caption', weight: 3, group: 'a11y-tables-lists'},
|
|
547
|
+
{id: 'td-has-header', weight: 10, group: 'a11y-tables-lists'},
|
|
540
548
|
{id: 'td-headers-attr', weight: 3, group: 'a11y-tables-lists'},
|
|
541
549
|
{id: 'th-has-data-cells', weight: 3, group: 'a11y-tables-lists'},
|
|
542
550
|
{id: 'valid-lang', weight: 3, group: 'a11y-language'},
|
|
@@ -125,14 +125,22 @@ class ExecutionContext {
|
|
|
125
125
|
|
|
126
126
|
this._session.setNextProtocolTimeout(timeout);
|
|
127
127
|
const response = await this._session.sendCommand('Runtime.evaluate', evaluationParams);
|
|
128
|
-
|
|
128
|
+
|
|
129
|
+
const ex = response.exceptionDetails;
|
|
130
|
+
if (ex) {
|
|
129
131
|
// An error occurred before we could even create a Promise, should be *very* rare.
|
|
130
132
|
// Also occurs when the expression is not valid JavaScript.
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const elidedExpression = expression.replace(/\s+/g, ' ').substring(0, 100);
|
|
134
|
+
const messageLines = [
|
|
135
|
+
'Runtime.evaluate exception',
|
|
136
|
+
`Expression: ${elidedExpression}\n---- (elided)`,
|
|
137
|
+
!ex.stackTrace ? `Parse error at: ${ex.lineNumber + 1}:${ex.columnNumber + 1}` : null,
|
|
138
|
+
ex.exception?.description || ex.text,
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
const evaluationError = new Error(messageLines.join('\n'));
|
|
141
|
+
return Promise.reject(evaluationError);
|
|
135
142
|
}
|
|
143
|
+
|
|
136
144
|
// Protocol should always return a 'result' object, but it is sometimes undefined. See #6026.
|
|
137
145
|
if (response.result === undefined) {
|
|
138
146
|
return Promise.reject(
|
|
@@ -18,12 +18,15 @@ export class TargetManager extends TargetManager_base {
|
|
|
18
18
|
constructor(cdpSession: LH.Puppeteer.CDPSession);
|
|
19
19
|
_enabled: boolean;
|
|
20
20
|
_rootCdpSession: import("../../../types/puppeteer.js").default.CDPSession;
|
|
21
|
+
_mainFrameId: string;
|
|
21
22
|
/**
|
|
22
23
|
* A map of target id to target/session information. Used to ensure unique
|
|
23
24
|
* attached targets.
|
|
24
25
|
* @type {Map<string, TargetWithSession>}
|
|
25
26
|
*/
|
|
26
27
|
_targetIdToTargets: Map<string, TargetWithSession>;
|
|
28
|
+
/** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
|
|
29
|
+
_executionContextIdToDescriptions: Map<string, LH.Crdp.Runtime.ExecutionContextDescription>;
|
|
27
30
|
/**
|
|
28
31
|
* @param {LH.Puppeteer.CDPSession} cdpSession
|
|
29
32
|
*/
|
|
@@ -32,6 +35,15 @@ export class TargetManager extends TargetManager_base {
|
|
|
32
35
|
* @param {LH.Crdp.Page.FrameNavigatedEvent} frameNavigatedEvent
|
|
33
36
|
*/
|
|
34
37
|
_onFrameNavigated(frameNavigatedEvent: LH.Crdp.Page.FrameNavigatedEvent): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* @param {LH.Crdp.Runtime.ExecutionContextCreatedEvent} event
|
|
40
|
+
*/
|
|
41
|
+
_onExecutionContextCreated(event: LH.Crdp.Runtime.ExecutionContextCreatedEvent): void;
|
|
42
|
+
/**
|
|
43
|
+
* @param {LH.Crdp.Runtime.ExecutionContextDestroyedEvent} event
|
|
44
|
+
*/
|
|
45
|
+
_onExecutionContextDestroyed(event: LH.Crdp.Runtime.ExecutionContextDestroyedEvent): void;
|
|
46
|
+
_onExecutionContextsCleared(): void;
|
|
35
47
|
/**
|
|
36
48
|
* @param {string} sessionId
|
|
37
49
|
* @return {LH.Gatherer.FRProtocolSession}
|
|
@@ -42,6 +54,7 @@ export class TargetManager extends TargetManager_base {
|
|
|
42
54
|
* @return {LH.Gatherer.FRProtocolSession}
|
|
43
55
|
*/
|
|
44
56
|
rootSession(): LH.Gatherer.FRProtocolSession;
|
|
57
|
+
mainFrameExecutionContexts(): import("devtools-protocol").Protocol.Runtime.ExecutionContextDescription[];
|
|
45
58
|
/**
|
|
46
59
|
* Returns a listener for all protocol events from session, and augments the
|
|
47
60
|
* event with the sessionId.
|
|
@@ -40,6 +40,7 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
40
40
|
|
|
41
41
|
this._enabled = false;
|
|
42
42
|
this._rootCdpSession = cdpSession;
|
|
43
|
+
this._mainFrameId = '';
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* A map of target id to target/session information. Used to ensure unique
|
|
@@ -47,9 +48,14 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
47
48
|
* @type {Map<string, TargetWithSession>}
|
|
48
49
|
*/
|
|
49
50
|
this._targetIdToTargets = new Map();
|
|
51
|
+
/** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
|
|
52
|
+
this._executionContextIdToDescriptions = new Map();
|
|
50
53
|
|
|
51
54
|
this._onSessionAttached = this._onSessionAttached.bind(this);
|
|
52
55
|
this._onFrameNavigated = this._onFrameNavigated.bind(this);
|
|
56
|
+
this._onExecutionContextCreated = this._onExecutionContextCreated.bind(this);
|
|
57
|
+
this._onExecutionContextDestroyed = this._onExecutionContextDestroyed.bind(this);
|
|
58
|
+
this._onExecutionContextsCleared = this._onExecutionContextsCleared.bind(this);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
/**
|
|
@@ -97,6 +103,12 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
97
103
|
return this._findSession(rootSessionId);
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
mainFrameExecutionContexts() {
|
|
107
|
+
return [...this._executionContextIdToDescriptions.values()].filter(executionContext => {
|
|
108
|
+
return executionContext.auxData.frameId === this._mainFrameId;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
/**
|
|
101
113
|
* @param {LH.Puppeteer.CDPSession} cdpSession
|
|
102
114
|
*/
|
|
@@ -153,6 +165,27 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
|
|
168
|
+
/**
|
|
169
|
+
* @param {LH.Crdp.Runtime.ExecutionContextCreatedEvent} event
|
|
170
|
+
*/
|
|
171
|
+
_onExecutionContextCreated(event) {
|
|
172
|
+
if (event.context.name === '__puppeteer_utility_world__') return;
|
|
173
|
+
if (event.context.name === 'lighthouse_isolated_context') return;
|
|
174
|
+
|
|
175
|
+
this._executionContextIdToDescriptions.set(event.context.uniqueId, event.context);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {LH.Crdp.Runtime.ExecutionContextDestroyedEvent} event
|
|
180
|
+
*/
|
|
181
|
+
_onExecutionContextDestroyed(event) {
|
|
182
|
+
this._executionContextIdToDescriptions.delete(event.executionContextUniqueId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_onExecutionContextsCleared() {
|
|
186
|
+
this._executionContextIdToDescriptions.clear();
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
/**
|
|
157
190
|
* Returns a listener for all protocol events from session, and augments the
|
|
158
191
|
* event with the sessionId.
|
|
@@ -183,10 +216,17 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
183
216
|
|
|
184
217
|
this._enabled = true;
|
|
185
218
|
this._targetIdToTargets = new Map();
|
|
219
|
+
this._executionContextIdToDescriptions = new Map();
|
|
186
220
|
|
|
187
221
|
this._rootCdpSession.on('Page.frameNavigated', this._onFrameNavigated);
|
|
222
|
+
this._rootCdpSession.on('Runtime.executionContextCreated', this._onExecutionContextCreated);
|
|
223
|
+
this._rootCdpSession.on('Runtime.executionContextDestroyed', this._onExecutionContextDestroyed);
|
|
224
|
+
this._rootCdpSession.on('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
|
|
188
225
|
|
|
189
226
|
await this._rootCdpSession.send('Page.enable');
|
|
227
|
+
await this._rootCdpSession.send('Runtime.enable');
|
|
228
|
+
|
|
229
|
+
this._mainFrameId = (await this._rootCdpSession.send('Page.getFrameTree')).frameTree.frame.id;
|
|
190
230
|
|
|
191
231
|
// Start with the already attached root session.
|
|
192
232
|
await this._onSessionAttached(this._rootCdpSession);
|
|
@@ -197,14 +237,23 @@ class TargetManager extends ProtocolEventEmitter {
|
|
|
197
237
|
*/
|
|
198
238
|
async disable() {
|
|
199
239
|
this._rootCdpSession.off('Page.frameNavigated', this._onFrameNavigated);
|
|
240
|
+
this._rootCdpSession.off('Runtime.executionContextCreated', this._onExecutionContextCreated);
|
|
241
|
+
this._rootCdpSession.off('Runtime.executionContextDestroyed',
|
|
242
|
+
this._onExecutionContextDestroyed);
|
|
243
|
+
this._rootCdpSession.off('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
|
|
200
244
|
|
|
201
245
|
for (const {cdpSession, protocolListener} of this._targetIdToTargets.values()) {
|
|
202
246
|
cdpSession.off('*', protocolListener);
|
|
203
247
|
cdpSession.off('sessionattached', this._onSessionAttached);
|
|
204
248
|
}
|
|
205
249
|
|
|
250
|
+
await this._rootCdpSession.send('Page.disable');
|
|
251
|
+
await this._rootCdpSession.send('Runtime.disable');
|
|
252
|
+
|
|
206
253
|
this._enabled = false;
|
|
207
254
|
this._targetIdToTargets = new Map();
|
|
255
|
+
this._executionContextIdToDescriptions = new Map();
|
|
256
|
+
this._mainFrameId = '';
|
|
208
257
|
}
|
|
209
258
|
}
|
|
210
259
|
|
|
@@ -39,34 +39,33 @@ async function runA11yChecks() {
|
|
|
39
39
|
resultTypes: ['violations', 'inapplicable'],
|
|
40
40
|
rules: {
|
|
41
41
|
// Consider http://go/prcpg for expert review of the aXe rules.
|
|
42
|
-
'tabindex': {enabled: true},
|
|
43
42
|
'accesskeys': {enabled: true},
|
|
44
|
-
'heading-order': {enabled: true},
|
|
45
|
-
'meta-viewport': {enabled: true},
|
|
46
|
-
'duplicate-id': {enabled: false},
|
|
47
|
-
'table-fake-caption': {enabled: false},
|
|
48
|
-
'td-has-header': {enabled: false},
|
|
49
|
-
'marquee': {enabled: false},
|
|
50
43
|
'area-alt': {enabled: false},
|
|
51
|
-
'
|
|
44
|
+
'aria-roledescription': {enabled: false},
|
|
45
|
+
'aria-treeitem-name': {enabled: true},
|
|
46
|
+
'audio-caption': {enabled: false},
|
|
52
47
|
'blink': {enabled: false},
|
|
53
|
-
'
|
|
48
|
+
'duplicate-id': {enabled: false},
|
|
49
|
+
'frame-focusable-content': {enabled: false},
|
|
50
|
+
'frame-title-unique': {enabled: false},
|
|
51
|
+
'heading-order': {enabled: true},
|
|
52
|
+
'html-xml-lang-mismatch': {enabled: true},
|
|
54
53
|
'identical-links-same-purpose': {enabled: false},
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
54
|
+
'input-button-name': {enabled: true},
|
|
55
|
+
'link-in-text-block': {enabled: false},
|
|
56
|
+
'marquee': {enabled: false},
|
|
57
|
+
'meta-viewport': {enabled: true},
|
|
59
58
|
// https://github.com/dequelabs/axe-core/issues/2958
|
|
60
59
|
'nested-interactive': {enabled: false},
|
|
61
|
-
'
|
|
62
|
-
'aria-roledescription': {enabled: false},
|
|
63
|
-
'scrollable-region-focusable': {enabled: false},
|
|
64
|
-
// TODO(paulirish): create audits and enable these 5.
|
|
65
|
-
'input-button-name': {enabled: false},
|
|
60
|
+
'no-autoplay-audio': {enabled: false},
|
|
66
61
|
'role-img-alt': {enabled: false},
|
|
62
|
+
'scrollable-region-focusable': {enabled: false},
|
|
67
63
|
'select-name': {enabled: false},
|
|
68
|
-
'
|
|
69
|
-
'
|
|
64
|
+
'server-side-image-map': {enabled: false},
|
|
65
|
+
'svg-img-alt': {enabled: false},
|
|
66
|
+
'tabindex': {enabled: true},
|
|
67
|
+
'table-fake-caption': {enabled: true},
|
|
68
|
+
'td-has-header': {enabled: true},
|
|
70
69
|
},
|
|
71
70
|
});
|
|
72
71
|
|