diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 06e4ef7d6b653..3e7463e62e558 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -94,9 +94,9 @@ async function startTraceViewerServer(traceUrls: string[], options?: Options): P }); const params = traceUrls.map(t => `trace=${t}`); + const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined); - if (options?.transport) { - const transport = options?.transport; + if (transport) { const guid = createGuid(); params.push('ws=' + guid); const wss = new wsServer({ server: server.server(), path: '/' + guid }); @@ -135,7 +135,6 @@ async function startTraceViewerServer(traceUrls: string[], options?: Options): P } export async function openTraceViewerApp(traceUrls: string[], browserName: string, options?: Options): Promise { - const stdinServer = options?.isServer ? new StdinServer() : undefined; const { url } = await startTraceViewerServer(traceUrls, options); const traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true }); const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName; @@ -176,7 +175,6 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin page.on('close', () => process.exit()); await page.mainFrame().goto(serverSideCallMetadata(), url); - stdinServer?.setPage(page); return page; } @@ -187,7 +185,7 @@ async function openTraceInBrowser(traceUrls: string[], options?: Options) { await open(url, { wait: true }).catch(() => {}); } -class StdinServer { +class StdinServer implements Transport { private _pollTimer: NodeJS.Timeout | undefined; private _traceUrl: string | undefined; private _page: Page | undefined; @@ -197,7 +195,6 @@ class StdinServer { const url = data.toString().trim(); if (url === this._traceUrl) return; - this._traceUrl = url; if (url.endsWith('.json')) this._pollLoadTrace(url); else @@ -206,15 +203,24 @@ class StdinServer { process.stdin.on('close', () => this._selfDestruct()); } - setPage(page: Page) { - this._page = page; - if (this._traceUrl) - this._loadTrace(this._traceUrl); + async dispatch(method: string, params: any) { + if (method === 'ready') { + if (this._traceUrl) + this._loadTrace(this._traceUrl); + } + } + + onclose() { + this._selfDestruct(); } + sendEvent?: (method: string, params: any) => void; + close?: () => void; + private _loadTrace(url: string) { + this._traceUrl = url; clearTimeout(this._pollTimer); - this._page?.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`).catch(() => {}); + this.sendEvent?.('loadTrace', { url }); } private _pollLoadTrace(url: string) { diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index a689ada37b59e..a800a94d8b6d8 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -87,6 +87,9 @@ class UIMode { this._transport = { dispatch: async (method, params) => { + if (method === 'ping') + return; + if (method === 'exit') { exitPromise.resolve(); return; diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 25eda6cfbd39c..c8a687f1abcbc 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -37,11 +37,14 @@ import { toggleTheme } from '@web/theme'; import { artifactsFolderName } from '@testIsomorphic/folders'; import { msToString, settings, useSetting } from '@web/uiUtils'; import type { ActionTraceEvent } from '@trace/trace'; +import { connect } from './wsPort'; let updateRootSuite: (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress | undefined) => void = () => {}; let runWatchedTests = (fileNames: string[]) => {}; let xtermSize = { cols: 80, rows: 24 }; +let sendMessage: (method: string, params?: any) => Promise = async () => {}; + const xtermDataSource: XtermDataSource = { pending: [], clear: () => {}, @@ -96,7 +99,10 @@ export const UIModeView: React.FC<{}> = ({ React.useEffect(() => { inputRef.current?.focus(); setIsLoading(true); - initWebSocket(() => setIsDisconnected(true)).then(() => reloadTests()); + connect({ onEvent: dispatchEvent, onClose: () => setIsDisconnected(true) }).then(send => { + sendMessage = send; + reloadTests(); + }); }, [reloadTests]); updateRootSuite = React.useCallback((config: FullConfig, rootSuite: Suite, loadErrors: TestError[], newProgress: Progress | undefined) => { @@ -639,43 +645,6 @@ const refreshRootSuite = (eraseResults: boolean): Promise => { return sendMessage('list', {}); }; -let lastId = 0; -let _ws: WebSocket; -const callbacks = new Map void, reject: (arg: Error) => void }>(); - -const initWebSocket = async (onClose: () => void) => { - const guid = new URLSearchParams(window.location.search).get('ws'); - const ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.hostname}:${window.location.port}/${guid}`); - await new Promise(f => ws.addEventListener('open', f)); - ws.addEventListener('close', 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 { - dispatchMessage(method, params); - } - }); - _ws = ws; -}; - -const sendMessage = async (method: string, params: any): Promise => { - const id = ++lastId; - const message = { id, method, params }; - _ws.send(JSON.stringify(message)); - return new Promise((resolve, reject) => { - callbacks.set(id, { resolve, reject }); - }); -}; - const sendMessageNoReply = (method: string, params?: any) => { if ((window as any)._overrideProtocolForTest) { (window as any)._overrideProtocolForTest({ method, params }).catch(() => {}); @@ -687,7 +656,7 @@ const sendMessageNoReply = (method: string, params?: any) => { }); }; -const dispatchMessage = (method: string, params?: any) => { +const dispatchEvent = (method: string, params?: any) => { if (method === 'listChanged') { refreshRootSuite(false).catch(() => {}); return; diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 3445665ce3fb8..066572171cd09 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -21,6 +21,7 @@ import { MultiTraceModel } from './modelUtil'; import './workbench.css'; import { toggleTheme } from '@web/theme'; import { Workbench } from './workbench'; +import { connect } from './wsPort'; export const WorkbenchLoader: React.FunctionComponent<{ }> = () => { @@ -82,13 +83,19 @@ export const WorkbenchLoader: React.FunctionComponent<{ } } - (window as any).setTraceURL = (url: string) => { - setTraceURLs([url]); - setDragOver(false); - setProcessingErrorMessage(null); - }; - if (earlyTraceURL) { - (window as any).setTraceURL(earlyTraceURL); + if (params.has('isServer')) { + connect({ + onEvent(method: string, params?: any) { + if (method === 'loadTrace') { + setTraceURLs([params!.url]); + setDragOver(false); + setProcessingErrorMessage(null); + } + }, + onClose() {} + }).then(sendMessage => { + sendMessage('ready'); + }); } else if (!newTraceURLs.some(url => url.startsWith('blob:'))) { // Don't re-use blob file URLs on page load (results in Fetch error) setTraceURLs(newTraceURLs); @@ -176,9 +183,3 @@ export const WorkbenchLoader: React.FunctionComponent<{ }; export const emptyModel = new MultiTraceModel([]); - -let earlyTraceURL: string | undefined = undefined; - -(window as any).setTraceURL = (url: string) => { - earlyTraceURL = url; -}; diff --git a/packages/trace-viewer/src/ui/wsPort.ts b/packages/trace-viewer/src/ui/wsPort.ts new file mode 100644 index 0000000000000..fc08d4cdf445c --- /dev/null +++ b/packages/trace-viewer/src/ui/wsPort.ts @@ -0,0 +1,54 @@ +/** + * 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 void, reject: (arg: Error) => void }>(); + +export async function connect(options: { onEvent: (method: string, params?: any) => void, onClose: () => void }): Promise<(method: string, params?: any) => Promise> { + const guid = new URLSearchParams(window.location.search).get('ws'); + const ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.hostname}:${window.location.port}/${guid}`); + 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 => { + const id = ++lastId; + const message = { id, method, params }; + _ws.send(JSON.stringify(message)); + return new Promise((resolve, reject) => { + callbacks.set(id, { resolve, reject }); + }); +};