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('--host <host>', 'Host to start the server on', 'localhost');
|
||||
command.option('--port <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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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<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))
|
||||
return 'restarted';
|
||||
const testServer = new TestServer(configLocation);
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/embedded.tsx"></script>
|
||||
<script type="module" src="/src/ideMode.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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(<EmbeddedWorkbenchLoader />);
|
||||
ReactDOM.createRoot(document.querySelector('#root')!).render(<IDEModeView />);
|
||||
})();
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<string[]>([]);
|
||||
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
|
||||
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(() => {
|
||||
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 <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='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||
</div>
|
||||
<Workbench model={model} openPage={openPage} onOpenExternally={openSourceLocation} showSettings />
|
||||
<Workbench model={model} onSelectionChanged={selectionChanged} showSettings />
|
||||
{!traceURLs.length && <div className='empty-state'>
|
||||
<div className='title'>Select test to see the trace</div>
|
||||
</div>}
|
||||
|
|
@ -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'),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue