From 0106a54e6ea65c94e7a2788c7e21a9b406d474d2 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 9 Mar 2023 20:02:42 -0800 Subject: [PATCH] chore: ui mode fixes (#21546) For https://github.com/microsoft/playwright/issues/21541 --- packages/trace-viewer/src/ui/callTab.css | 8 +- packages/trace-viewer/src/ui/callTab.tsx | 41 +------ packages/trace-viewer/src/ui/errorMessage.css | 24 ++++ packages/trace-viewer/src/ui/errorMessage.tsx | 56 ++++++++++ packages/trace-viewer/src/ui/watchMode.tsx | 104 +++++++++++------- .../web/src/components/codeMirrorWrapper.css | 4 + packages/web/src/components/listView.css | 1 + packages/web/src/components/listView.tsx | 18 ++- packages/web/src/components/xtermWrapper.css | 4 + 9 files changed, 171 insertions(+), 89 deletions(-) create mode 100644 packages/trace-viewer/src/ui/errorMessage.css create mode 100644 packages/trace-viewer/src/ui/errorMessage.tsx diff --git a/packages/trace-viewer/src/ui/callTab.css b/packages/trace-viewer/src/ui/callTab.css index 032c97b165..522a1fae87 100644 --- a/packages/trace-viewer/src/ui/callTab.css +++ b/packages/trace-viewer/src/ui/callTab.css @@ -94,12 +94,6 @@ color: var(--blue); } -.call-error-message { - font-family: var(--vscode-editor-font-family); - font-weight: var(--vscode-editor-font-weight); - font-size: var(--vscode-editor-font-size); - background-color: var(--vscode-inputValidation-errorBackground); - white-space: pre; - overflow: auto; +.call-tab .error-message { padding: 5px; } diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx index 92ac679fe4..d5a0171127 100644 --- a/packages/trace-viewer/src/ui/callTab.tsx +++ b/packages/trace-viewer/src/ui/callTab.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import ansi2html from 'ansi-to-html'; import type { SerializedValue } from '@protocol/channels'; import type { ActionTraceEvent } from '@trace/trace'; import { msToString } from '@web/uiUtils'; @@ -23,6 +22,7 @@ import './callTab.css'; import { CopyToClipboard } from './copyToClipboard'; import { asLocator } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators'; +import { ErrorMessage } from './errorMessage'; export const CallTab: React.FunctionComponent<{ action: ActionTraceEvent | undefined, @@ -39,7 +39,7 @@ export const CallTab: React.FunctionComponent<{ const wallTime = action.wallTime ? new Date(action.wallTime).toLocaleString() : null; const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out'; return
- {!!error && } + {!!error && } {!!error &&
Call
}
{action.apiName}
{<> @@ -144,40 +144,3 @@ function parseSerializedValue(value: SerializedValue, handles: any[] | undefined } return ''; } - -const ErrorMessage: React.FC<{ - error: string; -}> = ({ error }) => { - const html = React.useMemo(() => { - const config: any = { - bg: 'var(--vscode-panel-background)', - fg: 'var(--vscode-foreground)', - }; - config.colors = ansiColors; - return new ansi2html(config).toHtml(escapeHTML(error)); - }, [error]); - return
; -}; - -const ansiColors = { - 0: '#000', - 1: '#C00', - 2: '#0C0', - 3: '#C50', - 4: '#00C', - 5: '#C0C', - 6: '#0CC', - 7: '#CCC', - 8: '#555', - 9: '#F55', - 10: '#5F5', - 11: '#FF5', - 12: '#55F', - 13: '#F5F', - 14: '#5FF', - 15: '#FFF' -}; - -function escapeHTML(text: string): string { - return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); -} diff --git a/packages/trace-viewer/src/ui/errorMessage.css b/packages/trace-viewer/src/ui/errorMessage.css new file mode 100644 index 0000000000..d50af176a2 --- /dev/null +++ b/packages/trace-viewer/src/ui/errorMessage.css @@ -0,0 +1,24 @@ +/* + 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. +*/ + +.error-message { + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); + font-size: var(--vscode-editor-font-size); + background-color: var(--vscode-inputValidation-errorBackground); + white-space: pre; + overflow: auto; +} diff --git a/packages/trace-viewer/src/ui/errorMessage.tsx b/packages/trace-viewer/src/ui/errorMessage.tsx new file mode 100644 index 0000000000..fca090dbfc --- /dev/null +++ b/packages/trace-viewer/src/ui/errorMessage.tsx @@ -0,0 +1,56 @@ +/** + * 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 ansi2html from 'ansi-to-html'; +import * as React from 'react'; +import './errorMessage.css'; + +export const ErrorMessage: React.FC<{ + error: string; +}> = ({ error }) => { + const html = React.useMemo(() => { + const config: any = { + bg: 'var(--vscode-panel-background)', + fg: 'var(--vscode-foreground)', + }; + config.colors = ansiColors; + return new ansi2html(config).toHtml(escapeHTML(error)); + }, [error]); + return
; +}; + +const ansiColors = { + 0: '#000', + 1: '#C00', + 2: '#0C0', + 3: '#C50', + 4: '#00C', + 5: '#C0C', + 6: '#0CC', + 7: '#CCC', + 8: '#555', + 9: '#F55', + 10: '#5F5', + 11: '#FF5', + 12: '#55F', + 13: '#F5F', + 14: '#5FF', + 15: '#FFF' +}; + +function escapeHTML(text: string): string { + return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); +} diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index eac4073bc9..590e66da75 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -37,12 +37,16 @@ let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {}; let updateStepsProgress: () => void = () => {}; let runWatchedTests = () => {}; let runVisibleTests = () => {}; +let xtermSize = { cols: 80, rows: 24 }; const xtermDataSource: XtermDataSource = { pending: [], clear: () => {}, write: data => xtermDataSource.pending.push(data), - resize: (cols: number, rows: number) => sendMessageNoReply('resizeTerminal', { cols, rows }), + resize: (cols: number, rows: number) => { + xtermSize = { cols, rows }; + sendMessageNoReply('resizeTerminal', { cols, rows }); + }, }; export const WatchModeView: React.FC<{}> = ({ @@ -76,6 +80,18 @@ export const WatchModeView: React.FC<{}> = ({ }; const runTests = (testIds: string[]) => { + // Clear test results. + { + const testIdSet = new Set(testIds); + for (const test of rootSuite.value?.allTests() || []) { + if (testIdSet.has(test.id)) + test.results = []; + } + setRootSuite({ ...rootSuite }); + } + + const time = ' [' + new Date().toLocaleTimeString() + ']'; + xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m'); setProgress({ total: testIds.length, passed: 0, failed: 0 }); setIsRunningTest(true); sendMessage('run', { testIds }).then(() => { @@ -83,13 +99,14 @@ export const WatchModeView: React.FC<{}> = ({ }); }; + const result = selectedTest?.results[0]; return
- + {(result && result.duration >= 0) ? : }
setSettingsVisible(false)}>Tests
- + runVisibleTests()} disabled={isRunningTest}> sendMessageNoReply('stop')} disabled={!isRunningTest}> refreshRootSuite(true)} disabled={isRunningTest}> setIsWatchingFiles(!isWatchingFiles)}> @@ -108,7 +125,7 @@ export const WatchModeView: React.FC<{}> = ({
- Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed + Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed
; }; @@ -134,23 +151,22 @@ export const TestList: React.FC<{ refreshRootSuite(true); }, []); - const { rootItem, treeItemMap, visibleTestIds } = React.useMemo(() => { + const { rootItem, treeItemMap } = React.useMemo(() => { const rootItem = createTree(rootSuite.value, projects); filterTree(rootItem, filterText); + hideOnlyTests(rootItem); const treeItemMap = new Map(); const visibleTestIds = new Set(); const visit = (treeItem: TreeItem) => { - if (treeItem.kind === 'test') - visibleTestIds.add(treeItem.id); - treeItem.children?.forEach(visit); + if (treeItem.kind === 'case') + treeItem.tests.forEach(t => visibleTestIds.add(t.id)); + treeItem.children.forEach(visit); treeItemMap.set(treeItem.id, treeItem); }; visit(rootItem); - hideOnlyTests(rootItem); - return { rootItem, treeItemMap, visibleTestIds }; - }, [filterText, rootSuite, projects]); - - runVisibleTests = () => runTests([...visibleTestIds]); + runVisibleTests = () => runTests([...visibleTestIds]); + return { rootItem, treeItemMap }; + }, [filterText, rootSuite, projects, runTests]); const { selectedTreeItem } = React.useMemo(() => { const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined; @@ -168,7 +184,6 @@ export const TestList: React.FC<{ }, [selectedTreeItem, isWatchingFiles]); const runTreeItem = (treeItem: TreeItem) => { - // expandedItems.set(treeItem.id, true); setSelectedTreeItemId(treeItem.id); runTests(collectTestIds(treeItem)); }; @@ -209,6 +224,8 @@ export const TestList: React.FC<{ return 'codicon-error'; if (treeItem.status === 'passed') return 'codicon-check'; + if (treeItem.status === 'skipped') + return 'codicon-circle-slash'; return 'codicon-circle-outline'; }} selectedItem={selectedTreeItem} @@ -252,33 +269,38 @@ export const SettingsView: React.FC<{ ; }; -export const TraceView: React.FC<{ - test: TestCase | undefined, -}> = ({ test }) => { +export const InProgressTraceView: React.FC<{ + testResult: TestResult | undefined, +}> = ({ testResult }) => { const [model, setModel] = React.useState(); const [stepsProgress, setStepsProgress] = React.useState(0); updateStepsProgress = () => setStepsProgress(stepsProgress + 1); React.useEffect(() => { - (async () => { - if (!test) { - setModel(undefined); - return; - } + setModel(testResult ? stepsToModel(testResult) : undefined); + }, [stepsProgress, testResult]); - const result = test.results?.[0]; - if (result) { - const attachment = result.attachments.find(a => a.name === 'trace'); - if (attachment && attachment.path) - loadSingleTraceFile(attachment.path).then(setModel); - else - setModel(stepsToModel(result)); - } else { - setModel(undefined); - } - })(); - }, [test, stepsProgress]); + return ; +}; +export const FinishedTraceView: React.FC<{ + testResult: TestResult, +}> = ({ testResult }) => { + const [model, setModel] = React.useState(); + + React.useEffect(() => { + // Test finished. + const attachment = testResult.attachments.find(a => a.name === 'trace'); + if (attachment && attachment.path) + loadSingleTraceFile(attachment.path).then(setModel); + }, [testResult]); + + return ; +}; + +export const TraceView: React.FC<{ + model: MultiTraceModel | undefined, +}> = ({ model }) => { const xterm = ; return xtermDataSource.clear()}>, @@ -412,7 +434,7 @@ type TreeItemBase = { title: string; location: Location, children: TreeItem[]; - status: 'none' | 'running' | 'passed' | 'failed'; + status: 'none' | 'running' | 'passed' | 'failed' | 'skipped'; }; type GroupItem = TreeItemBase & { @@ -476,12 +498,14 @@ function createTree(rootSuite: Suite | undefined, projects: Map parentGroup.children.push(testCaseItem); } - let status: 'none' | 'running' | 'passed' | 'failed' = 'none'; + let status: 'none' | 'running' | 'passed' | 'failed' | 'skipped' = 'none'; if (test.results.some(r => r.duration === -1)) status = 'running'; + else if (test.results.length && test.outcome() === 'skipped') + status = 'skipped'; else if (test.results.length && test.outcome() !== 'expected') status = 'failed'; - else if (test.outcome() === 'expected') + else if (test.results.length && test.outcome() === 'expected') status = 'passed'; testCaseItem.tests.push(test); @@ -508,11 +532,13 @@ function createTree(rootSuite: Suite | undefined, projects: Map propagateStatus(child); let allPassed = treeItem.children.length > 0; + let allSkipped = treeItem.children.length > 0; let hasFailed = false; let hasRunning = false; for (const child of treeItem.children) { - allPassed = allPassed && child.status === 'passed'; + allSkipped = allSkipped && child.status === 'skipped'; + allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped'); hasFailed = hasFailed || child.status === 'failed'; hasRunning = hasRunning || child.status === 'running'; } @@ -521,6 +547,8 @@ function createTree(rootSuite: Suite | undefined, projects: Map treeItem.status = 'running'; else if (hasFailed) treeItem.status = 'failed'; + else if (allSkipped) + treeItem.status = 'skipped'; else if (allPassed) treeItem.status = 'passed'; }; diff --git a/packages/web/src/components/codeMirrorWrapper.css b/packages/web/src/components/codeMirrorWrapper.css index eb3c180760..f209c1260d 100644 --- a/packages/web/src/components/codeMirrorWrapper.css +++ b/packages/web/src/components/codeMirrorWrapper.css @@ -117,6 +117,10 @@ body.dark-mode .CodeMirror span.cm-type { } .CodeMirror .CodeMirror-gutters { + z-index: 0; +} + +.CodeMirror .CodeMirror-gutterwrapper { background: var(--vscode-editor-background); border-right: 1px solid var(--vscode-editorGroup-border); color: var(--vscode-editorLineNumber-foreground); diff --git a/packages/web/src/components/listView.css b/packages/web/src/components/listView.css index 5fb533627f..60b789bb78 100644 --- a/packages/web/src/components/listView.css +++ b/packages/web/src/components/listView.css @@ -54,6 +54,7 @@ .list-view-content:focus .list-view-entry.selected * { color: var(--vscode-list-activeSelectionForeground) !important; + background-color: transparent !important; } .list-view-content:focus .list-view-entry.error.selected { diff --git a/packages/web/src/components/listView.tsx b/packages/web/src/components/listView.tsx index fcb1333f45..035ff44322 100644 --- a/packages/web/src/components/listView.tsx +++ b/packages/web/src/components/listView.tsx @@ -121,11 +121,19 @@ export function ListView({ onMouseLeave={() => setHighlightedItem(undefined)} > {indentation ?
: undefined} - {icon &&
{ - e.stopPropagation(); - e.preventDefault(); - onIconClicked?.(item); - }}>
} + {icon &&
{ + e.preventDefault(); + e.stopPropagation(); + }} + onClick={e => { + e.stopPropagation(); + e.preventDefault(); + onIconClicked?.(item); + }} + >
} {typeof rendered === 'string' ?
{rendered}
: rendered} ; })} diff --git a/packages/web/src/components/xtermWrapper.css b/packages/web/src/components/xtermWrapper.css index 895ccaaafd..d51932ca22 100644 --- a/packages/web/src/components/xtermWrapper.css +++ b/packages/web/src/components/xtermWrapper.css @@ -16,6 +16,10 @@ @import '../third_party/vscode/colors.css'; +.xterm-wrapper { + padding-left: 5px; +} + .xterm-wrapper .xterm-viewport { background-color: var(--vscode-panel-background) !important; color: var(--vscode-foreground) !important;