feature(trace-viewer): embedded mode support PoC (#30885)
Companion PR of https://github.com/microsoft/playwright-vscode/pull/483
This commit is contained in:
parent
f1b04aaaf4
commit
9bc45ea2fc
27
packages/trace-viewer/embedded.html
Normal file
27
packages/trace-viewer/embedded.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Playwright Trace Viewer for VS Code</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/embedded.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
packages/trace-viewer/src/embedded.tsx
Normal file
61
packages/trace-viewer/src/embedded.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '@web/common.css';
|
||||||
|
import { applyTheme } from '@web/theme';
|
||||||
|
import '@web/third_party/vscode/codicon.css';
|
||||||
|
import React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
|
||||||
|
|
||||||
|
(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.`);
|
||||||
|
navigator.serviceWorker.register('sw.bundle.js');
|
||||||
|
if (!navigator.serviceWorker.controller) {
|
||||||
|
await new Promise<void>(f => {
|
||||||
|
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep SW running.
|
||||||
|
setInterval(function() { fetch('ping'); }, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render(<EmbeddedWorkbenchLoader />, document.querySelector('#root'));
|
||||||
|
})();
|
||||||
68
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
Normal file
68
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--vscode-editor-background);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .empty-state {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .empty-state {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-progress {
|
||||||
|
background-color: var(--vscode-progressBar-background);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-loader {
|
||||||
|
contain: size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limit to a reasonable minimum viewport */
|
||||||
|
html, body {
|
||||||
|
min-width: 550px;
|
||||||
|
min-height: 450px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
96
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
Normal file
96
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import type { ContextEntry } from '../entries';
|
||||||
|
import { MultiTraceModel } from './modelUtil';
|
||||||
|
import './embeddedWorkbenchLoader.css';
|
||||||
|
import { Workbench } from './workbench';
|
||||||
|
import { currentTheme, toggleTheme } from '@web/theme';
|
||||||
|
|
||||||
|
function openPage(url: string, target?: string) {
|
||||||
|
if (url)
|
||||||
|
window.parent!.postMessage({ command: 'openExternal', params: { url, target } }, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmbeddedWorkbenchLoader: 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);
|
||||||
|
|
||||||
|
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' }, '*');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (traceURLs.length) {
|
||||||
|
const swListener = (event: any) => {
|
||||||
|
if (event.data.method === 'progress')
|
||||||
|
setProgress(event.data.params);
|
||||||
|
};
|
||||||
|
navigator.serviceWorker.addEventListener('message', swListener);
|
||||||
|
setProgress({ done: 0, total: 1 });
|
||||||
|
const contextEntries: ContextEntry[] = [];
|
||||||
|
for (let i = 0; i < traceURLs.length; i++) {
|
||||||
|
const url = traceURLs[i];
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('trace', url);
|
||||||
|
const response = await fetch(`contexts?${params.toString()}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
setProcessingErrorMessage((await response.json()).error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contextEntries.push(...(await response.json()));
|
||||||
|
}
|
||||||
|
navigator.serviceWorker.removeEventListener('message', swListener);
|
||||||
|
const model = new MultiTraceModel(contextEntries);
|
||||||
|
setProgress({ done: 0, total: 0 });
|
||||||
|
setModel(model);
|
||||||
|
} else {
|
||||||
|
setModel(emptyModel);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [traceURLs]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (processingErrorMessage)
|
||||||
|
window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*');
|
||||||
|
}, [processingErrorMessage]);
|
||||||
|
|
||||||
|
return <div className='vbox workbench-loader'>
|
||||||
|
<div className='progress'>
|
||||||
|
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||||
|
</div>
|
||||||
|
<Workbench model={model} openPage={openPage} />
|
||||||
|
{!traceURLs.length && <div className='empty-state'>
|
||||||
|
<div className='title'>Select test to see the trace</div>
|
||||||
|
</div>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emptyModel = new MultiTraceModel([]);
|
||||||
|
|
@ -38,7 +38,8 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
setIsInspecting: (isInspecting: boolean) => void,
|
setIsInspecting: (isInspecting: boolean) => void,
|
||||||
highlightedLocator: string,
|
highlightedLocator: string,
|
||||||
setHighlightedLocator: (locator: string) => void,
|
setHighlightedLocator: (locator: string) => void,
|
||||||
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
|
openPage?: (url: string, target?: string) => Window | any,
|
||||||
|
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => {
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
||||||
|
|
||||||
|
|
@ -190,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
})}
|
})}
|
||||||
<div style={{ flex: 'auto' }}></div>
|
<div style={{ flex: 'auto' }}></div>
|
||||||
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!popoutUrl} onClick={() => {
|
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!popoutUrl} onClick={() => {
|
||||||
const win = window.open(popoutUrl || '', '_blank');
|
if (!openPage)
|
||||||
|
openPage = window.open;
|
||||||
|
const win = openPage(popoutUrl || '', '_blank');
|
||||||
win?.addEventListener('DOMContentLoaded', () => {
|
win?.addEventListener('DOMContentLoaded', () => {
|
||||||
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
||||||
new ConsoleAPI(injectedScript);
|
new ConsoleAPI(injectedScript);
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ export const Workbench: React.FunctionComponent<{
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
inert?: boolean,
|
inert?: boolean,
|
||||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert }) => {
|
openPage?: (url: string, target?: string) => Window | any,
|
||||||
|
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => {
|
||||||
const [selectedAction, setSelectedActionImpl] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
const [selectedAction, setSelectedActionImpl] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
||||||
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||||
|
|
@ -234,7 +235,8 @@ export const Workbench: React.FunctionComponent<{
|
||||||
isInspecting={isInspecting}
|
isInspecting={isInspecting}
|
||||||
setIsInspecting={setIsInspecting}
|
setIsInspecting={setIsInspecting}
|
||||||
highlightedLocator={highlightedLocator}
|
highlightedLocator={highlightedLocator}
|
||||||
setHighlightedLocator={locatorPicked} />
|
setHighlightedLocator={locatorPicked}
|
||||||
|
openPage={openPage} />
|
||||||
<TabbedPane
|
<TabbedPane
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,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'),
|
||||||
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue