chore: add test server stub (#29568)

This commit is contained in:
Pavel Feldman 2024-02-20 09:56:33 -08:00 committed by GitHub
parent 84fefdaac6
commit d573c515a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 170 additions and 19 deletions

View File

@ -40,6 +40,7 @@ export * from './time';
export * from './timeoutRunner';
export * from './traceUtils';
export * from './userAgent';
export * from './wsServer';
export * from './zipFile';
export * from './zones';
export * from './isomorphic/locatorGenerators';

View File

@ -33,12 +33,12 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { context: BrowserContext | undefined, hash: string } };
type BaseTestFixtures = {
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>,
_contextReuseMode: ContextReuseMode
_optionContextReuseMode: ContextReuseMode
};
export const fixtures: Fixtures<TestFixtures, WorkerFixtures, BaseTestFixtures> = {
_contextReuseMode: 'when-possible',
_optionContextReuseMode: 'when-possible',
serviceWorkers: 'block',

View File

@ -25,6 +25,7 @@
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
"./lib/runner/runner": "./lib/runner/runner.js",
"./lib/runner/testServer": "./lib/runner/testServer.js",
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
"./lib/transform/transform": "./lib/transform/transform.js",
"./lib/internalsForTest": "./lib/internalsForTest.js",

View File

@ -40,6 +40,7 @@ export class FullConfigInternal {
readonly configDir: string;
readonly configCLIOverrides: ConfigCLIOverrides;
readonly ignoreSnapshots: boolean;
readonly preserveOutputDir: boolean;
readonly webServers: Exclude<FullConfig['webServer'], null>[];
readonly plugins: TestRunnerPluginRegistration[];
readonly projects: FullProjectInternal[] = [];
@ -68,6 +69,7 @@ export class FullConfigInternal {
this.configDir = configDir;
this.configCLIOverrides = configCLIOverrides;
this.globalOutputDir = takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, config.outputDir), throwawayArtifactsPath, path.resolve(process.cwd()));
this.preserveOutputDir = configCLIOverrides.preserveOutputDir || false;
this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots, false);
const privateConfiguration = (config as any)['@playwright/test'];
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));

View File

@ -25,6 +25,7 @@ export type ConfigCLIOverrides = {
globalTimeout?: number;
maxFailures?: number;
outputDir?: string;
preserveOutputDir?: boolean;
quiet?: boolean;
repeatEach?: number;
retries?: number;

View File

@ -44,14 +44,16 @@ if ((process as any)['__pw_initiator__']) {
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions: BrowserContextOptions,
_contextReuseMode: ContextReuseMode,
_reuseContext: boolean,
_setupContextOptions: void;
_setupArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
};
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
_browserOptions: LaunchOptions;
_optionContextReuseMode: ContextReuseMode,
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
_reuseContext: boolean,
};
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
@ -63,8 +65,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
launchOptions: [{}, { scope: 'worker', option: true }],
connectOptions: [async ({}, use) => {
await use(connectOptionsFromEnv());
connectOptions: [async ({ _optionConnectOptions }, use) => {
await use(connectOptionsFromEnv() || _optionConnectOptions);
}, { scope: 'worker', option: true }],
screenshot: ['off', { scope: 'worker', option: true }],
video: ['off', { scope: 'worker', option: true }],
@ -88,7 +90,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
(browserType as any)._defaultLaunchOptions = undefined;
}, { scope: 'worker', auto: true }],
browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use, testInfo) => {
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
@ -97,7 +99,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
...connectOptions,
exposeNetwork: connectOptions.exposeNetwork ?? (connectOptions as any)._exposeNetwork,
headers: {
...(process.env.PW_TEST_REUSE_CONTEXT ? { 'x-playwright-reuse-context': '1' } : {}),
...(_reuseContext ? { 'x-playwright-reuse-context': '1' } : {}),
// HTTP headers are ASCII only (not UTF-8).
'x-playwright-launch-options': jsonStringifyForceASCII(_browserOptions),
...connectOptions.headers,
@ -348,12 +350,16 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
}, { scope: 'test', _title: 'context' } as any],
_contextReuseMode: process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'),
_optionContextReuseMode: ['none', { scope: 'worker' }],
_optionConnectOptions: [undefined, { scope: 'worker' }],
_reuseContext: [async ({ video, _contextReuseMode }, use, testInfo) => {
const reuse = _contextReuseMode === 'force' || (_contextReuseMode === 'when-possible' && !shouldCaptureVideo(normalizeVideoMode(video), testInfo));
_reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
let mode = _optionContextReuseMode;
if (process.env.PW_TEST_REUSE_CONTEXT)
mode = process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none');
const reuse = mode === 'force' || (mode === 'when-possible' && normalizeVideoMode(video) === 'off');
await use(reuse);
}, { scope: 'test', _title: 'context' } as any],
}, { scope: 'worker', _title: 'context' } as any],
context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => {
attachConnectedHeaderIfNeeded(testInfo, browser);

View File

@ -35,6 +35,7 @@ export { program } from 'playwright-core/lib/cli/program';
import type { ReporterDescription } from '../types/test';
import { prepareErrorStack } from './reporters/base';
import { affectedTestFiles, cacheDir } from './transform/compilationCache';
import { runTestServer } from './runner/testServer';
function addTestCommand(program: Command) {
const command = program.command('test [test-filter...]');
@ -115,6 +116,15 @@ function addFindRelatedTestFilesCommand(program: Command) {
});
}
function addTestServerCommand(program: Command) {
const command = program.command('test-server', { hidden: true });
command.description('start test server');
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
command.action(options => {
void runTestServer(options.config);
});
}
function addShowReportCommand(program: Command) {
const command = program.command('show-report [report]');
command.description('show HTML report');
@ -339,3 +349,4 @@ addListFilesCommand(program);
addMergeReportsCommand(program);
addClearCacheCommand(program);
addFindRelatedTestFilesCommand(program);
addTestServerCommand(program);

View File

@ -88,7 +88,8 @@ function addGlobalSetupTasks(taskRunner: TaskRunner<TestRun>, config: FullConfig
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
if (config.config.globalSetup || config.config.globalTeardown)
taskRunner.addTask('global setup', createGlobalSetupTask());
taskRunner.addTask('clear output', createRemoveOutputDirsTask());
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
taskRunner.addTask('clear output', createRemoveOutputDirsTask());
}
function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) {
@ -165,8 +166,6 @@ function createGlobalSetupTask(): Task<TestRun> {
function createRemoveOutputDirsTask(): Task<TestRun> {
return {
setup: async ({ config }) => {
if (process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
return;
const outputDirs = new Set<string>();
const projects = filterProjects(config.projects, config.cliProjectFilter);
projects.forEach(p => outputDirs.add(p.project.outputDir));

View File

@ -0,0 +1,125 @@
/**
* 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 type http from 'http';
import { ManualPromise, createGuid } from 'playwright-core/lib/utils';
import { WSServer } from 'playwright-core/lib/utils';
import type { WebSocket } from 'playwright-core/lib/utilsBundle';
import type { FullResult } from 'playwright/types/testReporter';
import type { FullConfigInternal } from '../common/config';
import { loadConfigFromFile } from '../common/configLoader';
import { InternalReporter } from '../reporters/internalReporter';
import { Multiplexer } from '../reporters/multiplexer';
import { createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch } from './tasks';
type PlaywrightTestOptions = {
headed?: boolean,
oneWorker?: boolean,
trace?: 'on' | 'off',
projects?: string[];
grep?: string;
reuseContext?: boolean,
connectWsEndpoint?: string;
};
export async function runTestServer(configFile: string) {
process.env.PW_TEST_HTML_REPORT_OPEN = 'never';
process.env.FORCE_COLOR = '1';
const config = await loadConfigFromFile(configFile);
if (!config)
return;
const dispatcher = new Dispatcher(config);
const wss = new WSServer({
onConnection(request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) {
ws.on('message', async message => {
const { id, method, params } = JSON.parse(message.toString());
const result = await (dispatcher as any)[method](params);
ws.send(JSON.stringify({ id, result }));
});
return {
async close() {}
};
},
});
const url = await wss.listen(0, 'localhost', '/' + createGuid());
// eslint-disable-next-line no-console
console.log(`Listening on ${url}`);
}
class Dispatcher {
private _config: FullConfigInternal;
constructor(config: FullConfigInternal) {
this._config = config;
}
async test(params: { mode: 'list' | 'run', locations: string[], options: PlaywrightTestOptions, reporter: string, env: NodeJS.ProcessEnv }) {
for (const name in params.env)
process.env[name] = params.env[name];
if (params.mode === 'list')
await listTests(this._config, params.reporter, params.locations);
if (params.mode === 'run')
await runTests(this._config, params.reporter, params.locations, params.options);
}
}
async function listTests(config: FullConfigInternal, reporterPath: string, locations: string[] | undefined) {
config.cliArgs = [...(locations || []), '--reporter=null'];
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'list', [[reporterPath]])));
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config);
const taskStatus = await taskRunner.run(testRun, 0);
let status: FullResult['status'] = testRun.failureTracker.result();
if (status === 'passed' && taskStatus !== 'passed')
status = taskStatus;
const modifiedResult = await reporter.onEnd({ status });
if (modifiedResult && modifiedResult.status)
status = modifiedResult.status;
await reporter.onExit();
}
async function runTests(config: FullConfigInternal, reporterPath: string, locations: string[] | undefined, options: PlaywrightTestOptions) {
config.cliArgs = locations || [];
config.cliGrep = options.grep;
config.cliProjectFilter = options.projects;
config.configCLIOverrides.reporter = [[reporterPath]];
config.configCLIOverrides.repeatEach = 1;
config.configCLIOverrides.retries = 0;
config.configCLIOverrides.workers = options.oneWorker ? 1 : undefined;
config.configCLIOverrides.preserveOutputDir = true;
config.configCLIOverrides.use = {
trace: options.trace,
headless: options.headed ? false : undefined,
_optionContextReuseMode: options.reuseContext ? 'when-possible' : undefined,
_optionConnectOptions: options.connectWsEndpoint ? { wsEndpoint: options.connectWsEndpoint } : undefined,
};
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, 'run')));
const taskRunner = createTaskRunnerForWatch(config, reporter);
const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config);
const stop = new ManualPromise();
const status = await taskRunner.run(testRun, 0, stop);
await reporter.onEnd({ status });
await reporter.onExit();
}

View File

@ -417,15 +417,20 @@ async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: nu
config.config.workers = 1;
showBrowserServer = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: 1 });
const wsEndpoint = await showBrowserServer.listen();
process.env.PW_TEST_REUSE_CONTEXT = '1';
process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint;
config.configCLIOverrides.use = {
...config.configCLIOverrides.use,
_optionContextReuseMode: 'when-possible',
_optionConnectOptions: { wsEndpoint },
};
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`);
} else {
config.config.workers = originalWorkers;
if (config.configCLIOverrides.use) {
delete config.configCLIOverrides.use._optionContextReuseMode;
delete config.configCLIOverrides.use._optionConnectOptions;
}
await showBrowserServer?.close();
showBrowserServer = undefined;
delete process.env.PW_TEST_REUSE_CONTEXT;
delete process.env.PW_TEST_CONNECT_WS_ENDPOINT;
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('off')}\n`);
}
}