db28203e64
This PR adds a basic support for WebDriver BiDi that currently includes only the ability to establish a connection and shutdown the browser. Therefore, the implementation is marked as internal and won't show up in the changelog as it's barely useful at the moment. The API classes are kept as classes instead of interfaces so that clients relying on instanceof checks still work.
298 lines
8.3 KiB
TypeScript
298 lines
8.3 KiB
TypeScript
/**
|
|
* Copyright 2020 Google Inc. All rights reserved.
|
|
*
|
|
* 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
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
import Protocol from 'devtools-protocol';
|
|
import expect from 'expect';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import rimraf from 'rimraf';
|
|
import sinon from 'sinon';
|
|
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
|
|
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
|
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
|
import {
|
|
PuppeteerLaunchOptions,
|
|
PuppeteerNode,
|
|
} from '../../lib/cjs/puppeteer/node/Puppeteer.js';
|
|
import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js';
|
|
import {TestServer} from '../../utils/testserver/lib/index.js';
|
|
import {extendExpectWithToBeGolden} from './utils.js';
|
|
import * as Mocha from 'mocha';
|
|
|
|
const setupServer = async () => {
|
|
const assetsPath = path.join(__dirname, '../assets');
|
|
const cachedPath = path.join(__dirname, '../assets', 'cached');
|
|
|
|
const server = await TestServer.create(assetsPath);
|
|
const port = server.port;
|
|
server.enableHTTPCache(cachedPath);
|
|
server.PORT = port;
|
|
server.PREFIX = `http://localhost:${port}`;
|
|
server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`;
|
|
server.EMPTY_PAGE = `http://localhost:${port}/empty.html`;
|
|
|
|
const httpsServer = await TestServer.createHTTPS(assetsPath);
|
|
const httpsPort = httpsServer.port;
|
|
httpsServer.enableHTTPCache(cachedPath);
|
|
httpsServer.PORT = httpsPort;
|
|
httpsServer.PREFIX = `https://localhost:${httpsPort}`;
|
|
httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`;
|
|
httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`;
|
|
|
|
return {server, httpsServer};
|
|
};
|
|
|
|
export const getTestState = (): PuppeteerTestState => {
|
|
return state as PuppeteerTestState;
|
|
};
|
|
|
|
const product =
|
|
process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome';
|
|
|
|
const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false;
|
|
|
|
const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
|
|
const isHeadless = headless === 'true' || headless === 'chrome';
|
|
const isFirefox = product === 'firefox';
|
|
const isChrome = product === 'chrome';
|
|
const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp';
|
|
|
|
let extraLaunchOptions = {};
|
|
try {
|
|
extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}');
|
|
} catch (error) {
|
|
if (isErrorLike(error)) {
|
|
console.warn(
|
|
`Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.`
|
|
);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const defaultBrowserOptions = Object.assign(
|
|
{
|
|
handleSIGINT: true,
|
|
executablePath: process.env['BINARY'],
|
|
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
|
|
dumpio: !!process.env['DUMPIO'],
|
|
protocol: protocol as 'cdp' | 'webDriverBiDi',
|
|
},
|
|
extraLaunchOptions
|
|
);
|
|
|
|
(async (): Promise<void> => {
|
|
if (defaultBrowserOptions.executablePath) {
|
|
console.warn(
|
|
`WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}`
|
|
);
|
|
} else {
|
|
// TODO(jackfranklin): declare updateRevision in some form for the Firefox
|
|
// launcher.
|
|
if (product === 'firefox') {
|
|
// @ts-expect-error _updateRevision is defined on the FF launcher
|
|
// but not the Chrome one. The types need tidying so that TS can infer that
|
|
// properly and not error here.
|
|
await puppeteer._launcher._updateRevision();
|
|
}
|
|
const executablePath = puppeteer.executablePath();
|
|
if (!fs.existsSync(executablePath)) {
|
|
throw new Error(
|
|
`Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests`
|
|
);
|
|
}
|
|
}
|
|
})();
|
|
|
|
declare module 'expect/build/types' {
|
|
interface Matchers<R> {
|
|
toBeGolden(x: string): R;
|
|
}
|
|
}
|
|
|
|
const setupGoldenAssertions = (): void => {
|
|
let suffix = product.toLowerCase();
|
|
if (suffix === 'chrome') {
|
|
// TODO: to avoid moving golden folders.
|
|
suffix = 'chromium';
|
|
}
|
|
const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
|
|
const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
|
|
if (fs.existsSync(OUTPUT_DIR)) {
|
|
rimraf.sync(OUTPUT_DIR);
|
|
}
|
|
extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR);
|
|
};
|
|
|
|
setupGoldenAssertions();
|
|
|
|
interface PuppeteerTestState {
|
|
browser: Browser;
|
|
context: BrowserContext;
|
|
page: Page;
|
|
puppeteer: PuppeteerNode;
|
|
defaultBrowserOptions: PuppeteerLaunchOptions;
|
|
server: TestServer;
|
|
httpsServer: TestServer;
|
|
isFirefox: boolean;
|
|
isChrome: boolean;
|
|
isHeadless: boolean;
|
|
headless: string;
|
|
puppeteerPath: string;
|
|
}
|
|
const state: Partial<PuppeteerTestState> = {};
|
|
|
|
export const itOnlyRegularInstall = (
|
|
description: string,
|
|
body: Mocha.AsyncFunc
|
|
): Mocha.Test => {
|
|
if (alternativeInstall || process.env['BINARY']) {
|
|
return it.skip(description, body);
|
|
} else {
|
|
return it(description, body);
|
|
}
|
|
};
|
|
|
|
if (
|
|
process.env['MOCHA_WORKER_ID'] === undefined ||
|
|
process.env['MOCHA_WORKER_ID'] === '0'
|
|
) {
|
|
console.log(
|
|
`Running unit tests with:
|
|
-> product: ${product}
|
|
-> binary: ${
|
|
defaultBrowserOptions.executablePath ||
|
|
path.relative(process.cwd(), puppeteer.executablePath())
|
|
}
|
|
-> mode: ${
|
|
isHeadless
|
|
? headless === 'chrome'
|
|
? '--headless=chrome'
|
|
: '--headless'
|
|
: 'headful'
|
|
}`
|
|
);
|
|
}
|
|
|
|
process.on('unhandledRejection', reason => {
|
|
throw reason;
|
|
});
|
|
|
|
export const setupTestBrowserHooks = (): void => {
|
|
before(async () => {
|
|
const browser = await puppeteer.launch(defaultBrowserOptions);
|
|
state.browser = browser;
|
|
});
|
|
|
|
after(async () => {
|
|
await state.browser?.close();
|
|
state.browser = undefined;
|
|
});
|
|
};
|
|
|
|
export const setupTestPageAndContextHooks = (): void => {
|
|
beforeEach(async () => {
|
|
state.context = await state.browser!.createIncognitoBrowserContext();
|
|
state.page = await state.context.newPage();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await state.context?.close();
|
|
state.context = undefined;
|
|
state.page = undefined;
|
|
});
|
|
};
|
|
|
|
export const mochaHooks = {
|
|
beforeAll: [
|
|
async (): Promise<void> => {
|
|
const {server, httpsServer} = await setupServer();
|
|
|
|
state.puppeteer = puppeteer;
|
|
state.defaultBrowserOptions = defaultBrowserOptions;
|
|
state.server = server;
|
|
state.httpsServer = httpsServer;
|
|
state.isFirefox = isFirefox;
|
|
state.isChrome = isChrome;
|
|
state.isHeadless = isHeadless;
|
|
state.headless = headless;
|
|
state.puppeteerPath = path.resolve(path.join(__dirname, '../..'));
|
|
},
|
|
],
|
|
|
|
beforeEach: async (): Promise<void> => {
|
|
state.server!.reset();
|
|
state.httpsServer!.reset();
|
|
},
|
|
|
|
afterAll: [
|
|
async (): Promise<void> => {
|
|
await state.server!.stop();
|
|
state.server = undefined;
|
|
await state.httpsServer!.stop();
|
|
state.httpsServer = undefined;
|
|
},
|
|
],
|
|
|
|
afterEach: (): void => {
|
|
sinon.restore();
|
|
},
|
|
};
|
|
|
|
export const expectCookieEquals = (
|
|
cookies: Protocol.Network.Cookie[],
|
|
expectedCookies: Array<Partial<Protocol.Network.Cookie>>
|
|
): void => {
|
|
const {isChrome} = getTestState();
|
|
if (!isChrome) {
|
|
// Only keep standard properties when testing on a browser other than Chrome.
|
|
expectedCookies = expectedCookies.map(cookie => {
|
|
return {
|
|
domain: cookie.domain,
|
|
expires: cookie.expires,
|
|
httpOnly: cookie.httpOnly,
|
|
name: cookie.name,
|
|
path: cookie.path,
|
|
secure: cookie.secure,
|
|
session: cookie.session,
|
|
size: cookie.size,
|
|
value: cookie.value,
|
|
};
|
|
});
|
|
}
|
|
|
|
expect(cookies.length).toBe(expectedCookies.length);
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
expect(cookies[i]).toMatchObject(expectedCookies[i]!);
|
|
}
|
|
};
|
|
|
|
export const shortWaitForArrayToHaveAtLeastNElements = async (
|
|
data: unknown[],
|
|
minLength: number,
|
|
attempts = 3,
|
|
timeout = 50
|
|
): Promise<void> => {
|
|
for (let i = 0; i < attempts; i++) {
|
|
if (data.length >= minLength) {
|
|
break;
|
|
}
|
|
await new Promise(resolve => {
|
|
return setTimeout(resolve, timeout);
|
|
});
|
|
}
|
|
};
|