chore: move dev server to config-based framework extensibility (#30234)

This commit is contained in:
Pavel Feldman 2024-04-05 08:39:51 -07:00 committed by GitHub
parent e18a358bc6
commit 5043bd55dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 104 additions and 50 deletions

3
package-lock.json generated
View file

@ -8220,9 +8220,6 @@
"playwright-core": "1.44.0-next",
"vite": "^5.0.13"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}

View file

@ -1,19 +0,0 @@
#!/usr/bin/env node
/**
* 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.
*/
const { program } = require('./lib/program');
program.parse(process.argv);

View file

@ -16,7 +16,7 @@
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('playwright/test');
const { fixtures } = require('./lib/mount');
const { clearCacheCommand, findRelatedTestFilesCommand } = require('./lib/cliOverrides');
const { clearCacheCommand, runDevServerCommand, findRelatedTestFilesCommand } = require('./lib/cliOverrides');
const { createPlugin } = require('./lib/vitePlugin');
const defineConfig = (...configs) => {
@ -31,6 +31,7 @@ const defineConfig = (...configs) => {
],
cli: {
'clear-cache': clearCacheCommand,
'dev-server': runDevServerCommand,
'find-related-test-files': findRelatedTestFilesCommand,
},
}

View file

@ -29,8 +29,5 @@
"playwright-core": "1.44.0-next",
"vite": "^5.0.13",
"playwright": "1.44.0-next"
},
"bin": {
"playwright": "cli.js"
}
}

View file

@ -20,6 +20,8 @@ import { affectedTestFiles, cacheDir } from 'playwright/lib/transform/compilatio
import { buildBundle } from './vitePlugin';
import { resolveDirs } from './viteUtils';
import type { FullConfig, Suite } from 'playwright/types/testReporter';
import { runDevServer } from './devServer';
import type { FullConfigInternal } from 'playwright/lib/common/config';
export async function clearCacheCommand(config: FullConfig, configDir: string) {
const dirs = await resolveDirs(configDir, config);
@ -32,3 +34,7 @@ export async function findRelatedTestFilesCommand(files: string[], config: Full
await buildBundle(config, configDir, suite);
return { testFiles: affectedTestFiles(files) };
}
export async function runDevServerCommand(config: FullConfigInternal) {
return await runDevServer(config);
}

View file

@ -17,18 +17,14 @@
import fs from 'fs';
import path from 'path';
import { Watcher } from 'playwright/lib/fsWatcher';
import { loadConfigFromFileRestartIfNeeded } from 'playwright/lib/common/configLoader';
import { Runner } from 'playwright/lib/runner/runner';
import type { PluginContext } from 'rollup';
import { source as injectedSource } from './generated/indexSource';
import { createConfig, populateComponentsFromTests, resolveDirs, transformIndexFile, frameworkConfig } from './viteUtils';
import type { ComponentRegistry } from './viteUtils';
import type { FullConfigInternal } from 'playwright/lib/common/config';
export async function runDevServer(configFile: string) {
const config = await loadConfigFromFileRestartIfNeeded(configFile);
if (!config)
return;
export async function runDevServer(config: FullConfigInternal): Promise<() => Promise<void>> {
const { registerSourceFile, frameworkPluginFactory } = frameworkConfig(config.config);
const runner = new Runner(config);
await runner.loadAllTests();
@ -39,7 +35,7 @@ export async function runDevServer(configFile: string) {
if (!dirs) {
// eslint-disable-next-line no-console
console.log(`Template file playwright/index.html is missing.`);
return;
return async () => {};
}
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
const viteConfig = await createConfig(dirs, config.config, frameworkPluginFactory, false);
@ -56,7 +52,7 @@ export async function runDevServer(configFile: string) {
await devServer.listen();
const protocol = viteConfig.server.https ? 'https:' : 'http:';
// eslint-disable-next-line no-console
console.log(`Test Server listening on ${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`);
console.log(`Dev Server listening on ${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`);
const projectDirs = new Set<string>();
const projectOutputs = new Set<string>();
@ -85,4 +81,5 @@ export async function runDevServer(configFile: string) {
devServer.moduleGraph.onFileChange(rootModule.file!);
});
globalWatcher.update([...projectDirs], [...projectOutputs], false);
return () => Promise.all([devServer.close(), globalWatcher.close()]).then(() => {});
}

View file

@ -14,19 +14,4 @@
* limitations under the License.
*/
import type { Command } from 'playwright-core/lib/utilsBundle';
import { program } from 'playwright/lib/program';
import { runDevServer } from './devServer';
export { program } from 'playwright/lib/program';
function addDevServerCommand(program: Command) {
const command = program.command('dev-server', { hidden: true });
command.description('start dev server');
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
command.action(options => {
runDevServer(options.config);
});
}
addDevServerCommand(program);

View file

@ -96,7 +96,7 @@ export async function buildBundle(config: FullConfig, configDir: string, suite:
const url = new URL(`${protocol}//${endpoint.host}:${endpoint.port}`);
if (await isURLAvailable(url, true)) {
// eslint-disable-next-line no-console
console.log(`Test Server is already running at ${url.toString()}, using it.\n`);
console.log(`Dev Server is already running at ${url.toString()}, using it.\n`);
process.env.PLAYWRIGHT_TEST_BASE_URL = url.toString();
return null;
}

View file

@ -63,6 +63,10 @@ export class Watcher {
});
}
async close() {
await this._fsWatcher?.close();
}
private _reportEventsIfAny() {
if (this._collector.length)
this._onChange(this._collector.slice());

View file

@ -19,6 +19,8 @@ import type { Metadata } from '../../types/test';
import type * as reporterTypes from '../../types/testReporter';
import type { ReporterV2 } from '../reporters/reporterV2';
// -- Reuse boundary -- Everything below this line is reused in the vscode extension.
export type StringIntern = (s: string) => string;
export type JsonLocation = reporterTypes.Location;
export type JsonError = string;

View file

@ -17,6 +17,8 @@
import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface';
import * as events from './events';
// -- Reuse boundary -- Everything below this line is reused in the vscode extension.
export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
readonly onClose: events.Event<void>;
readonly onReport: events.Event<any>;
@ -155,6 +157,14 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
return await this._sendMessage('runGlobalTeardown', params);
}
async startDevServer(params: Parameters<TestServerInterface['startDevServer']>[0]): ReturnType<TestServerInterface['startDevServer']> {
return await this._sendMessage('startDevServer', params);
}
async stopDevServer(params: Parameters<TestServerInterface['stopDevServer']>[0]): ReturnType<TestServerInterface['stopDevServer']> {
return await this._sendMessage('stopDevServer', params);
}
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
return await this._sendMessage('listFiles', params);
}

View file

@ -18,6 +18,8 @@ import type * as reporterTypes from '../../types/testReporter';
import type { Event } from './events';
import type { JsonEvent } from './teleReceiver';
// -- Reuse boundary -- Everything below this line is reused in the vscode extension.
export type ReportEntry = JsonEvent;
export interface TestServerInterface {
@ -52,6 +54,16 @@ export interface TestServerInterface {
status: reporterTypes.FullResult['status']
}>;
startDevServer(params: {}): Promise<{
report: ReportEntry[];
status: reporterTypes.FullResult['status']
}>;
stopDevServer(params: {}): Promise<{
report: ReportEntry[];
status: reporterTypes.FullResult['status']
}>;
listFiles(params: {
projects?: string[];
}): Promise<{

View file

@ -17,6 +17,8 @@
export type TestItemStatus = 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped';
import type * as reporterTypes from '../../types/testReporter';
// -- Reuse boundary -- Everything below this line is reused in the vscode extension.
export type TreeItemBase = {
kind: 'root' | 'group' | 'case' | 'test',
id: string;

View file

@ -105,6 +105,25 @@ function addFindRelatedTestFilesCommand(program: Command) {
});
}
function addDevServerCommand(program: Command) {
const command = program.command('dev-server', { hidden: true });
command.description('start dev server');
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
command.action(async options => {
const configInternal = await loadConfigFromFileRestartIfNeeded(options.config);
if (!configInternal)
return;
const { config } = configInternal;
const implementation = (config as any)['@playwright/test']?.['cli']?.['dev-server'];
if (implementation) {
await implementation(configInternal);
} else {
console.log(`DevServer is not available in the package you are using. Did you mean to use component testing?`);
gracefullyProcessExitDoNotHang(1);
}
});
}
function addTestServerCommand(program: Command) {
const command = program.command('test-server', { hidden: true });
command.description('start test server');
@ -362,4 +381,5 @@ addListFilesCommand(program);
addMergeReportsCommand(program);
addClearCacheCommand(program);
addFindRelatedTestFilesCommand(program);
addDevServerCommand(program);
addTestServerCommand(program);

View file

@ -21,6 +21,8 @@ import type * as teleReceiver from '../isomorphic/teleReceiver';
import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
import type { ReporterV2 } from './reporterV2';
// -- Reuse boundary -- Everything below this line is reused in the vscode extension.
export type TeleReporterEmitterOptions = {
omitOutput?: boolean;
omitBuffers?: boolean;

View file

@ -74,6 +74,7 @@ class TestServerDispatcher implements TestServerInterface {
private _serializer = require.resolve('./uiModeReporter');
private _watchTestDirs = false;
private _closeOnDisconnect = false;
private _devServerHandle: (() => Promise<void>) | undefined;
constructor(configFile: string | undefined) {
this._configFile = configFile;
@ -172,6 +173,43 @@ class TestServerDispatcher implements TestServerInterface {
return { status, report: globalSetup?.report || [] };
}
async startDevServer(params: Parameters<TestServerInterface['startDevServer']>[0]): ReturnType<TestServerInterface['startDevServer']> {
if (this._devServerHandle)
return { status: 'failed', report: [] };
const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile);
if (!config) {
reporter.onError(error!);
return { status: 'failed', report };
}
const devServerCommand = (config.config as any)['@playwright/test']?.['cli']?.['dev-server'];
if (!devServerCommand) {
reporter.onError({ message: 'No dev-server command found in the configuration' });
return { status: 'failed', report };
}
try {
this._devServerHandle = await devServerCommand(config);
return { status: 'passed', report };
} catch (e) {
reporter.onError(serializeError(e));
return { status: 'failed', report };
}
}
async stopDevServer(params: Parameters<TestServerInterface['stopDevServer']>[0]): ReturnType<TestServerInterface['stopDevServer']> {
if (!this._devServerHandle)
return { status: 'failed', report: [] };
try {
await this._devServerHandle();
this._devServerHandle = undefined;
return { status: 'passed', report: [] };
} catch (e) {
const { reporter, report } = await this._collectingReporter();
reporter.onError(serializeError(e));
return { status: 'failed', report };
}
}
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile);