mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
chore: prepare to reuse test server from ui mode (5) (#30005)
This commit is contained in:
parent
1bb463163b
commit
0a22a86e2e
@ -210,10 +210,10 @@ class StdinServer implements Transport {
|
||||
sendEvent?: (method: string, params: any) => void;
|
||||
close?: () => void;
|
||||
|
||||
private _loadTrace(url: string) {
|
||||
this._traceUrl = url;
|
||||
private _loadTrace(traceUrl: string) {
|
||||
this._traceUrl = traceUrl;
|
||||
clearTimeout(this._pollTimer);
|
||||
this.sendEvent?.('loadTrace', { url });
|
||||
this.sendEvent?.('loadTraceRequested', { traceUrl });
|
||||
}
|
||||
|
||||
private _pollLoadTrace(url: string) {
|
||||
|
@ -16,67 +16,94 @@
|
||||
|
||||
import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface';
|
||||
import type { Location, TestError } from 'playwright/types/testReporter';
|
||||
import { connect } from './wsPort';
|
||||
import type { Event } from '@testIsomorphic/events';
|
||||
import { EventEmitter } from '@testIsomorphic/events';
|
||||
import * as events from './events';
|
||||
|
||||
export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
|
||||
readonly onClose: Event<void>;
|
||||
readonly onListReport: Event<any>;
|
||||
readonly onTestReport: Event<any>;
|
||||
readonly onStdio: Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>;
|
||||
readonly onListChanged: Event<void>;
|
||||
readonly onTestFilesChanged: Event<string[]>;
|
||||
readonly onClose: events.Event<void>;
|
||||
readonly onListReport: events.Event<any>;
|
||||
readonly onTestReport: events.Event<any>;
|
||||
readonly onStdio: events.Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>;
|
||||
readonly onListChanged: events.Event<void>;
|
||||
readonly onTestFilesChanged: events.Event<{ testFiles: string[] }>;
|
||||
readonly onLoadTraceRequested: events.Event<{ traceUrl: string }>;
|
||||
|
||||
private _onCloseEmitter = new EventEmitter<void>();
|
||||
private _onListReportEmitter = new EventEmitter<any>();
|
||||
private _onTestReportEmitter = new EventEmitter<any>();
|
||||
private _onStdioEmitter = new EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>();
|
||||
private _onListChangedEmitter = new EventEmitter<void>();
|
||||
private _onTestFilesChangedEmitter = new EventEmitter<string[]>();
|
||||
private _onCloseEmitter = new events.EventEmitter<void>();
|
||||
private _onListReportEmitter = new events.EventEmitter<any>();
|
||||
private _onTestReportEmitter = new events.EventEmitter<any>();
|
||||
private _onStdioEmitter = new events.EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>();
|
||||
private _onListChangedEmitter = new events.EventEmitter<void>();
|
||||
private _onTestFilesChangedEmitter = new events.EventEmitter<{ testFiles: string[] }>();
|
||||
private _onLoadTraceRequestedEmitter = new events.EventEmitter<{ traceUrl: string }>();
|
||||
|
||||
private _send: Promise<(method: string, params?: any) => Promise<any>>;
|
||||
private _lastId = 0;
|
||||
private _ws: WebSocket;
|
||||
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
|
||||
private _connectedPromise: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
constructor(wsURL: string) {
|
||||
this.onClose = this._onCloseEmitter.event;
|
||||
this.onListReport = this._onListReportEmitter.event;
|
||||
this.onTestReport = this._onTestReportEmitter.event;
|
||||
this.onStdio = this._onStdioEmitter.event;
|
||||
this.onListChanged = this._onListChangedEmitter.event;
|
||||
this.onTestFilesChanged = this._onTestFilesChangedEmitter.event;
|
||||
this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event;
|
||||
|
||||
this._send = connect({
|
||||
onEvent: (method, params) => this._dispatchEvent(method, params),
|
||||
onClose: () => this._onCloseEmitter.fire(),
|
||||
this._ws = new WebSocket(wsURL);
|
||||
this._ws.addEventListener('message', event => {
|
||||
const message = JSON.parse(String(event.data));
|
||||
const { id, result, error, method, params } = message;
|
||||
if (id) {
|
||||
const callback = this._callbacks.get(id);
|
||||
if (!callback)
|
||||
return;
|
||||
this._callbacks.delete(id);
|
||||
if (error)
|
||||
callback.reject(new Error(error));
|
||||
else
|
||||
callback.resolve(result);
|
||||
} else {
|
||||
this._dispatchEvent(method, params);
|
||||
}
|
||||
});
|
||||
const pingInterval = setInterval(() => this._sendMessage('ping').catch(() => {}), 30000);
|
||||
this._connectedPromise = new Promise<void>((f, r) => {
|
||||
this._ws.addEventListener('open', () => {
|
||||
f();
|
||||
this._ws.send(JSON.stringify({ method: 'ready' }));
|
||||
});
|
||||
this._ws.addEventListener('error', r);
|
||||
});
|
||||
this._ws.addEventListener('close', () => {
|
||||
this._onCloseEmitter.fire();
|
||||
clearInterval(pingInterval);
|
||||
});
|
||||
}
|
||||
|
||||
private async _sendMessage(method: string, params?: any): Promise<any> {
|
||||
if ((window as any)._sniffProtocolForTest)
|
||||
(window as any)._sniffProtocolForTest({ method, params }).catch(() => {});
|
||||
|
||||
const send = await this._send;
|
||||
const logForTest = (window as any).__logForTest;
|
||||
const logForTest = (globalThis as any).__logForTest;
|
||||
logForTest?.({ method, params });
|
||||
return send(method, params).catch((e: Error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
|
||||
await this._connectedPromise;
|
||||
const id = ++this._lastId;
|
||||
const message = { id, method, params };
|
||||
this._ws.send(JSON.stringify(message));
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
private _dispatchEvent(method: string, params?: any) {
|
||||
if (method === 'close')
|
||||
this._onCloseEmitter.fire(undefined);
|
||||
else if (method === 'listReport')
|
||||
if (method === 'listReport')
|
||||
this._onListReportEmitter.fire(params);
|
||||
else if (method === 'testReport')
|
||||
this._onTestReportEmitter.fire(params);
|
||||
else if (method === 'stdio')
|
||||
this._onStdioEmitter.fire(params);
|
||||
else if (method === 'listChanged')
|
||||
this._onListChangedEmitter.fire(undefined);
|
||||
this._onListChangedEmitter.fire(params);
|
||||
else if (method === 'testFilesChanged')
|
||||
this._onTestFilesChangedEmitter.fire(params.testFileNames);
|
||||
this._onTestFilesChangedEmitter.fire(params);
|
||||
}
|
||||
|
||||
async ping(): Promise<void> {
|
@ -80,5 +80,16 @@ export interface TestServerInterfaceEvents {
|
||||
onTestReport: Event<any>;
|
||||
onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>;
|
||||
onListChanged: Event<void>;
|
||||
onTestFilesChanged: Event<string[]>;
|
||||
onTestFilesChanged: Event<{ testFiles: string[] }>;
|
||||
onLoadTraceRequested: Event<{ traceUrl: string }>;
|
||||
}
|
||||
|
||||
export interface TestServerInterfaceEventEmitters {
|
||||
dispatchEvent(event: 'close', params: {}): void;
|
||||
dispatchEvent(event: 'listReport', params: any): void;
|
||||
dispatchEvent(event: 'testReport', params: any): void;
|
||||
dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void;
|
||||
dispatchEvent(event: 'listChanged', params: {}): void;
|
||||
dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void;
|
||||
dispatchEvent(event: 'loadTraceRequested', params: { traceUrl: string }): void;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ export type TestCaseItem = TreeItemBase & {
|
||||
children: TestItem[];
|
||||
test: reporterTypes.TestCase | undefined;
|
||||
project: reporterTypes.FullProject | undefined;
|
||||
tags: Array<string>;
|
||||
};
|
||||
|
||||
export type TestItem = TreeItemBase & {
|
||||
@ -112,6 +113,7 @@ export class TestTree {
|
||||
status: 'none',
|
||||
project: undefined,
|
||||
test: undefined,
|
||||
tags: test.tags,
|
||||
};
|
||||
this._addChild(parentGroup, testCaseItem);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import ListReporter from '../reporters/list';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { SigIntWatcher } from './sigIntWatcher';
|
||||
import { Watcher } from '../fsWatcher';
|
||||
import type { TestServerInterface } from '../isomorphic/testServerInterface';
|
||||
import type { TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
|
||||
import { Runner } from './runner';
|
||||
import { serializeError } from '../util';
|
||||
import { prepareErrorStack } from '../reporters/base';
|
||||
@ -95,6 +95,7 @@ class TestServerDispatcher implements TestServerInterface {
|
||||
readonly transport: Transport;
|
||||
private _queue = Promise.resolve();
|
||||
private _globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
||||
readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
|
||||
|
||||
constructor(config: FullConfigInternal) {
|
||||
this._config = config;
|
||||
@ -106,8 +107,9 @@ class TestServerDispatcher implements TestServerInterface {
|
||||
this._testWatcher = new Watcher('flat', events => {
|
||||
const collector = new Set<string>();
|
||||
events.forEach(f => collectAffectedTestFiles(f.file, collector));
|
||||
this._dispatchEvent('testFilesChanged', { testFileNames: [...collector] });
|
||||
this._dispatchEvent('testFilesChanged', { testFiles: [...collector] });
|
||||
});
|
||||
this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
|
||||
}
|
||||
|
||||
async ping() {}
|
||||
@ -252,10 +254,6 @@ class TestServerDispatcher implements TestServerInterface {
|
||||
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']> {
|
||||
|
@ -42,7 +42,7 @@ import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
||||
import type { TreeItem } from '@testIsomorphic/testTree';
|
||||
import { testStatusIcon } from './testUtils';
|
||||
import { TestServerConnection } from './testServerConnection';
|
||||
import { TestServerConnection } from '@testIsomorphic/testServerConnection';
|
||||
|
||||
let updateRootSuite: (config: reporterTypes.FullConfig, rootSuite: reporterTypes.Suite, loadErrors: reporterTypes.TestError[], progress: Progress | undefined) => void = () => {};
|
||||
let runWatchedTests = (fileNames: string[]) => {};
|
||||
@ -90,7 +90,10 @@ export const UIModeView: React.FC<{}> = ({
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const reloadTests = React.useCallback(() => {
|
||||
const connection = new TestServerConnection();
|
||||
const guid = new URLSearchParams(window.location.search).get('ws');
|
||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||
const connection = new TestServerConnection(wsURL.toString());
|
||||
wireConnectionListeners(connection);
|
||||
connection.onClose(() => setIsDisconnected(true));
|
||||
setTestServerConnection(connection);
|
||||
@ -653,8 +656,8 @@ const wireConnectionListeners = (testServerConnection: TestServerConnection) =>
|
||||
testServerConnection.listTests({}).catch(() => {});
|
||||
});
|
||||
|
||||
testServerConnection.onTestFilesChanged(testFiles => {
|
||||
runWatchedTests(testFiles);
|
||||
testServerConnection.onTestFilesChanged(params => {
|
||||
runWatchedTests(params.testFiles);
|
||||
});
|
||||
|
||||
testServerConnection.onStdio(params => {
|
||||
|
@ -21,7 +21,7 @@ import { MultiTraceModel } from './modelUtil';
|
||||
import './workbenchLoader.css';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
import { Workbench } from './workbench';
|
||||
import { connect } from './wsPort';
|
||||
import { TestServerConnection } from '@testIsomorphic/testServerConnection';
|
||||
|
||||
export const WorkbenchLoader: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
@ -84,17 +84,14 @@ export const WorkbenchLoader: React.FunctionComponent<{
|
||||
}
|
||||
|
||||
if (params.has('isServer')) {
|
||||
connect({
|
||||
onEvent(method: string, params?: any) {
|
||||
if (method === 'loadTrace') {
|
||||
setTraceURLs(params!.url ? [params!.url] : []);
|
||||
setDragOver(false);
|
||||
setProcessingErrorMessage(null);
|
||||
}
|
||||
},
|
||||
onClose() {}
|
||||
}).then(sendMessage => {
|
||||
sendMessage('ready');
|
||||
const guid = new URLSearchParams(window.location.search).get('ws');
|
||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||
const testServerConnection = new TestServerConnection(wsURL.toString());
|
||||
testServerConnection.onLoadTraceRequested(async params => {
|
||||
setTraceURLs(params.traceUrl ? [params.traceUrl] : []);
|
||||
setDragOver(false);
|
||||
setProcessingErrorMessage(null);
|
||||
});
|
||||
} else if (!newTraceURLs.some(url => url.startsWith('blob:'))) {
|
||||
// Don't re-use blob file URLs on page load (results in Fetch error)
|
||||
|
@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
let lastId = 0;
|
||||
let _ws: WebSocket;
|
||||
const callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
|
||||
|
||||
export async function connect(options: { onEvent: (method: string, params?: any) => void, onClose: () => void }): Promise<(method: string, params?: any) => Promise<any>> {
|
||||
const guid = new URLSearchParams(window.location.search).get('ws');
|
||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||
const ws = new WebSocket(wsURL);
|
||||
await new Promise(f => ws.addEventListener('open', f));
|
||||
ws.addEventListener('close', options.onClose);
|
||||
ws.addEventListener('message', event => {
|
||||
const message = JSON.parse(event.data);
|
||||
const { id, result, error, method, params } = message;
|
||||
if (id) {
|
||||
const callback = callbacks.get(id);
|
||||
if (!callback)
|
||||
return;
|
||||
callbacks.delete(id);
|
||||
if (error)
|
||||
callback.reject(new Error(error));
|
||||
else
|
||||
callback.resolve(result);
|
||||
} else {
|
||||
options.onEvent(method, params);
|
||||
}
|
||||
});
|
||||
_ws = ws;
|
||||
setInterval(() => sendMessage('ping').catch(() => {}), 30000);
|
||||
return sendMessage;
|
||||
}
|
||||
|
||||
const sendMessage = async (method: string, params?: any): Promise<any> => {
|
||||
const id = ++lastId;
|
||||
const message = { id, method, params };
|
||||
_ws.send(JSON.stringify(message));
|
||||
return new Promise((resolve, reject) => {
|
||||
callbacks.set(id, { resolve, reject });
|
||||
});
|
||||
};
|
@ -169,7 +169,7 @@ test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, d
|
||||
`);
|
||||
});
|
||||
|
||||
test('should update test locations', async ({ runUITest, writeFiles, deleteFile }) => {
|
||||
test('should update test locations', async ({ runUITest, writeFiles }) => {
|
||||
const { page } = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
@ -182,8 +182,8 @@ test('should update test locations', async ({ runUITest, writeFiles, deleteFile
|
||||
◯ passes
|
||||
`);
|
||||
|
||||
const messages: any = [];
|
||||
await page.exposeBinding('_sniffProtocolForTest', (_, data) => messages.push(data));
|
||||
const messages: any[] = [];
|
||||
await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg));
|
||||
|
||||
const passesItemLocator = page.getByRole('listitem').filter({ hasText: 'passes' });
|
||||
await passesItemLocator.hover();
|
||||
|
Loading…
Reference in New Issue
Block a user