diff --git a/packages/trace-viewer/embedded.html b/packages/trace-viewer/embedded.html
new file mode 100644
index 0000000000000..7d0fd2f17525e
--- /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 0000000000000..8f0a09e560c2d
--- /dev/null
+++ b/packages/trace-viewer/src/embedded.tsx
@@ -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(f => {
+ navigator.serviceWorker.oncontrollerchange = () => f();
+ });
+ }
+
+ // Keep SW running.
+ setInterval(function() { fetch('ping'); }, 10000);
+ }
+
+ 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 0000000000000..227435532281a
--- /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 0000000000000..17a2211c41644
--- /dev/null
+++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
@@ -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([]);
+ 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/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx
index bdb955c13cd3b..4bc2abc9ebc76 100644
--- a/packages/trace-viewer/src/ui/snapshotTab.tsx
+++ b/packages/trace-viewer/src/ui/snapshotTab.tsx
@@ -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();
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
@@ -190,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{
})}
{
- 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);
diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx
index c50b345b75f22..afc003c6744af 100644
--- a/packages/trace-viewer/src/ui/workbench.tsx
+++ b/packages/trace-viewer/src/ui/workbench.tsx
@@ -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(undefined);
const [revealedStack, setRevealedStack] = React.useState(undefined);
const [highlightedAction, setHighlightedAction] = React.useState();
@@ -234,7 +235,8 @@ export const Workbench: React.FunctionComponent<{
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
- setHighlightedLocator={locatorPicked} />
+ setHighlightedLocator={locatorPicked}
+ openPage={openPage} />