chore(trace-viewer): ide mode
This commit is contained in:
parent
b487297460
commit
a55a2d933c
|
|
@ -114,6 +114,7 @@ function addTestServerCommand(program: Command) {
|
||||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||||
command.option('--host <host>', 'Host to start the server on', 'localhost');
|
command.option('--host <host>', 'Host to start the server on', 'localhost');
|
||||||
command.option('--port <port>', 'Port to start the server on', '0');
|
command.option('--port <port>', 'Port to start the server on', '0');
|
||||||
|
command.option('--ide-mode', 'IDE node');
|
||||||
command.action(opts => runTestServer(opts));
|
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 }) {
|
async function runTestServer(opts: { [key: string]: any }) {
|
||||||
const host = opts.host || 'localhost';
|
const host = opts.host || 'localhost';
|
||||||
const port = opts.port ? +opts.port : 0;
|
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')
|
if (status === 'restarted')
|
||||||
return;
|
return;
|
||||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||||
|
|
|
||||||
|
|
@ -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<reporterTypes.FullResult['status'] | 'restarted'> {
|
export async function runTestServer(configFile: string | undefined, options: { host?: string, port?: number, ideMode?: boolean }): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
|
||||||
const configLocation = resolveConfigLocation(configFile);
|
const configLocation = resolveConfigLocation(configFile);
|
||||||
return await innerRunTestServer(configLocation, options, async server => {
|
return await innerRunTestServer(configLocation, options, async server => {
|
||||||
|
if (options.ideMode)
|
||||||
|
await installRootRedirect(server, [], { ...options, webApp: 'ideMode.html' });
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid());
|
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<void>, configLocation: ConfigLocation) => Promise<void>): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
|
async function innerRunTestServer(configLocation: ConfigLocation, options: { host?: string, port?: number, ideMode?: boolean }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>, configLocation: ConfigLocation) => Promise<void>): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
|
||||||
if (restartWithExperimentalTsEsm(undefined, true))
|
if (restartWithExperimentalTsEsm(undefined, true))
|
||||||
return 'restarted';
|
return 'restarted';
|
||||||
const testServer = new TestServer(configLocation);
|
const testServer = new TestServer(configLocation);
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Playwright Trace Viewer for VS Code</title>
|
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
|
||||||
|
<title>Playwright Trace Viewer</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/embedded.tsx"></script>
|
<script type="module" src="/src/ideMode.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -18,30 +18,10 @@ import '@web/common.css';
|
||||||
import { applyTheme } from '@web/theme';
|
import { applyTheme } from '@web/theme';
|
||||||
import '@web/third_party/vscode/codicon.css';
|
import '@web/third_party/vscode/codicon.css';
|
||||||
import * as ReactDOM from 'react-dom/client';
|
import * as ReactDOM from 'react-dom/client';
|
||||||
import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
|
import { IDEModeView } from './ui/ideModeView';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
applyTheme();
|
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 (window.location.protocol !== 'file:') {
|
||||||
if (!navigator.serviceWorker)
|
if (!navigator.serviceWorker)
|
||||||
throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);
|
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);
|
setInterval(function() { fetch('ping'); }, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.querySelector('#root')!).render(<EmbeddedWorkbenchLoader />);
|
ReactDOM.createRoot(document.querySelector('#root')!).render(<IDEModeView />);
|
||||||
})();
|
})();
|
||||||
|
|
@ -48,6 +48,7 @@ body.dark-mode .empty-state {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
margin-top: -3px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,10 +57,51 @@ body.dark-mode .empty-state {
|
||||||
height: 100%;
|
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;
|
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 */
|
/* Limit to a reasonable minimum viewport */
|
||||||
html, body {
|
html, body {
|
||||||
min-width: 550px;
|
min-width: 550px;
|
||||||
|
|
@ -14,42 +14,28 @@
|
||||||
limitations under the License.
|
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 * as React from 'react';
|
||||||
import type { ContextEntry } from '../entries';
|
import type { ContextEntry } from '../entries';
|
||||||
|
import './ideModeView.css';
|
||||||
|
import type { ActionTraceEventInContext } from './modelUtil';
|
||||||
import { MultiTraceModel } from './modelUtil';
|
import { MultiTraceModel } from './modelUtil';
|
||||||
import './embeddedWorkbenchLoader.css';
|
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import { currentTheme, toggleTheme } from '@web/theme';
|
|
||||||
import type { SourceLocation } from './modelUtil';
|
|
||||||
|
|
||||||
function openPage(url: string, target?: string) {
|
export const IDEModeView: React.FunctionComponent = () => {
|
||||||
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 = () => {
|
|
||||||
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
|
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
|
||||||
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
|
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
|
||||||
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
|
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
|
||||||
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
|
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
||||||
|
|
||||||
React.useEffect(() => {
|
const selectionChanged = React.useCallback((action: ActionTraceEventInContext) => {
|
||||||
window.addEventListener('message', async ({ data: { method, params } }) => {
|
if (!testServerConnection || !action?.stack || action.stack.length === 0)
|
||||||
if (method === 'loadTraceRequested') {
|
return;
|
||||||
setTraceURLs(params.traceUrl ? [params.traceUrl] : []);
|
const [{ file, line, column }] = action.stack;
|
||||||
setProcessingErrorMessage(null);
|
testServerConnection.dispatchTraceViewerEventNoReply({ method: 'openSourceLocation', params: { file, line, column } });
|
||||||
} else if (method === 'applyTheme') {
|
}, [testServerConnection]);
|
||||||
if (currentTheme() !== params.theme)
|
|
||||||
toggleTheme();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// notify vscode that it is now listening to its messages
|
|
||||||
window.parent!.postMessage({ type: 'loaded' }, '*');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -66,10 +52,8 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('trace', url);
|
params.set('trace', url);
|
||||||
const response = await fetch(`contexts?${params.toString()}`);
|
const response = await fetch(`contexts?${params.toString()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok)
|
||||||
setProcessingErrorMessage((await response.json()).error);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
contextEntries.push(...(await response.json()));
|
contextEntries.push(...(await response.json()));
|
||||||
}
|
}
|
||||||
navigator.serviceWorker.removeEventListener('message', swListener);
|
navigator.serviceWorker.removeEventListener('message', swListener);
|
||||||
|
|
@ -83,15 +67,31 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
|
||||||
}, [traceURLs]);
|
}, [traceURLs]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (processingErrorMessage)
|
const guid = new URLSearchParams(window.location.search).get('ws');
|
||||||
window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*');
|
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||||
}, [processingErrorMessage]);
|
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 <div className='vbox workbench-loader'>
|
return <div className='vbox ide-mode'>
|
||||||
|
<div className='hbox header'>
|
||||||
|
<div className='logo'>
|
||||||
|
<img src='playwright-logo.svg' alt='Playwright logo' />
|
||||||
|
</div>
|
||||||
|
<div className='product'>Playwright</div>
|
||||||
|
{model.title && <div className='title'>{model.title}</div>}
|
||||||
|
<div className='spacer'></div>
|
||||||
|
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||||
|
</div>
|
||||||
<div className='progress'>
|
<div className='progress'>
|
||||||
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||||
</div>
|
</div>
|
||||||
<Workbench model={model} openPage={openPage} onOpenExternally={openSourceLocation} showSettings />
|
<Workbench model={model} onSelectionChanged={selectionChanged} showSettings />
|
||||||
{!traceURLs.length && <div className='empty-state'>
|
{!traceURLs.length && <div className='empty-state'>
|
||||||
<div className='title'>Select test to see the trace</div>
|
<div className='title'>Select test to see the trace</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
@ -44,7 +44,7 @@ export default defineConfig({
|
||||||
input: {
|
input: {
|
||||||
index: path.resolve(__dirname, 'index.html'),
|
index: path.resolve(__dirname, 'index.html'),
|
||||||
uiMode: path.resolve(__dirname, 'uiMode.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'),
|
recorder: path.resolve(__dirname, 'recorder.html'),
|
||||||
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue