mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
chore: ui mode fixes (#21546)
For https://github.com/microsoft/playwright/issues/21541
This commit is contained in:
parent
e737ff83b4
commit
0106a54e6e
@ -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;
|
||||
}
|
||||
|
@ -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 <div className='call-tab'>
|
||||
{!!error && <ErrorMessage error={error}></ErrorMessage>}
|
||||
{!!error && <ErrorMessage error={error} />}
|
||||
{!!error && <div className='call-section'>Call</div>}
|
||||
<div className='call-line'>{action.apiName}</div>
|
||||
{<>
|
||||
@ -144,40 +144,3 @@ function parseSerializedValue(value: SerializedValue, handles: any[] | undefined
|
||||
}
|
||||
return '<object>';
|
||||
}
|
||||
|
||||
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 <div className='call-error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
};
|
||||
|
||||
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]!));
|
||||
}
|
||||
|
24
packages/trace-viewer/src/ui/errorMessage.css
Normal file
24
packages/trace-viewer/src/ui/errorMessage.css
Normal file
@ -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;
|
||||
}
|
56
packages/trace-viewer/src/ui/errorMessage.tsx
Normal file
56
packages/trace-viewer/src/ui/errorMessage.tsx
Normal file
@ -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 <div className='error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
};
|
||||
|
||||
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]!));
|
||||
}
|
@ -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 <div className='vbox'>
|
||||
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
||||
<TraceView test={selectedTest}></TraceView>
|
||||
{(result && result.duration >= 0) ? <FinishedTraceView testResult={result} /> : <InProgressTraceView testResult={result} />}
|
||||
<div className='vbox watch-mode-sidebar'>
|
||||
<Toolbar>
|
||||
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
||||
<ToolbarButton icon='play' title='Run' onClick={runVisibleTests} disabled={isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='play' title='Run' onClick={() => runVisibleTests()} disabled={isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='eye-watch' title='Watch' toggled={isWatchingFiles} onClick={() => setIsWatchingFiles(!isWatchingFiles)}></ToolbarButton>
|
||||
@ -108,7 +125,7 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
</div>
|
||||
</SplitView>
|
||||
<div className='status-line'>
|
||||
Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed
|
||||
Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
@ -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<string, TreeItem>();
|
||||
const visibleTestIds = new Set<string>();
|
||||
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<{
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const TraceView: React.FC<{
|
||||
test: TestCase | undefined,
|
||||
}> = ({ test }) => {
|
||||
export const InProgressTraceView: React.FC<{
|
||||
testResult: TestResult | undefined,
|
||||
}> = ({ testResult }) => {
|
||||
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
||||
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 <TraceView model={model} />;
|
||||
};
|
||||
|
||||
export const FinishedTraceView: React.FC<{
|
||||
testResult: TestResult,
|
||||
}> = ({ testResult }) => {
|
||||
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Test finished.
|
||||
const attachment = testResult.attachments.find(a => a.name === 'trace');
|
||||
if (attachment && attachment.path)
|
||||
loadSingleTraceFile(attachment.path).then(setModel);
|
||||
}, [testResult]);
|
||||
|
||||
return <TraceView model={model} />;
|
||||
};
|
||||
|
||||
export const TraceView: React.FC<{
|
||||
model: MultiTraceModel | undefined,
|
||||
}> = ({ model }) => {
|
||||
const xterm = <XtermWrapper source={xtermDataSource}></XtermWrapper>;
|
||||
return <Workbench model={model} output={xterm} rightToolbar={[
|
||||
<ToolbarButton icon='trash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>,
|
||||
@ -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<string, boolean>
|
||||
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<string, boolean>
|
||||
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<string, boolean>
|
||||
treeItem.status = 'running';
|
||||
else if (hasFailed)
|
||||
treeItem.status = 'failed';
|
||||
else if (allSkipped)
|
||||
treeItem.status = 'skipped';
|
||||
else if (allPassed)
|
||||
treeItem.status = 'passed';
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -121,11 +121,19 @@ export function ListView<T>({
|
||||
onMouseLeave={() => setHighlightedItem(undefined)}
|
||||
>
|
||||
{indentation ? <div style={{ minWidth: indentation * 16 }}></div> : undefined}
|
||||
{icon && <div className={'codicon ' + (icon(item) || 'blank')} style={{ minWidth: 16, marginRight: 4 }} onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onIconClicked?.(item);
|
||||
}}></div>}
|
||||
{icon && <div
|
||||
className={'codicon ' + (icon(item) || 'blank')}
|
||||
style={{ minWidth: 16, marginRight: 4 }}
|
||||
onDoubleClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onIconClicked?.(item);
|
||||
}}
|
||||
></div>}
|
||||
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
||||
</div>;
|
||||
})}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user