lighthouse 8.6.0-dev.20211102 → 8.6.0-dev.20211106
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 +8 -8
- package/dist/report/standalone.js +40 -34
- package/flow-report/assets/standalone-flow-template.html +3 -2
- package/flow-report/src/icons.tsx +9 -0
- package/flow-report/src/sidebar/sidebar.tsx +18 -5
- package/flow-report/src/wrappers/report-renderer.tsx +1 -0
- package/flow-report/src/wrappers/report.tsx +2 -3
- package/flow-report/standalone-flow.tsx +4 -3
- package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
- package/package.json +1 -1
- package/report/assets/standalone-template.html +2 -3
- package/report/assets/styles.css +1 -1
- package/report/clients/bundle.js +1 -0
- package/report/clients/standalone.js +6 -13
- package/report/renderer/api.js +32 -0
- package/report/renderer/category-renderer.js +1 -1
- package/report/renderer/components.js +26 -26
- package/report/renderer/dom.js +16 -1
- package/report/renderer/drop-down-menu.js +2 -2
- package/report/renderer/element-screenshot-renderer.js +6 -6
- package/report/renderer/features-util.js +1 -1
- package/report/renderer/report-renderer.js +32 -9
- package/report/renderer/report-ui-features.js +34 -30
- package/report/renderer/swap-locale-feature.js +3 -3
- package/report/renderer/topbar-features.js +52 -48
- package/report/test/clients/bundle-test.js +8 -0
- package/report/test/renderer/report-renderer-axe-test.js +5 -2
- package/report/test/renderer/report-renderer-test.js +13 -10
- package/report/test/renderer/report-ui-features-test.js +10 -1
- package/report/test-assets/faux-psi-template.html +2 -7
- package/report/test-assets/faux-psi.js +15 -34
- package/report/types/html-renderer.d.ts +2 -0
- package/report/types/report-renderer.d.ts +23 -0
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef InstallOverlayFeatureParams
|
|
20
20
|
* @property {DOM} dom
|
|
21
|
-
* @property {Element}
|
|
21
|
+
* @property {Element} rootEl
|
|
22
22
|
* @property {Element} overlayContainerEl
|
|
23
23
|
* @property {LH.Audit.Details.FullPageScreenshot} fullPageScreenshot
|
|
24
24
|
*/
|
|
@@ -135,7 +135,7 @@ export class ElementScreenshotRenderer {
|
|
|
135
135
|
* @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
|
|
136
136
|
*/
|
|
137
137
|
static installFullPageScreenshot(el, screenshot) {
|
|
138
|
-
el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`);
|
|
138
|
+
el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -143,14 +143,14 @@ export class ElementScreenshotRenderer {
|
|
|
143
143
|
* @param {InstallOverlayFeatureParams} opts
|
|
144
144
|
*/
|
|
145
145
|
static installOverlayFeature(opts) {
|
|
146
|
-
const {dom,
|
|
146
|
+
const {dom, rootEl, overlayContainerEl, fullPageScreenshot} = opts;
|
|
147
147
|
const screenshotOverlayClass = 'lh-screenshot-overlay--enabled';
|
|
148
148
|
// Don't install the feature more than once.
|
|
149
|
-
if (
|
|
150
|
-
|
|
149
|
+
if (rootEl.classList.contains(screenshotOverlayClass)) return;
|
|
150
|
+
rootEl.classList.add(screenshotOverlayClass);
|
|
151
151
|
|
|
152
152
|
// Add a single listener to the provided element to handle all clicks within (event delegation).
|
|
153
|
-
|
|
153
|
+
rootEl.addEventListener('click', e => {
|
|
154
154
|
const target = /** @type {?HTMLElement} */ (e.target);
|
|
155
155
|
if (!target) return;
|
|
156
156
|
// Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @param {boolean} [force]
|
|
15
15
|
*/
|
|
16
16
|
export function toggleDarkTheme(dom, force) {
|
|
17
|
-
const el = dom.
|
|
17
|
+
const el = dom.rootEl;
|
|
18
18
|
// This seems unnecessary, but in DevTools, passing "undefined" as the second
|
|
19
19
|
// parameter acts like passing "false".
|
|
20
20
|
// https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819
|
|
@@ -36,22 +36,43 @@ export class ReportRenderer {
|
|
|
36
36
|
constructor(dom) {
|
|
37
37
|
/** @type {DOM} */
|
|
38
38
|
this._dom = dom;
|
|
39
|
+
/** @type {LH.Renderer.Options} */
|
|
40
|
+
this._opts = {};
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* @param {LH.Result} lhr
|
|
43
|
-
* @param {
|
|
45
|
+
* @param {HTMLElement?} rootEl Report root element containing the report
|
|
46
|
+
* @param {LH.Renderer.Options=} opts
|
|
44
47
|
* @return {!Element}
|
|
45
48
|
*/
|
|
46
|
-
renderReport(lhr,
|
|
49
|
+
renderReport(lhr, rootEl, opts) {
|
|
50
|
+
// Allow legacy report rendering API
|
|
51
|
+
if (!this._dom.rootEl && rootEl) {
|
|
52
|
+
console.warn('Please adopt the new report API in renderer/api.js.');
|
|
53
|
+
const closestRoot = rootEl.closest('.lh-root');
|
|
54
|
+
if (closestRoot) {
|
|
55
|
+
this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot);
|
|
56
|
+
} else {
|
|
57
|
+
rootEl.classList.add('lh-root', 'lh-vars');
|
|
58
|
+
this._dom.rootEl = rootEl;
|
|
59
|
+
}
|
|
60
|
+
} else if (this._dom.rootEl && rootEl) {
|
|
61
|
+
// Handle legacy flow-report case
|
|
62
|
+
this._dom.rootEl = rootEl;
|
|
63
|
+
}
|
|
64
|
+
if (opts) {
|
|
65
|
+
this._opts = opts;
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown');
|
|
48
69
|
|
|
49
70
|
const report = Util.prepareReportResult(lhr);
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
this._dom.rootEl.textContent = ''; // Remove previous report.
|
|
73
|
+
this._dom.rootEl.appendChild(this._renderReport(report));
|
|
53
74
|
|
|
54
|
-
return
|
|
75
|
+
return this._dom.rootEl;
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
/**
|
|
@@ -197,7 +218,7 @@ export class ReportRenderer {
|
|
|
197
218
|
gaugeWrapperEl.addEventListener('click', e => {
|
|
198
219
|
if (!gaugeWrapperEl.matches('[href^="#"]')) return;
|
|
199
220
|
const selector = gaugeWrapperEl.getAttribute('href');
|
|
200
|
-
const reportRoot =
|
|
221
|
+
const reportRoot = this._dom.rootEl;
|
|
201
222
|
if (!selector || !reportRoot) return;
|
|
202
223
|
const destEl = this._dom.find(selector, reportRoot);
|
|
203
224
|
e.preventDefault();
|
|
@@ -301,9 +322,11 @@ export class ReportRenderer {
|
|
|
301
322
|
|
|
302
323
|
const reportFragment = this._dom.createFragment();
|
|
303
324
|
reportFragment.append(this._dom.createComponent('styles'));
|
|
304
|
-
const topbarDocumentFragment = this._renderReportTopbar(report);
|
|
305
325
|
|
|
306
|
-
|
|
326
|
+
if (!this._opts.omitTopbar) {
|
|
327
|
+
reportFragment.appendChild(this._renderReportTopbar(report));
|
|
328
|
+
}
|
|
329
|
+
|
|
307
330
|
reportFragment.appendChild(reportContainer);
|
|
308
331
|
reportContainer.appendChild(headerContainer);
|
|
309
332
|
reportContainer.appendChild(reportSection);
|
|
@@ -311,7 +334,7 @@ export class ReportRenderer {
|
|
|
311
334
|
|
|
312
335
|
if (fullPageScreenshot) {
|
|
313
336
|
ElementScreenshotRenderer.installFullPageScreenshot(
|
|
314
|
-
|
|
337
|
+
this._dom.rootEl, fullPageScreenshot.screenshot);
|
|
315
338
|
}
|
|
316
339
|
|
|
317
340
|
return reportFragment;
|
|
@@ -40,16 +40,17 @@ function getTableRows(tableEl) {
|
|
|
40
40
|
export class ReportUIFeatures {
|
|
41
41
|
/**
|
|
42
42
|
* @param {DOM} dom
|
|
43
|
+
* @param {LH.Renderer.Options} opts
|
|
43
44
|
*/
|
|
44
|
-
constructor(dom) {
|
|
45
|
+
constructor(dom, opts = {}) {
|
|
45
46
|
/** @type {LH.Result} */
|
|
46
47
|
this.json; // eslint-disable-line no-unused-expressions
|
|
47
48
|
/** @type {DOM} */
|
|
48
49
|
this._dom = dom;
|
|
49
|
-
/** @type {Document} */
|
|
50
|
-
this._document = this._dom.document();
|
|
51
|
-
this._topbar = new TopbarFeatures(this, dom);
|
|
52
50
|
|
|
51
|
+
this._opts = opts;
|
|
52
|
+
|
|
53
|
+
this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom);
|
|
53
54
|
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -61,16 +62,19 @@ export class ReportUIFeatures {
|
|
|
61
62
|
initFeatures(lhr) {
|
|
62
63
|
this.json = lhr;
|
|
63
64
|
|
|
64
|
-
this._topbar
|
|
65
|
-
|
|
65
|
+
if (this._topbar) {
|
|
66
|
+
this._topbar.enable(lhr);
|
|
67
|
+
this._topbar.resetUIState();
|
|
68
|
+
}
|
|
66
69
|
this._setupMediaQueryListeners();
|
|
67
70
|
this._setupThirdPartyFilter();
|
|
68
|
-
this._setupElementScreenshotOverlay(this._dom.
|
|
71
|
+
this._setupElementScreenshotOverlay(this._dom.rootEl);
|
|
69
72
|
|
|
70
73
|
let turnOffTheLights = false;
|
|
71
74
|
// Do not query the system preferences for DevTools - DevTools should only apply dark theme
|
|
72
75
|
// if dark is selected in the settings panel.
|
|
73
|
-
|
|
76
|
+
const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks;
|
|
77
|
+
if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
74
78
|
turnOffTheLights = true;
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -94,7 +98,7 @@ export class ReportUIFeatures {
|
|
|
94
98
|
const hasMetricError = lhr.categories.performance && lhr.categories.performance.auditRefs
|
|
95
99
|
.some(audit => Boolean(audit.group === 'metrics' && lhr.audits[audit.id].errorMessage));
|
|
96
100
|
if (hasMetricError) {
|
|
97
|
-
const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this.
|
|
101
|
+
const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._dom.rootEl);
|
|
98
102
|
toggleInputEl.checked = true;
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -109,7 +113,7 @@ export class ReportUIFeatures {
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
// Fill in all i18n data.
|
|
112
|
-
for (const node of this._dom.findAll('[data-i18n]', this._dom.
|
|
116
|
+
for (const node of this._dom.findAll('[data-i18n]', this._dom.rootEl)) {
|
|
113
117
|
// These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
|
|
114
118
|
// so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
|
|
115
119
|
const i18nKey = node.getAttribute('data-i18n');
|
|
@@ -119,18 +123,15 @@ export class ReportUIFeatures {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/**
|
|
122
|
-
* @param {{
|
|
126
|
+
* @param {{text: string, icon?: string, onClick: () => void}} opts
|
|
123
127
|
*/
|
|
124
128
|
addButton(opts) {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
|
|
129
|
-
const containerEl = opts.container || metricsEl;
|
|
130
|
-
if (!containerEl) return;
|
|
129
|
+
// Use qSA directly to as we don't want to throw (if this element is missing).
|
|
130
|
+
const metricsEl = this._dom.rootEl.querySelector('.lh-audit-group--metrics');
|
|
131
|
+
if (!metricsEl) return;
|
|
131
132
|
|
|
132
|
-
let buttonsEl =
|
|
133
|
-
if (!buttonsEl) buttonsEl = this._dom.createChildOf(
|
|
133
|
+
let buttonsEl = metricsEl.querySelector('.lh-buttons');
|
|
134
|
+
if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons');
|
|
134
135
|
|
|
135
136
|
const classes = [
|
|
136
137
|
'lh-button',
|
|
@@ -150,8 +151,10 @@ export class ReportUIFeatures {
|
|
|
150
151
|
* @return {string}
|
|
151
152
|
*/
|
|
152
153
|
getReportHtml() {
|
|
153
|
-
this._topbar
|
|
154
|
-
|
|
154
|
+
if (this._topbar) {
|
|
155
|
+
this._topbar.resetUIState();
|
|
156
|
+
}
|
|
157
|
+
return `<!doctype html><body>${this._dom.rootEl.outerHTML}`;
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
/**
|
|
@@ -163,7 +166,7 @@ export class ReportUIFeatures {
|
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
_enableFireworks() {
|
|
166
|
-
const scoresContainer = this._dom.find('.lh-scores-container', this.
|
|
169
|
+
const scoresContainer = this._dom.find('.lh-scores-container', this._dom.rootEl);
|
|
167
170
|
scoresContainer.classList.add('lh-score100');
|
|
168
171
|
scoresContainer.addEventListener('click', _ => {
|
|
169
172
|
scoresContainer.classList.toggle('lh-fireworks-paused');
|
|
@@ -183,7 +186,9 @@ export class ReportUIFeatures {
|
|
|
183
186
|
* be in their closed state (not opened) and the templates should be unstamped.
|
|
184
187
|
*/
|
|
185
188
|
_resetUIState() {
|
|
186
|
-
this._topbar
|
|
189
|
+
if (this._topbar) {
|
|
190
|
+
this._topbar.resetUIState();
|
|
191
|
+
}
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
/**
|
|
@@ -191,8 +196,7 @@ export class ReportUIFeatures {
|
|
|
191
196
|
* @param {MediaQueryList|MediaQueryListEvent} mql
|
|
192
197
|
*/
|
|
193
198
|
onMediaQueryChange(mql) {
|
|
194
|
-
|
|
195
|
-
root.classList.toggle('lh-narrow', mql.matches);
|
|
199
|
+
this._dom.rootEl.classList.toggle('lh-narrow', mql.matches);
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
_setupThirdPartyFilter() {
|
|
@@ -209,7 +213,7 @@ export class ReportUIFeatures {
|
|
|
209
213
|
];
|
|
210
214
|
|
|
211
215
|
// Get all tables with a text url column.
|
|
212
|
-
const tables = Array.from(this.
|
|
216
|
+
const tables = Array.from(this._dom.rootEl.querySelectorAll('table.lh-table'));
|
|
213
217
|
const tablesWithUrls = tables
|
|
214
218
|
.filter(el =>
|
|
215
219
|
el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
|
|
@@ -275,9 +279,9 @@ export class ReportUIFeatures {
|
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
/**
|
|
278
|
-
* @param {Element}
|
|
282
|
+
* @param {Element} rootEl
|
|
279
283
|
*/
|
|
280
|
-
_setupElementScreenshotOverlay(
|
|
284
|
+
_setupElementScreenshotOverlay(rootEl) {
|
|
281
285
|
const fullPageScreenshot =
|
|
282
286
|
this.json.audits['full-page-screenshot'] &&
|
|
283
287
|
this.json.audits['full-page-screenshot'].details &&
|
|
@@ -287,8 +291,8 @@ export class ReportUIFeatures {
|
|
|
287
291
|
|
|
288
292
|
ElementScreenshotRenderer.installOverlayFeature({
|
|
289
293
|
dom: this._dom,
|
|
290
|
-
|
|
291
|
-
overlayContainerEl:
|
|
294
|
+
rootEl: rootEl,
|
|
295
|
+
overlayContainerEl: rootEl,
|
|
292
296
|
fullPageScreenshot,
|
|
293
297
|
});
|
|
294
298
|
}
|
|
@@ -33,17 +33,17 @@ export class SwapLocaleFeature {
|
|
|
33
33
|
throw new Error('missing icuMessagePaths');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
this._dom.find('.lh-tools-locale', this._dom.
|
|
36
|
+
this._dom.find('.lh-tools-locale', this._dom.rootEl).classList.remove('lh-hidden');
|
|
37
37
|
|
|
38
38
|
const currentLocale = this._reportUIFeatures.json.configSettings.locale;
|
|
39
39
|
|
|
40
|
-
const containerEl = this._dom.find('.lh-tools-locale__selector-wrapper', this._dom.
|
|
40
|
+
const containerEl = this._dom.find('.lh-tools-locale__selector-wrapper', this._dom.rootEl);
|
|
41
41
|
containerEl.removeAttribute('aria-hidden');
|
|
42
42
|
const selectEl = this._dom.createChildOf(containerEl, 'select', 'lh-locale-selector');
|
|
43
43
|
selectEl.name = 'lh-locale-list';
|
|
44
44
|
selectEl.setAttribute('role', 'menuitem');
|
|
45
45
|
|
|
46
|
-
const toggleEl = this._dom.find('.lh-tool-locale__button', this._dom.
|
|
46
|
+
const toggleEl = this._dom.find('.lh-tool-locale__button', this._dom.rootEl);
|
|
47
47
|
toggleEl.addEventListener('click', () => {
|
|
48
48
|
toggleEl.classList.toggle('lh-active');
|
|
49
49
|
});
|
|
@@ -24,15 +24,13 @@ export class TopbarFeatures {
|
|
|
24
24
|
this.lhr; // eslint-disable-line no-unused-expressions
|
|
25
25
|
this._reportUIFeatures = reportUIFeatures;
|
|
26
26
|
this._dom = dom;
|
|
27
|
-
/** @type {Document} */
|
|
28
|
-
this._document = this._dom.document();
|
|
29
27
|
this._dropDownMenu = new DropDownMenu(this._dom);
|
|
30
28
|
this._copyAttempt = false;
|
|
31
29
|
/** @type {HTMLElement} */
|
|
32
30
|
this.topbarEl; // eslint-disable-line no-unused-expressions
|
|
33
31
|
/** @type {HTMLElement} */
|
|
34
|
-
this.
|
|
35
|
-
/** @type {HTMLElement} */
|
|
32
|
+
this.categoriesEl; // eslint-disable-line no-unused-expressions
|
|
33
|
+
/** @type {HTMLElement?} */
|
|
36
34
|
this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
|
|
37
35
|
/** @type {HTMLElement} */
|
|
38
36
|
this.highlightEl; // eslint-disable-line no-unused-expressions
|
|
@@ -40,7 +38,6 @@ export class TopbarFeatures {
|
|
|
40
38
|
this.onKeyUp = this.onKeyUp.bind(this);
|
|
41
39
|
this.onCopy = this.onCopy.bind(this);
|
|
42
40
|
this.collapseAllDetails = this.collapseAllDetails.bind(this);
|
|
43
|
-
this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/**
|
|
@@ -48,33 +45,15 @@ export class TopbarFeatures {
|
|
|
48
45
|
*/
|
|
49
46
|
enable(lhr) {
|
|
50
47
|
this.lhr = lhr;
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
48
|
+
this._dom.rootEl.addEventListener('keyup', this.onKeyUp);
|
|
49
|
+
this._dom.document().addEventListener('copy', this.onCopy);
|
|
53
50
|
this._dropDownMenu.setup(this.onDropDownMenuClick);
|
|
54
51
|
this._setUpCollapseDetailsAfterPrinting();
|
|
55
52
|
|
|
56
|
-
const topbarLogo = this._dom.find('.lh-topbar__logo', this.
|
|
53
|
+
const topbarLogo = this._dom.find('.lh-topbar__logo', this._dom.rootEl);
|
|
57
54
|
topbarLogo.addEventListener('click', () => toggleDarkTheme(this._dom));
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
if (Object.keys(this.lhr.categories).length >= 2) {
|
|
61
|
-
this._setupStickyHeaderElements();
|
|
62
|
-
const containerEl = this._dom.find('.lh-container', this._document);
|
|
63
|
-
const elToAddScrollListener = this._getScrollParent(containerEl);
|
|
64
|
-
elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
|
|
65
|
-
|
|
66
|
-
// Use ResizeObserver where available.
|
|
67
|
-
// TODO: there is an issue with incorrect position numbers and, as a result, performance
|
|
68
|
-
// issues due to layout thrashing.
|
|
69
|
-
// See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
|
|
70
|
-
// For now, limit to DevTools.
|
|
71
|
-
if (this._dom.isDevTools()) {
|
|
72
|
-
const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
|
|
73
|
-
resizeObserver.observe(containerEl);
|
|
74
|
-
} else {
|
|
75
|
-
window.addEventListener('resize', this._updateStickyHeaderOnScroll);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
56
|
+
this._setupStickyHeader();
|
|
78
57
|
}
|
|
79
58
|
|
|
80
59
|
/**
|
|
@@ -112,7 +91,7 @@ export class TopbarFeatures {
|
|
|
112
91
|
try {
|
|
113
92
|
this._reportUIFeatures._saveFile(new Blob([htmlStr], {type: 'text/html'}));
|
|
114
93
|
} catch (e) {
|
|
115
|
-
this._dom.fireEventOn('lh-log', this.
|
|
94
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {
|
|
116
95
|
cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
|
|
117
96
|
});
|
|
118
97
|
}
|
|
@@ -152,7 +131,7 @@ export class TopbarFeatures {
|
|
|
152
131
|
e.preventDefault();
|
|
153
132
|
e.clipboardData.setData('text/plain', JSON.stringify(this.lhr, null, 2));
|
|
154
133
|
|
|
155
|
-
this._dom.fireEventOn('lh-log', this.
|
|
134
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {
|
|
156
135
|
cmd: 'log', msg: 'Report JSON copied to clipboard',
|
|
157
136
|
});
|
|
158
137
|
}
|
|
@@ -164,28 +143,28 @@ export class TopbarFeatures {
|
|
|
164
143
|
* Copies the report JSON to the clipboard (if supported by the browser).
|
|
165
144
|
*/
|
|
166
145
|
onCopyButtonClick() {
|
|
167
|
-
this._dom.fireEventOn('lh-analytics', this.
|
|
146
|
+
this._dom.fireEventOn('lh-analytics', this._dom.document(), {
|
|
168
147
|
cmd: 'send',
|
|
169
148
|
fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
|
|
170
149
|
});
|
|
171
150
|
|
|
172
151
|
try {
|
|
173
|
-
if (this.
|
|
152
|
+
if (this._dom.document().queryCommandSupported('copy')) {
|
|
174
153
|
this._copyAttempt = true;
|
|
175
154
|
|
|
176
155
|
// Note: In Safari 10.0.1, execCommand('copy') returns true if there's
|
|
177
156
|
// a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
|
|
178
|
-
if (!this.
|
|
157
|
+
if (!this._dom.document().execCommand('copy')) {
|
|
179
158
|
this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
|
|
180
159
|
|
|
181
|
-
this._dom.fireEventOn('lh-log', this.
|
|
160
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {
|
|
182
161
|
cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
|
|
183
162
|
});
|
|
184
163
|
}
|
|
185
164
|
}
|
|
186
165
|
} catch (e) {
|
|
187
166
|
this._copyAttempt = false;
|
|
188
|
-
this._dom.fireEventOn('lh-log', this.
|
|
167
|
+
this._dom.fireEventOn('lh-log', this._dom.document(), {cmd: 'log', msg: e.message});
|
|
189
168
|
}
|
|
190
169
|
}
|
|
191
170
|
|
|
@@ -206,7 +185,7 @@ export class TopbarFeatures {
|
|
|
206
185
|
* open a `<details>` element.
|
|
207
186
|
*/
|
|
208
187
|
expandAllDetails() {
|
|
209
|
-
const details = this._dom.findAll('.lh-categories details', this.
|
|
188
|
+
const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
|
|
210
189
|
details.map(detail => detail.open = true);
|
|
211
190
|
}
|
|
212
191
|
|
|
@@ -215,7 +194,7 @@ export class TopbarFeatures {
|
|
|
215
194
|
* open a `<details>` element.
|
|
216
195
|
*/
|
|
217
196
|
collapseAllDetails() {
|
|
218
|
-
const details = this._dom.findAll('.lh-categories details', this.
|
|
197
|
+
const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
|
|
219
198
|
details.map(detail => detail.open = false);
|
|
220
199
|
}
|
|
221
200
|
|
|
@@ -235,7 +214,7 @@ export class TopbarFeatures {
|
|
|
235
214
|
/**
|
|
236
215
|
* Finds the first scrollable ancestor of `element`. Falls back to the document.
|
|
237
216
|
* @param {Element} element
|
|
238
|
-
* @return {
|
|
217
|
+
* @return {Element | Document}
|
|
239
218
|
*/
|
|
240
219
|
_getScrollParent(element) {
|
|
241
220
|
const {overflowY} = window.getComputedStyle(element);
|
|
@@ -273,24 +252,49 @@ export class TopbarFeatures {
|
|
|
273
252
|
}
|
|
274
253
|
}
|
|
275
254
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this.
|
|
279
|
-
this.
|
|
255
|
+
_setupStickyHeader() {
|
|
256
|
+
// Cache these elements to avoid qSA on each onscroll.
|
|
257
|
+
this.topbarEl = this._dom.find('div.lh-topbar', this._dom.rootEl);
|
|
258
|
+
this.categoriesEl = this._dom.find('div.lh-categories', this._dom.rootEl);
|
|
259
|
+
|
|
260
|
+
// Defer behind rAF to avoid forcing layout.
|
|
261
|
+
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
|
|
262
|
+
// Only present in the DOM if it'll be used (>=2 categories)
|
|
263
|
+
try {
|
|
264
|
+
this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl);
|
|
265
|
+
} catch {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
280
268
|
|
|
281
|
-
|
|
282
|
-
|
|
269
|
+
// Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
|
|
270
|
+
this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
|
|
271
|
+
|
|
272
|
+
// Update sticky header visibility and highlight when page scrolls/resizes.
|
|
273
|
+
const scrollParent = this._getScrollParent(this._dom.rootEl);
|
|
274
|
+
// The 'scroll' handler must be should be on {Element | Document}...
|
|
275
|
+
scrollParent.addEventListener('scroll', () => this._updateStickyHeader());
|
|
276
|
+
// However resizeObserver needs an element, *not* the document.
|
|
277
|
+
const resizeTarget = scrollParent instanceof window.Document
|
|
278
|
+
? document.documentElement
|
|
279
|
+
: scrollParent;
|
|
280
|
+
new window.ResizeObserver(() => this._updateStickyHeader()).observe(resizeTarget);
|
|
281
|
+
}));
|
|
283
282
|
}
|
|
284
283
|
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Toggle visibility and update highlighter position
|
|
286
|
+
*/
|
|
287
|
+
_updateStickyHeader() {
|
|
288
|
+
if (!this.stickyHeaderEl) return;
|
|
289
|
+
|
|
290
|
+
// Show sticky header when the main 5 gauges clear the topbar.
|
|
287
291
|
const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
|
|
288
|
-
const
|
|
289
|
-
const showStickyHeader = topbarBottom >=
|
|
292
|
+
const categoriesTop = this.categoriesEl.getBoundingClientRect().top;
|
|
293
|
+
const showStickyHeader = topbarBottom >= categoriesTop;
|
|
290
294
|
|
|
291
295
|
// Highlight mini gauge when section is in view.
|
|
292
296
|
// In view = the last category that starts above the middle of the window.
|
|
293
|
-
const categoryEls = Array.from(this.
|
|
297
|
+
const categoryEls = Array.from(this._dom.rootEl.querySelectorAll('.lh-category'));
|
|
294
298
|
const categoriesAboveTheMiddle =
|
|
295
299
|
categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
|
|
296
300
|
const highlightIndex =
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
|
|
11
11
|
import jsdom from 'jsdom';
|
|
12
|
+
import {jest} from '@jest/globals';
|
|
12
13
|
|
|
13
14
|
import * as lighthouseRenderer from '../../clients/bundle.js';
|
|
14
15
|
import {LH_ROOT} from '../../../root.js';
|
|
@@ -22,16 +23,23 @@ const sampleResultsStr =
|
|
|
22
23
|
describe('lighthouseRenderer bundle', () => {
|
|
23
24
|
let document;
|
|
24
25
|
beforeAll(() => {
|
|
26
|
+
global.console.warn = jest.fn();
|
|
27
|
+
|
|
25
28
|
const {window} = new jsdom.JSDOM();
|
|
26
29
|
document = window.document;
|
|
27
30
|
|
|
28
31
|
global.window = global.self = window;
|
|
32
|
+
global.window.requestAnimationFrame = fn => fn();
|
|
29
33
|
// Stub out matchMedia for Node.
|
|
30
34
|
global.self.matchMedia = function() {
|
|
31
35
|
return {
|
|
32
36
|
addListener: function() {},
|
|
33
37
|
};
|
|
34
38
|
};
|
|
39
|
+
global.window.ResizeObserver = class ResizeObserver {
|
|
40
|
+
observe() { }
|
|
41
|
+
unobserve() { }
|
|
42
|
+
};
|
|
35
43
|
});
|
|
36
44
|
|
|
37
45
|
afterAll(() => {
|
|
@@ -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);
|
|
@@ -47,10 +50,10 @@ describe('ReportRendererAxe', () => {
|
|
|
47
50
|
});
|
|
48
51
|
|
|
49
52
|
it('renders without axe violations', async () => {
|
|
50
|
-
const container = renderer._dom.
|
|
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: {
|