diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index dd8d181676..a74a3796b4 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -114,6 +114,7 @@ function addTestServerCommand(program: Command) { command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); command.option('--host ', 'Host to start the server on', 'localhost'); command.option('--port ', 'Port to start the server on', '0'); + command.option('--ide-mode', 'IDE node'); command.action(opts => runTestServer(opts)); } @@ -227,7 +228,8 @@ async function runTests(args: string[], opts: { [key: string]: any }) { async function runTestServer(opts: { [key: string]: any }) { const host = opts.host || 'localhost'; const port = opts.port ? +opts.port : 0; - const status = await testServer.runTestServer(opts.config, { host, port }); + const ideMode = !!opts.ideMode; + const status = await testServer.runTestServer(opts.config, { host, port, ideMode }); if (status === 'restarted') return; const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 74f712df4f..804708c4ac 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -479,15 +479,17 @@ export async function runUIMode(configFile: string | undefined, options: TraceVi }); } -export async function runTestServer(configFile: string | undefined, options: { host?: string, port?: number }): Promise { +export async function runTestServer(configFile: string | undefined, options: { host?: string, port?: number, ideMode?: boolean }): Promise { const configLocation = resolveConfigLocation(configFile); return await innerRunTestServer(configLocation, options, async server => { + if (options.ideMode) + await installRootRedirect(server, [], { ...options, webApp: 'ideMode.html' }); // eslint-disable-next-line no-console console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid()); }); } -async function innerRunTestServer(configLocation: ConfigLocation, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise, configLocation: ConfigLocation) => Promise): Promise { +async function innerRunTestServer(configLocation: ConfigLocation, options: { host?: string, port?: number, ideMode?: boolean }, openUI: (server: HttpServer, cancelPromise: ManualPromise, configLocation: ConfigLocation) => Promise): Promise { if (restartWithExperimentalTsEsm(undefined, true)) return 'restarted'; const testServer = new TestServer(configLocation); diff --git a/packages/trace-viewer/embedded.html b/packages/trace-viewer/ideMode.html similarity index 82% rename from packages/trace-viewer/embedded.html rename to packages/trace-viewer/ideMode.html index 7d0fd2f175..514804219b 100644 --- a/packages/trace-viewer/embedded.html +++ b/packages/trace-viewer/ideMode.html @@ -18,10 +18,11 @@ - Playwright Trace Viewer for VS Code + + Playwright Trace Viewer
- + diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/ideMode.tsx similarity index 68% rename from packages/trace-viewer/src/embedded.tsx rename to packages/trace-viewer/src/ideMode.tsx index 4f1503dcf2..c9340d072c 100644 --- a/packages/trace-viewer/src/embedded.tsx +++ b/packages/trace-viewer/src/ideMode.tsx @@ -18,30 +18,10 @@ import '@web/common.css'; import { applyTheme } from '@web/theme'; import '@web/third_party/vscode/codicon.css'; import * as ReactDOM from 'react-dom/client'; -import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader'; +import { IDEModeView } from './ui/ideModeView'; (async () => { applyTheme(); - - // workaround to send keystrokes back to vscode webview to keep triggering key bindings there - const handleKeyEvent = (e: KeyboardEvent) => { - if (!e.isTrusted) - return; - window.parent?.postMessage({ - type: e.type, - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat, - }, '*'); - }; - window.addEventListener('keydown', handleKeyEvent); - window.addEventListener('keyup', handleKeyEvent); - if (window.location.protocol !== 'file:') { if (!navigator.serviceWorker) throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`); @@ -56,5 +36,5 @@ import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader'; setInterval(function() { fetch('ping'); }, 10000); } - ReactDOM.createRoot(document.querySelector('#root')!).render(); + ReactDOM.createRoot(document.querySelector('#root')!).render(); })(); diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css b/packages/trace-viewer/src/ui/ideModeView.css similarity index 68% rename from packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css rename to packages/trace-viewer/src/ui/ideModeView.css index 2274355322..aef3fe4a8f 100644 --- a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css +++ b/packages/trace-viewer/src/ui/ideModeView.css @@ -48,6 +48,7 @@ body.dark-mode .empty-state { flex: none; width: 100%; height: 3px; + margin-top: -3px; z-index: 10; } @@ -56,10 +57,51 @@ body.dark-mode .empty-state { height: 100%; } -.workbench-loader { +.header { + display: flex; + background-color: #000; + flex: none; + flex-basis: 48px; + line-height: 48px; + font-size: 16px; + color: #cccccc; +} + +.ide-mode { contain: size; } +.ide-mode .header .toolbar-button { + margin: 12px; + padding: 8px 4px; +} + +.ide-mode .logo { + margin-left: 16px; + display: flex; + align-items: center; +} + +.ide-mode .logo img { + height: 32px; + width: 32px; + pointer-events: none; + flex: none; +} + +.ide-mode .product { + font-weight: 600; + margin-left: 16px; + flex: none; +} + +.ide-mode .header .title { + margin-left: 16px; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; +} + /* Limit to a reasonable minimum viewport */ html, body { min-width: 550px; diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/ideModeView.tsx similarity index 54% rename from packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx rename to packages/trace-viewer/src/ui/ideModeView.tsx index 1f1b5327da..761f68c163 100644 --- a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/ideModeView.tsx @@ -14,42 +14,28 @@ limitations under the License. */ +import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection'; +import { ToolbarButton } from '@web/components/toolbarButton'; +import { toggleTheme } from '@web/theme'; import * as React from 'react'; import type { ContextEntry } from '../entries'; +import './ideModeView.css'; +import type { ActionTraceEventInContext } from './modelUtil'; import { MultiTraceModel } from './modelUtil'; -import './embeddedWorkbenchLoader.css'; import { Workbench } from './workbench'; -import { currentTheme, toggleTheme } from '@web/theme'; -import type { SourceLocation } from './modelUtil'; -function openPage(url: string, target?: string) { - if (url) - window.parent!.postMessage({ method: 'openExternal', params: { url, target } }, '*'); -} - -function openSourceLocation({ file, line, column }: SourceLocation) { - window.parent!.postMessage({ method: 'openSourceLocation', params: { file, line, column } }, '*'); -} - -export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => { +export const IDEModeView: React.FunctionComponent = () => { const [traceURLs, setTraceURLs] = React.useState([]); const [model, setModel] = React.useState(emptyModel); const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); - const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null); + const [testServerConnection, setTestServerConnection] = React.useState(); - React.useEffect(() => { - window.addEventListener('message', async ({ data: { method, params } }) => { - if (method === 'loadTraceRequested') { - setTraceURLs(params.traceUrl ? [params.traceUrl] : []); - setProcessingErrorMessage(null); - } else if (method === 'applyTheme') { - if (currentTheme() !== params.theme) - toggleTheme(); - } - }); - // notify vscode that it is now listening to its messages - window.parent!.postMessage({ type: 'loaded' }, '*'); - }, []); + const selectionChanged = React.useCallback((action: ActionTraceEventInContext) => { + if (!testServerConnection || !action?.stack || action.stack.length === 0) + return; + const [{ file, line, column }] = action.stack; + testServerConnection.dispatchTraceViewerEventNoReply({ method: 'openSourceLocation', params: { file, line, column } }); + }, [testServerConnection]); React.useEffect(() => { (async () => { @@ -66,10 +52,8 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => { const params = new URLSearchParams(); params.set('trace', url); const response = await fetch(`contexts?${params.toString()}`); - if (!response.ok) { - setProcessingErrorMessage((await response.json()).error); + if (!response.ok) return; - } contextEntries.push(...(await response.json())); } navigator.serviceWorker.removeEventListener('message', swListener); @@ -83,15 +67,31 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => { }, [traceURLs]); React.useEffect(() => { - if (processingErrorMessage) - window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*'); - }, [processingErrorMessage]); + 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(new WebSocketTestServerTransport(wsURL)); + testServerConnection.onLoadTraceRequested(async params => { + setTraceURLs(params.traceUrl ? [params.traceUrl] : []); + }); + testServerConnection.dispatchTraceViewerEventNoReply({ method: 'loaded', params: {} }); + setTestServerConnection(testServerConnection); + }, []); - return
+ return
+
+
+ Playwright logo +
+
Playwright
+ {model.title &&
{model.title}
} +
+ toggleTheme()}> +
- + {!traceURLs.length &&
Select test to see the trace
} diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index 0e2e9cb642..15377ad88c 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -44,7 +44,7 @@ export default defineConfig({ input: { index: path.resolve(__dirname, 'index.html'), uiMode: path.resolve(__dirname, 'uiMode.html'), - embedded: path.resolve(__dirname, 'embedded.html'), + ideMode: path.resolve(__dirname, 'ideMode.html'), recorder: path.resolve(__dirname, 'recorder.html'), snapshot: path.resolve(__dirname, 'snapshot.html'), },