diff --git a/packages/trace-viewer/embedded.html b/packages/trace-viewer/embedded.html
new file mode 100644
index 0000000000..7d0fd2f175
--- /dev/null
+++ b/packages/trace-viewer/embedded.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Playwright Trace Viewer for VS Code
+
+
+
+
+
+
diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/embedded.tsx
new file mode 100644
index 0000000000..7f64d10bd3
--- /dev/null
+++ b/packages/trace-viewer/src/embedded.tsx
@@ -0,0 +1,69 @@
+/**
+ * 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';
+import { setPopoutFunction } from './ui/popout';
+
+(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 (window.location.href.includes('isUnderTest=true'))
+ await new Promise(f => setTimeout(f, 1000));
+ 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(f => {
+ navigator.serviceWorker.oncontrollerchange = () => f();
+ });
+ }
+
+ // Keep SW running.
+ setInterval(function() { fetch('ping'); }, 10000);
+ }
+
+ setPopoutFunction((url: string, target?: string) => {
+ if (url)
+ window.parent?.postMessage({ command: 'openExternal', params: { url, target } }, '*');
+ });
+
+ ReactDOM.render(, document.querySelector('#root'));
+})();
diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
new file mode 100644
index 0000000000..2274355322
--- /dev/null
+++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
@@ -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;
+}
diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
new file mode 100644
index 0000000000..fcb355b566
--- /dev/null
+++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
@@ -0,0 +1,91 @@
+/*
+ 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';
+
+export const EmbeddedWorkbenchLoader: 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);
+
+ 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
+
+
+ {!traceURLs.length &&
+
Select test to see the trace
+
}
+
;
+};
+
+export const emptyModel = new MultiTraceModel([]);
diff --git a/packages/trace-viewer/src/ui/popout.ts b/packages/trace-viewer/src/ui/popout.ts
new file mode 100644
index 0000000000..e14ae635ec
--- /dev/null
+++ b/packages/trace-viewer/src/ui/popout.ts
@@ -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.
+ */
+
+type PopoutFunction = (url: string, target?: string) => Window | any;
+
+let popoutFn: PopoutFunction = window.open;
+export function setPopoutFunction(fn: PopoutFunction) {
+ popoutFn = fn;
+}
+
+export function popout(url: string, target?: string): Window | undefined {
+ const win = popoutFn(url, target);
+ return win instanceof Window ? win : undefined;
+}
diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx
index bdb955c13c..7823d929cb 100644
--- a/packages/trace-viewer/src/ui/snapshotTab.tsx
+++ b/packages/trace-viewer/src/ui/snapshotTab.tsx
@@ -29,6 +29,7 @@ import type { Language } from '@isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import { TabbedPaneTab } from '@web/components/tabbedPane';
import { BrowserFrame } from './browserFrame';
+import { popout } from './popout';
export const SnapshotTab: React.FunctionComponent<{
action: ActionTraceEvent | undefined,
@@ -190,7 +191,7 @@ export const SnapshotTab: React.FunctionComponent<{
})}
{
- const win = window.open(popoutUrl || '', '_blank');
+ const win = popout(popoutUrl || '', '_blank');
win?.addEventListener('DOMContentLoaded', () => {
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
new ConsoleAPI(injectedScript);
diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts
index 2e5178132d..a051466a94 100644
--- a/packages/trace-viewer/vite.config.ts
+++ b/packages/trace-viewer/vite.config.ts
@@ -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: {