/** * Copyright (c) Microsoft Corporation. * * 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 { Fixtures, _baseTest } from '@playwright/test'; import * as path from 'path'; import * as fs from 'fs'; import { installCoverageHooks } from './coverage'; import * as childProcess from 'child_process'; import { start } from 'playwright-core/lib/outofprocess'; import { PlaywrightClient } from 'playwright-core/lib/remote/playwrightClient'; import type { LaunchOptions } from 'playwright-core'; import { commonFixtures, CommonFixtures, serverFixtures, ServerFixtures, ServerOptions } from './commonFixtures'; export type BrowserName = 'chromium' | 'firefox' | 'webkit'; type Mode = 'default' | 'driver' | 'service'; type BaseOptions = { mode: Mode; browserName: BrowserName; channel: LaunchOptions['channel']; video: boolean | undefined; trace: boolean | undefined; headless: boolean | undefined; }; type BaseFixtures = { platform: 'win32' | 'darwin' | 'linux'; playwright: typeof import('playwright-core'); toImpl: (rpcObject: any) => any; isWindows: boolean; isMac: boolean; isLinux: boolean; }; class DriverMode { private _playwrightObject: any; async setup(workerIndex: number) { this._playwrightObject = await start(); return this._playwrightObject; } async teardown() { await this._playwrightObject.stop(); } } class ServiceMode { private _client: import('playwright-core/src/remote/playwrightClient').PlaywrightClient; private _serviceProcess: childProcess.ChildProcess; async setup(workerIndex: number) { const port = 10507 + workerIndex; this._serviceProcess = childProcess.fork(path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'), ['run-server', String(port)], { stdio: 'pipe' }); this._serviceProcess.stderr.pipe(process.stderr); await new Promise(f => { this._serviceProcess.stdout.on('data', data => { if (data.toString().includes('Listening on')) f(); }); }); this._serviceProcess.on('exit', this._onExit); this._client = await PlaywrightClient.connect({ wsEndpoint: `ws://localhost:${port}/ws` }); return this._client.playwright(); } async teardown() { await this._client.close(); this._serviceProcess.removeListener('exit', this._onExit); const processExited = new Promise(f => this._serviceProcess.on('exit', f)); this._serviceProcess.kill(); await processExited; } private _onExit(exitCode: number, signal: string) { throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`); } } class DefaultMode { async setup(workerIndex: number) { return require('playwright-core'); } async teardown() { } } const baseFixtures: Fixtures<{}, BaseOptions & BaseFixtures> = { mode: [ 'default', { scope: 'worker' } ], browserName: [ 'chromium' , { scope: 'worker' } ], channel: [ undefined, { scope: 'worker' } ], video: [ undefined, { scope: 'worker' } ], trace: [ undefined, { scope: 'worker' } ], headless: [ undefined, { scope: 'worker' } ], platform: [ process.platform as 'win32' | 'darwin' | 'linux', { scope: 'worker' } ], playwright: [ async ({ mode }, run, workerInfo) => { const modeImpl = { default: new DefaultMode(), service: new ServiceMode(), driver: new DriverMode(), }[mode]; require('playwright-core/lib/utils/utils').setUnderTest(); const playwright = await modeImpl.setup(workerInfo.workerIndex); await run(playwright); await modeImpl.teardown(); }, { scope: 'worker' } ], toImpl: [ async ({ playwright }, run) => run((playwright as any)._toImpl), { scope: 'worker' } ], isWindows: [ process.platform === 'win32', { scope: 'worker' } ], isMac: [ process.platform === 'darwin', { scope: 'worker' } ], isLinux: [ process.platform === 'linux', { scope: 'worker' } ], }; type CoverageOptions = { coverageName?: string; }; const coverageFixtures: Fixtures<{}, CoverageOptions & { __collectCoverage: void }> = { coverageName: [ undefined, { scope: 'worker' } ], __collectCoverage: [ async ({ coverageName }, run, workerInfo) => { if (!coverageName) { await run(); return; } const { coverage, uninstall } = installCoverageHooks(coverageName); await run(); uninstall(); const coveragePath = path.join(__dirname, '..', 'coverage-report', workerInfo.workerIndex + '.json'); const coverageJSON = Array.from(coverage.keys()).filter(key => coverage.get(key)); await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true }); await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8'); }, { scope: 'worker', auto: true } ], }; export type CommonOptions = BaseOptions & ServerOptions & CoverageOptions; export type CommonWorkerFixtures = CommonOptions & BaseFixtures; export const baseTest = _baseTest.extend(commonFixtures).extend<{}, CoverageOptions>(coverageFixtures).extend(serverFixtures as any).extend<{}, BaseOptions & BaseFixtures>(baseFixtures);