blob: c33028cc66754cb25e0175d61e86d2fa6d9339d7 [file] [log] [blame]
/**
* @license
* Copyright 2021 The Go 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 { Browser, Page, ScreenshotOptions, WaitForOptions } from 'puppeteer';
/**
* global declares global variables available in e2e test files.
*/
declare global {
/**
* A controller for the instance of Chrome used in for the tests.
*/
const browser: Browser;
/**
* pageErrors is a record of uncaught exceptions that have occured in a test suite.
*/
const pageErrors: Error[];
}
const {
GO_DISCOVERY_E2E_AUTHORIZATION = null,
GO_DISCOVERY_E2E_BASE_URL = 'http://host.docker.internal:8080',
GO_DISCOVERY_E2E_QUOTA_BYPASS = null,
} = process.env;
/**
* blockedOrigins is used to block requests to badge URLs typically
* found in project READMEs. When code coverage updates or builds fail
* these badges can causes the e2e tests to fail.
*/
const blockedOrigins = ['https://codecov.io', 'https://travis-ci.com'];
/**
* newPage opens a new chrome tab, sets up a request intercept
* to provide an authorization header for relevant requests, and
* prefixes page.goto urls with the base URL for the test current
* environment.
* @returns a new page context
*/
export async function newPage(): Promise<Page> {
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', r => {
if (blockedOrigins.some(o => r.url().startsWith(o))) {
r.abort();
return;
}
const url = new URL(r.url());
let headers = r.headers();
if (GO_DISCOVERY_E2E_AUTHORIZATION && url.origin === GO_DISCOVERY_E2E_BASE_URL) {
headers = {
...r.headers(),
Authorization: `Bearer ${GO_DISCOVERY_E2E_AUTHORIZATION}`,
'X-Go-Discovery-Auth-Bypass-Quota': GO_DISCOVERY_E2E_QUOTA_BYPASS,
};
}
r.continue({ headers });
});
page.on('pageerror', err => {
this.global.pageErrors.push(err);
});
const go = page.goto;
page.goto = (path: string, opts?: WaitForOptions) =>
go.call(page, GO_DISCOVERY_E2E_BASE_URL + path, { waitUntil: 'networkidle0', ...opts });
// Setting captureBeyondViewport false to avoid a chromium bug
// where the page size becomes unstable during screenshots:
// https://github.com/puppeteer/puppeteer/issues/7043.
const screenshot = page.screenshot;
page.screenshot = (options: ScreenshotOptions) =>
screenshot.call(page, { captureBeyondViewport: false, ...options });
return page;
}
/**
* select will create a data-test-id attribute selector for a given test id.
* @param testId the test id of the element to select.
* @param rest a place to add combinators and additional selectors.
* @returns an attribute selector.
*/
export function select(testId: string, rest = ''): string {
return `[data-test-id="${testId}"] ${rest}`;
}
/**
* prepare disables page transitions and animations.
* @param page The page to prepare
*/
export async function prepare(page: Page): Promise<void> {
await Promise.all([
page.addStyleTag({
content: `
*,
*::after,
*::before {
transition-delay: 0s !important;
transition-duration: 0s !important;
animation-delay: -0.0001s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
caret-color: transparent;
}`,
}),
]);
}
/**
* $eval wraps page.$eval to check if an element exists before
* attempting to run the callback.
* @param page the current page
* @param selector a css selector
* @param cb an operation to perform on the selected element
*/
export async function $eval(
page: Page,
selector: string,
cb?: (el: Element) => unknown
): Promise<void> {
if (await page.$(selector)) {
await page.$eval(selector, cb);
}
}
/**
* $$eval wraps page.$$eval to check if an element exists before
* attempting to run the callback.
* @param page the current page
* @param selector a css selector
* @param cb an operation to perform on an array of the selected elements
*/
export async function $$eval(
page: Page,
selector: string,
cb?: (els: Element[]) => unknown
): Promise<void> {
if (await page.$(selector)) {
await page.$$eval(selector, cb);
}
}
/**
* TestOptions are options for basic page tests.
*/
export interface TestOptions {
/**
* path is the pathname of the page to visit.
*/
path: string;
/**
* mobile will set the page to a mobile viewport size.
*/
mobile?: boolean;
/**
* prepare will prepare the page for screenshot.
*/
prepare?: typeof prepare;
}
/**
* a11ySnapshotTest asserts that the a11y tree matches the snapshot.
* @param page a page object.
* @param opts test options.
*/
export async function a11ySnapshotTest(
page: Page,
opts: TestOptions = { path: '' }
): Promise<void> {
if (opts.mobile) {
await page.setViewport({ width: 411, height: 731 });
} else {
await page.setViewport({ width: 1600, height: 900 });
}
await page.goto(opts.path);
await (opts.prepare ?? prepare)(page);
const a11yTree = await page.accessibility.snapshot();
expect(a11yTree).toMatchSnapshot();
await page.close();
}
/**
* fullScreenshotTest asserts that the full page screenshot matches the
* image snapshot.
* @param path a page object.
* @param opts test options.
*/
export async function fullScreenshotTest(
page: Page,
opts: TestOptions = { path: '' }
): Promise<void> {
if (opts.mobile) {
await page.setViewport({ width: 411, height: 731 });
} else {
await page.setViewport({ width: 1600, height: 900 });
}
await page.goto(opts.path);
await (opts.prepare ?? prepare)(page);
const image = await page.screenshot({ fullPage: true });
expect(image).toMatchImageSnapshot();
await page.close();
}