mirror of
https://github.com/microsoft/playwright.git
synced 2024-08-17 16:10:29 +03:00
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,
|
||||
highlightedLocator: string,
|
||||
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 [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
||||
|
||||
@ -190,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
})}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<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', () => {
|
||||
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
||||
new ConsoleAPI(injectedScript);
|
||||
|
@ -51,7 +51,8 @@ export const Workbench: React.FunctionComponent<{
|
||||
isLive?: boolean,
|
||||
status?: UITestStatus,
|
||||
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 [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||
@ -234,7 +235,8 @@ export const Workbench: React.FunctionComponent<{
|
||||
isInspecting={isInspecting}
|
||||
setIsInspecting={setIsInspecting}
|
||||
highlightedLocator={highlightedLocator}
|
||||
setHighlightedLocator={locatorPicked} />
|
||||
setHighlightedLocator={locatorPicked}
|
||||
openPage={openPage} />
|
||||
<TabbedPane
|
||||
tabs={[
|
||||
{
|
||||
|
@ -45,6 +45,7 @@ export default defineConfig({
|
||||
input: {
|
||||
index: path.resolve(__dirname, 'index.html'),
|
||||
uiMode: path.resolve(__dirname, 'uiMode.html'),
|
||||
embedded: path.resolve(__dirname, 'embedded.html'),
|
||||
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
||||
},
|
||||
output: {
|
||||
|
Loading…
Reference in New Issue
Block a user