blob: 56a6c208a0bf88f252e6ff386be8f8ac7e58be01 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import {type ElementHandle} from 'puppeteer-core';
import {
$,
click,
clickMoreTabsButton,
getBrowserAndPages,
goToResource,
waitFor,
waitForElementWithTextContent,
waitForFunction,
} from '../../shared/helper.js';
import {getQuotaUsage, waitForQuotaUsage} from './application-helpers.js';
export async function navigateToLighthouseTab(path?: string): Promise<ElementHandle<Element>> {
let lighthouseTabButton = await $('#tab-lighthouse');
// Lighthouse tab can be hidden if the frontend is in a dockable state.
if (!lighthouseTabButton) {
await clickMoreTabsButton();
lighthouseTabButton = await waitForElementWithTextContent('Lighthouse');
}
await lighthouseTabButton.click();
await waitFor('.view-container > .lighthouse');
const {target, frontend} = getBrowserAndPages();
if (path) {
await target.bringToFront();
await goToResource(path);
await frontend.bringToFront();
}
return waitFor('.lighthouse-start-view');
}
// Instead of watching the worker or controller/panel internals, we wait for the Lighthouse renderer
// to create the new report DOM. And we pull the LHR and artifacts off the lh-root node.
export async function waitForResult() {
const {target, frontend} = await getBrowserAndPages();
// Ensure the target page is in front so the Lighthouse run can finish.
await target.bringToFront();
await waitForFunction(() => {
return frontend.evaluate(`(async () => {
const Lighthouse = await import('./panels/lighthouse/lighthouse.js');
return Lighthouse.LighthousePanel.LighthousePanel.instance().reportSelector.hasItems();
})()`);
});
// Bring the DT frontend back in front to render the Lighthouse report.
await frontend.bringToFront();
const reportEl = await waitFor('.lh-root');
const result = await reportEl.evaluate(elem => {
// @ts-expect-error we installed this obj on a DOM element
const lhr = elem._lighthouseResultForTesting;
// @ts-expect-error we installed this obj on a DOM element
const artifacts = elem._lighthouseArtifactsForTesting;
// Delete so any subsequent runs don't accidentally reuse this.
// @ts-expect-error
delete elem._lighthouseResultForTesting;
// @ts-expect-error
delete elem._lighthouseArtifactsForTesting;
return {lhr, artifacts};
});
return {...result, reportEl};
}
// Can't reference ToolbarSettingCheckbox inside e2e
type CheckboxLabel = Element&{checkboxElement: HTMLInputElement};
/**
* Set the category checkboxes
* @param selectedCategoryIds One of 'performance'|'accessibility'|'best-practices'|'seo'|'pwa'
*/
export async function selectCategories(selectedCategoryIds: string[]) {
const startViewHandle = await waitFor('.lighthouse-start-view');
const checkboxHandles = await startViewHandle.$$('[is=dt-checkbox]');
for (const checkboxHandle of checkboxHandles) {
await checkboxHandle.evaluate((dtCheckboxElem, selectedCategoryIds: string[]) => {
const elem = dtCheckboxElem as CheckboxLabel;
const categoryId = elem.getAttribute('data-lh-category') || '';
elem.checkboxElement.checked = selectedCategoryIds.includes(categoryId);
elem.checkboxElement.dispatchEvent(new Event('change')); // Need change event to update the backing setting.
}, selectedCategoryIds);
}
}
export async function selectRadioOption(value: string, optionName: string) {
const startViewHandle = await waitFor('.lighthouse-start-view');
await startViewHandle.$eval(`input[value="${value}"][name="${optionName}"]`, radioElem => {
(radioElem as HTMLInputElement).checked = true;
(radioElem as HTMLInputElement)
.dispatchEvent(new Event('change')); // Need change event to update the backing setting.
});
}
export async function selectMode(mode: 'navigation'|'timespan'|'snapshot') {
await selectRadioOption(mode, 'lighthouse.mode');
}
export async function selectDevice(device: 'mobile'|'desktop') {
await selectRadioOption(device, 'lighthouse.device-type');
}
export async function setToolbarCheckboxWithText(enabled: boolean, textContext: string) {
const toolbarHandle = await waitFor('.lighthouse-settings-pane .toolbar');
const label = await waitForElementWithTextContent(textContext, toolbarHandle);
await label.evaluate((label, enabled: boolean) => {
const rootNode = label.getRootNode() as ShadowRoot;
const checkboxId = label.getAttribute('for') as string;
const checkboxElem = rootNode.getElementById(checkboxId) as HTMLInputElement;
checkboxElem.checked = enabled;
checkboxElem.dispatchEvent(new Event('change')); // Need change event to update the backing setting.
}, enabled);
}
export async function setThrottlingMethod(throttlingMethod: 'simulate'|'devtools') {
const toolbarHandle = await waitFor('.lighthouse-settings-pane .toolbar');
await toolbarHandle.evaluate((toolbar, throttlingMethod) => {
const selectElem = toolbar.shadowRoot?.querySelector('select') as HTMLSelectElement;
const optionElem = selectElem.querySelector(`option[value="${throttlingMethod}"]`) as HTMLOptionElement;
optionElem.selected = true;
selectElem.dispatchEvent(new Event('change')); // Need change event to update the backing setting.
}, throttlingMethod);
}
export async function clickStartButton() {
await click('.lighthouse-start-view devtools-button');
}
export async function isGenerateReportButtonDisabled() {
const buttonContainer = await waitFor<HTMLElement>('.lighthouse-start-button-container');
const button = await waitFor('button', buttonContainer);
return button.evaluate(element => element.hasAttribute('disabled'));
}
export async function getHelpText() {
const helpTextHandle = await waitFor('.lighthouse-start-view .lighthouse-help-text');
return helpTextHandle.evaluate(helpTextEl => helpTextEl.textContent);
}
export async function openStorageView() {
await click('#tab-resources');
await waitFor('.storage-group-list-item');
await click('[aria-label="Storage"]');
}
export async function clearSiteData() {
await goToResource('empty.html');
await openStorageView();
await waitForFunction(async () => {
await click('#storage-view-clear-button');
return (await getQuotaUsage()) === 0;
});
}
export async function waitForStorageUsage(p: (quota: number) => boolean) {
await openStorageView();
await waitForQuotaUsage(p);
await click('#tab-lighthouse');
}
export async function waitForTimespanStarted() {
await waitForElementWithTextContent('Timespan started, interact with the page');
}
export async function endTimespan() {
const endTimespanBtn = await waitForElementWithTextContent('End timespan');
await endTimespanBtn.click();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getAuditsBreakdown(lhr: any, flakyAudits: string[] = []) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const auditResults = Object.values(lhr.audits) as any[];
const irrelevantDisplayModes = new Set(['notApplicable', 'manual']);
const applicableAudits = auditResults.filter(
audit => !irrelevantDisplayModes.has(audit.scoreDisplayMode),
);
const informativeAudits = applicableAudits.filter(
audit => audit.scoreDisplayMode === 'informative',
);
const erroredAudits = applicableAudits.filter(
audit => audit.score === null && audit && !informativeAudits.includes(audit),
);
// 0.5 is the minimum score before we consider an audit "failed"
// https://github.com/GoogleChrome/lighthouse/blob/d956ec929d2b67028279f5e40d7e9a515a0b7404/report/renderer/util.js#L27
const failedAudits = applicableAudits.filter(
audit => audit.score !== null && audit.score < 0.5 && !flakyAudits.includes(audit.id),
);
return {auditResults, erroredAudits, failedAudits};
}
export async function getTargetViewport() {
const {target} = await getBrowserAndPages();
return target.evaluate(() => ({
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
outerWidth: window.outerWidth,
outerHeight: window.outerHeight,
devicePixelRatio: window.devicePixelRatio,
}));
}
export async function getServiceWorkerCount() {
const {target} = await getBrowserAndPages();
return target.evaluate(async () => {
return (await navigator.serviceWorker.getRegistrations()).length;
});
}
export async function registerServiceWorker() {
const {target} = getBrowserAndPages();
await target.evaluate(async () => {
// @ts-expect-error Custom function added to global scope.
await window.registerServiceWorker();
});
assert.strictEqual(await getServiceWorkerCount(), 1);
}
export async function interceptNextFileSave(): Promise<() => Promise<string>> {
const {frontend} = await getBrowserAndPages();
await frontend.evaluate(() => {
// @ts-expect-error
const original = InspectorFrontendHost.save;
const nextFilePromise = new Promise(resolve => {
// @ts-expect-error
InspectorFrontendHost.save = (_, content) => {
resolve(content);
};
});
nextFilePromise.finally(() => {
// @ts-expect-error
InspectorFrontendHost.save = original;
});
// @ts-expect-error
window.__nextFile = nextFilePromise;
});
// @ts-expect-error
return () => frontend.evaluate(() => window.__nextFile);
}
export async function renderHtmlInIframe(html: string) {
const {target} = getBrowserAndPages();
return (await target.evaluateHandle(async html => {
const iframe = document.createElement('iframe');
iframe.srcdoc = html;
document.documentElement.append(iframe);
await new Promise(resolve => iframe.addEventListener('load', resolve));
return iframe.contentDocument;
}, html)).asElement() as ElementHandle<Document>;
}