From ff303c7d8cfd3d79a4f97ecb15693ea94e124367 Mon Sep 17 00:00:00 2001 From: Rui Figueira Date: Wed, 26 Jun 2024 04:18:11 +0100 Subject: [PATCH 1/2] chore(trace-viewer): embedded mode --- packages/trace-viewer/embedded.html | 27 ++++++ packages/trace-viewer/src/embedded.tsx | 69 ++++++++++++++ .../src/ui/embeddedWorkbenchLoader.css | 68 ++++++++++++++ .../src/ui/embeddedWorkbenchLoader.tsx | 91 +++++++++++++++++++ packages/trace-viewer/src/ui/popout.ts | 27 ++++++ packages/trace-viewer/src/ui/snapshotTab.tsx | 3 +- packages/trace-viewer/vite.config.ts | 1 + 7 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 packages/trace-viewer/embedded.html create mode 100644 packages/trace-viewer/src/embedded.tsx create mode 100644 packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css create mode 100644 packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx create mode 100644 packages/trace-viewer/src/ui/popout.ts 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..7f64d10bd3239 --- /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 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..fcb355b5665ff --- /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 0000000000000..e14ae635ec1dc --- /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 bdb955c13cd3b..7823d929cbadf 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 2e5178132d445..a051466a94da7 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: { From 7700f968479fb283b159f01b92954dfeb6854a22 Mon Sep 17 00:00:00 2001 From: Rui Figueira Date: Fri, 28 Jun 2024 01:53:51 +0100 Subject: [PATCH 2/2] chore(trace-viewer): code review --- packages/trace-viewer/src/embedded.tsx | 8 ------ .../src/ui/embeddedWorkbenchLoader.tsx | 9 +++++-- packages/trace-viewer/src/ui/popout.ts | 27 ------------------- packages/trace-viewer/src/ui/snapshotTab.tsx | 8 +++--- packages/trace-viewer/src/ui/workbench.tsx | 6 +++-- 5 files changed, 16 insertions(+), 42 deletions(-) delete mode 100644 packages/trace-viewer/src/ui/popout.ts diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/embedded.tsx index 7f64d10bd3239..8f0a09e560c2d 100644 --- a/packages/trace-viewer/src/embedded.tsx +++ b/packages/trace-viewer/src/embedded.tsx @@ -20,7 +20,6 @@ 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(); @@ -45,8 +44,6 @@ import { setPopoutFunction } from './ui/popout'; 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'); @@ -60,10 +57,5 @@ import { setPopoutFunction } from './ui/popout'; 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.tsx b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx index fcb355b5665ff..17a2211c41644 100644 --- a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx @@ -21,6 +21,11 @@ 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); @@ -38,7 +43,7 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => { } }); // notify vscode that it is now listening to its messages - window.parent?.postMessage({ type: 'loaded' }, '*'); + window.parent!.postMessage({ type: 'loaded' }, '*'); }, []); React.useEffect(() => { @@ -81,7 +86,7 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
- + {!traceURLs.length &&
Select test to see the trace
} diff --git a/packages/trace-viewer/src/ui/popout.ts b/packages/trace-viewer/src/ui/popout.ts deleted file mode 100644 index e14ae635ec1dc..0000000000000 --- a/packages/trace-viewer/src/ui/popout.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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 7823d929cbadf..4bc2abc9ebc76 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -29,7 +29,6 @@ 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, @@ -39,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'); @@ -191,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{ })}
{ - const win = popout(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} />