mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 11:46:42 +03:00
chore: prepare to reuse test server from ui mode (2) (#29966)
This commit is contained in:
parent
6faadf5160
commit
ef4438ee99
@ -14,12 +14,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TestError } from '../../types/testReporter';
|
import type * as reporterTypes from '../../types/testReporter';
|
||||||
|
|
||||||
export interface TestServerInterface {
|
export interface TestServerInterface {
|
||||||
listFiles(params: {
|
ping(): Promise<void>;
|
||||||
configFile: string;
|
|
||||||
}): Promise<{
|
watch(params: {
|
||||||
|
fileNames: string[];
|
||||||
|
}): Promise<void>;
|
||||||
|
|
||||||
|
open(params: { location: reporterTypes.Location }): Promise<void>;
|
||||||
|
|
||||||
|
resizeTerminal(params: { cols: number, rows: number }): Promise<void>;
|
||||||
|
|
||||||
|
checkBrowsers(): Promise<{ hasBrowsers: boolean }>;
|
||||||
|
|
||||||
|
installBrowsers(): Promise<void>;
|
||||||
|
|
||||||
|
listFiles(): Promise<{
|
||||||
projects: {
|
projects: {
|
||||||
name: string;
|
name: string;
|
||||||
testDir: string;
|
testDir: string;
|
||||||
@ -27,41 +39,40 @@ export interface TestServerInterface {
|
|||||||
files: string[];
|
files: string[];
|
||||||
}[];
|
}[];
|
||||||
cliEntryPoint?: string;
|
cliEntryPoint?: string;
|
||||||
error?: TestError;
|
error?: reporterTypes.TestError;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
listTests(params: {
|
listTests(params: {
|
||||||
configFile: string;
|
reporter?: string;
|
||||||
locations: string[];
|
fileNames?: string[];
|
||||||
reporter: string;
|
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
test(params: {
|
runTests(params: {
|
||||||
configFile: string;
|
reporter?: string;
|
||||||
locations: string[];
|
locations?: string[];
|
||||||
reporter: string;
|
grep?: string;
|
||||||
|
testIds?: string[];
|
||||||
headed?: boolean;
|
headed?: boolean;
|
||||||
oneWorker?: boolean;
|
oneWorker?: boolean;
|
||||||
trace?: 'on' | 'off';
|
trace?: 'on' | 'off';
|
||||||
projects?: string[];
|
projects?: string[];
|
||||||
grep?: string;
|
|
||||||
reuseContext?: boolean;
|
reuseContext?: boolean;
|
||||||
connectWsEndpoint?: string;
|
connectWsEndpoint?: string;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
findRelatedTestFiles(params: {
|
findRelatedTestFiles(params: {
|
||||||
configFile: string;
|
|
||||||
files: string[];
|
files: string[];
|
||||||
}): Promise<{ testFiles: string[]; errors?: TestError[]; }>;
|
}): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>;
|
||||||
|
|
||||||
stop(params: {
|
stop(): Promise<void>;
|
||||||
configFile: string;
|
|
||||||
}): Promise<void>;
|
|
||||||
|
|
||||||
closeGracefully(): Promise<void>;
|
closeGracefully(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestServerEvents {
|
export interface TestServerEvents {
|
||||||
on(event: 'report', listener: (params: any) => void): void;
|
on(event: 'listReport', listener: (params: any) => void): void;
|
||||||
|
on(event: 'testReport', listener: (params: any) => void): void;
|
||||||
on(event: 'stdio', listener: (params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }) => void): void;
|
on(event: 'stdio', listener: (params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }) => void): void;
|
||||||
|
on(event: 'listChanged', listener: () => void): void;
|
||||||
|
on(event: 'testFilesChanged', listener: (testFileNames: string[]) => void): void;
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||||
import { ManualPromise } from 'playwright-core/lib/utils';
|
import { ManualPromise, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
||||||
import type { Transport, HttpServer } from 'playwright-core/lib/utils';
|
import type { Transport, HttpServer } from 'playwright-core/lib/utils';
|
||||||
import type { FullResult } from '../../types/testReporter';
|
import type { FullResult, Location, TestError } from '../../types/testReporter';
|
||||||
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
|
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
import { InternalReporter } from '../reporters/internalReporter';
|
import { InternalReporter } from '../reporters/internalReporter';
|
||||||
@ -29,14 +29,15 @@ import ListReporter from '../reporters/list';
|
|||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
import { SigIntWatcher } from './sigIntWatcher';
|
import { SigIntWatcher } from './sigIntWatcher';
|
||||||
import { Watcher } from '../fsWatcher';
|
import { Watcher } from '../fsWatcher';
|
||||||
|
import type { TestServerInterface } from './testServerInterface';
|
||||||
|
import { Runner } from './runner';
|
||||||
|
import { serializeError } from '../util';
|
||||||
|
import { prepareErrorStack } from '../reporters/base';
|
||||||
|
|
||||||
class TestServer {
|
class TestServer {
|
||||||
private _config: FullConfigInternal;
|
private _config: FullConfigInternal;
|
||||||
private _transport: Transport | undefined;
|
private _dispatcher: TestServerDispatcher | undefined;
|
||||||
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
|
||||||
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
||||||
private _globalWatcher: Watcher;
|
|
||||||
private _testWatcher: Watcher;
|
|
||||||
private _originalStdoutWrite: NodeJS.WriteStream['write'];
|
private _originalStdoutWrite: NodeJS.WriteStream['write'];
|
||||||
private _originalStderrWrite: NodeJS.WriteStream['write'];
|
private _originalStderrWrite: NodeJS.WriteStream['write'];
|
||||||
|
|
||||||
@ -56,12 +57,6 @@ class TestServer {
|
|||||||
|
|
||||||
this._originalStdoutWrite = process.stdout.write;
|
this._originalStdoutWrite = process.stdout.write;
|
||||||
this._originalStderrWrite = process.stderr.write;
|
this._originalStderrWrite = process.stderr.write;
|
||||||
this._globalWatcher = new Watcher('deep', () => this._dispatchEvent('listChanged', {}));
|
|
||||||
this._testWatcher = new Watcher('flat', events => {
|
|
||||||
const collector = new Set<string>();
|
|
||||||
events.forEach(f => collectAffectedTestFiles(f.file, collector));
|
|
||||||
this._dispatchEvent('testFilesChanged', { testFileNames: [...collector] });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runGlobalSetup(): Promise<FullResult['status']> {
|
async runGlobalSetup(): Promise<FullResult['status']> {
|
||||||
@ -81,56 +76,18 @@ class TestServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(options: { host?: string, port?: number }): Promise<HttpServer> {
|
async start(options: { host?: string, port?: number }): Promise<HttpServer> {
|
||||||
let queue = Promise.resolve();
|
this._dispatcher = new TestServerDispatcher(this._config);
|
||||||
const transport: Transport = {
|
return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport });
|
||||||
dispatch: async (method, params) => {
|
|
||||||
if (method === 'ping')
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (method === 'watch') {
|
|
||||||
this._watchFiles(params.fileNames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (method === 'open' && params.location) {
|
|
||||||
open('vscode://file/' + params.location).catch(e => this._originalStderrWrite.call(process.stderr, String(e)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (method === 'resizeTerminal') {
|
|
||||||
process.stdout.columns = params.cols;
|
|
||||||
process.stdout.rows = params.rows;
|
|
||||||
process.stderr.columns = params.cols;
|
|
||||||
process.stderr.columns = params.rows;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (method === 'stop') {
|
|
||||||
void this._stopTests();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (method === 'checkBrowsers')
|
|
||||||
return { hasBrowsers: hasSomeBrowsers() };
|
|
||||||
if (method === 'installBrowsers') {
|
|
||||||
await installBrowsers();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queue = queue.then(() => this._queueListOrRun(method, params));
|
|
||||||
await queue;
|
|
||||||
},
|
|
||||||
|
|
||||||
onclose: () => {},
|
|
||||||
};
|
|
||||||
this._transport = transport;
|
|
||||||
return await startTraceViewerServer({ ...options, transport });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wireStdIO() {
|
wireStdIO() {
|
||||||
if (!process.env.PWTEST_DEBUG) {
|
if (!process.env.PWTEST_DEBUG) {
|
||||||
process.stdout.write = (chunk: string | Buffer) => {
|
process.stdout.write = (chunk: string | Buffer) => {
|
||||||
this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
|
this._dispatcher?._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
process.stderr.write = (chunk: string | Buffer) => {
|
process.stderr.write = (chunk: string | Buffer) => {
|
||||||
this._dispatchEvent('stdio', chunkToPayload('stderr', chunk));
|
this._dispatcher?._dispatchEvent('stdio', chunkToPayload('stderr', chunk));
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -142,20 +99,71 @@ class TestServer {
|
|||||||
process.stderr.write = this._originalStderrWrite;
|
process.stderr.write = this._originalStderrWrite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _queueListOrRun(method: string, params: any) {
|
class TestServerDispatcher implements TestServerInterface {
|
||||||
if (method === 'list')
|
private _config: FullConfigInternal;
|
||||||
await this._listTests();
|
private _globalWatcher: Watcher;
|
||||||
if (method === 'run')
|
private _testWatcher: Watcher;
|
||||||
await this._runTests(params.testIds, params.projects);
|
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
||||||
|
readonly transport: Transport;
|
||||||
|
private _queue = Promise.resolve();
|
||||||
|
|
||||||
|
constructor(config: FullConfigInternal) {
|
||||||
|
this._config = config;
|
||||||
|
this.transport = {
|
||||||
|
dispatch: (method, params) => (this as any)[method](params),
|
||||||
|
onclose: () => {},
|
||||||
|
};
|
||||||
|
this._globalWatcher = new Watcher('deep', () => this._dispatchEvent('listChanged', {}));
|
||||||
|
this._testWatcher = new Watcher('flat', events => {
|
||||||
|
const collector = new Set<string>();
|
||||||
|
events.forEach(f => collectAffectedTestFiles(f.file, collector));
|
||||||
|
this._dispatchEvent('testFilesChanged', { testFileNames: [...collector] });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dispatchEvent(method: string, params?: any) {
|
async ping() {}
|
||||||
this._transport?.sendEvent?.(method, params);
|
|
||||||
|
async open(params: { location: Location }) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
open('vscode://file/' + params.location.file + ':' + params.location.column).catch(e => console.error(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _listTests() {
|
async resizeTerminal(params: { cols: number; rows: number; }) {
|
||||||
|
process.stdout.columns = params.cols;
|
||||||
|
process.stdout.rows = params.rows;
|
||||||
|
process.stderr.columns = params.cols;
|
||||||
|
process.stderr.columns = params.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> {
|
||||||
|
return { hasBrowsers: hasSomeBrowsers() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async installBrowsers() {
|
||||||
|
await installBrowsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async listFiles() {
|
||||||
|
try {
|
||||||
|
const runner = new Runner(this._config);
|
||||||
|
return runner.listTestFiles();
|
||||||
|
} catch (e) {
|
||||||
|
const error: TestError = serializeError(e);
|
||||||
|
error.location = prepareErrorStack(e.stack).location;
|
||||||
|
return { projects: [], error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTests(params: { reporter?: string; fileNames: string[]; }) {
|
||||||
|
this._queue = this._queue.then(() => this._innerListTests(params));
|
||||||
|
await this._queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _innerListTests(params: { reporter?: string; fileNames?: string[]; }) {
|
||||||
const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent('listReport', e), { omitBuffers: true }));
|
const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent('listReport', e), { omitBuffers: true }));
|
||||||
|
this._config.cliArgs = params.fileNames || [];
|
||||||
this._config.cliListOnly = true;
|
this._config.cliListOnly = true;
|
||||||
this._config.testIdMatcher = undefined;
|
this._config.testIdMatcher = undefined;
|
||||||
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
|
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
|
||||||
@ -174,12 +182,20 @@ class TestServer {
|
|||||||
this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
|
this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _runTests(testIds: string[], projects: string[]) {
|
async runTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }) {
|
||||||
await this._stopTests();
|
this._queue = this._queue.then(() => this._innerRunTests(params));
|
||||||
|
await this._queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _innerRunTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }) {
|
||||||
|
await this.stop();
|
||||||
|
const { testIds, projects, locations, grep } = params;
|
||||||
|
|
||||||
const testIdSet = testIds ? new Set<string>(testIds) : null;
|
const testIdSet = testIds ? new Set<string>(testIds) : null;
|
||||||
|
this._config.cliArgs = locations ? locations : [];
|
||||||
|
this._config.cliGrep = grep;
|
||||||
this._config.cliListOnly = false;
|
this._config.cliListOnly = false;
|
||||||
this._config.cliProjectFilter = projects.length ? projects : undefined;
|
this._config.cliProjectFilter = projects?.length ? projects : undefined;
|
||||||
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
|
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
|
||||||
|
|
||||||
const reporters = await createReporters(this._config, 'ui');
|
const reporters = await createReporters(this._config, 'ui');
|
||||||
@ -200,19 +216,32 @@ class TestServer {
|
|||||||
await run;
|
await run;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _watchFiles(fileNames: string[]) {
|
async watch(params: { fileNames: string[]; }) {
|
||||||
const files = new Set<string>();
|
const files = new Set<string>();
|
||||||
for (const fileName of fileNames) {
|
for (const fileName of params.fileNames) {
|
||||||
files.add(fileName);
|
files.add(fileName);
|
||||||
dependenciesForTestFile(fileName).forEach(file => files.add(file));
|
dependenciesForTestFile(fileName).forEach(file => files.add(file));
|
||||||
}
|
}
|
||||||
this._testWatcher.update([...files], [], true);
|
this._testWatcher.update([...files], [], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _stopTests() {
|
findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: TestError[] | undefined; }> {
|
||||||
|
const runner = new Runner(this._config);
|
||||||
|
return runner.findRelatedTestFiles('out-of-process', params.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
this._testRun?.stop?.resolve();
|
this._testRun?.stop?.resolve();
|
||||||
await this._testRun?.run;
|
await this._testRun?.run;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeGracefully() {
|
||||||
|
gracefullyProcessExitDoNotHang(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_dispatchEvent(method: string, params?: any) {
|
||||||
|
this.transport.sendEvent?.(method, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<FullResult['status']> {
|
export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<FullResult['status']> {
|
||||||
|
@ -166,7 +166,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||||||
setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
||||||
setRunningState({ testIds });
|
setRunningState({ testIds });
|
||||||
|
|
||||||
await sendMessage('run', { testIds: [...testIds], projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p) });
|
await sendMessage('runTests', { testIds: [...testIds], projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p) });
|
||||||
// Clear pending tests in case of interrupt.
|
// Clear pending tests in case of interrupt.
|
||||||
for (const test of testModel.rootSuite?.allTests() || []) {
|
for (const test of testModel.rootSuite?.allTests() || []) {
|
||||||
if (test.results[0]?.duration === -1)
|
if (test.results[0]?.duration === -1)
|
||||||
@ -625,7 +625,7 @@ const refreshRootSuite = (): Promise<void> => {
|
|||||||
},
|
},
|
||||||
pathSeparator,
|
pathSeparator,
|
||||||
});
|
});
|
||||||
return sendMessage('list', {});
|
return sendMessage('listTests', {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessageNoReply = (method: string, params?: any) => {
|
const sendMessageNoReply = (method: string, params?: any) => {
|
||||||
@ -641,7 +641,7 @@ const sendMessageNoReply = (method: string, params?: any) => {
|
|||||||
|
|
||||||
const dispatchEvent = (method: string, params?: any) => {
|
const dispatchEvent = (method: string, params?: any) => {
|
||||||
if (method === 'listChanged') {
|
if (method === 'listChanged') {
|
||||||
sendMessage('list', {}).catch(() => {});
|
sendMessage('listTests', {}).catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,6 +286,6 @@ test('should not watch output', async ({ runUITest }) => {
|
|||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
expect(commands).toContain('run');
|
expect(commands).toContain('runTests');
|
||||||
expect(commands).not.toContain('list');
|
expect(commands).not.toContain('listTests');
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user