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} />