diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index 3ee6fd6c8d..2395eb58be 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -36,6 +36,7 @@ class UIMode { globalCleanup: (() => Promise) | undefined; private _watcher: FSWatcher | undefined; private _watchTestFile: string | undefined; + private _originalStderr: (buffer: string | Uint8Array) => void; constructor(config: FullConfigInternal) { this._config = config; @@ -44,6 +45,15 @@ class UIMode { p.retries = 0; config._internal.configCLIOverrides.use = config._internal.configCLIOverrides.use || {}; config._internal.configCLIOverrides.use.trace = 'on'; + this._originalStderr = process.stderr.write.bind(process.stderr); + process.stdout.write = (chunk: string | Buffer) => { + this._dispatchEvent({ method: 'stdio', params: chunkToPayload('stdout', chunk) }); + return true; + }; + process.stderr.write = (chunk: string | Buffer) => { + this._dispatchEvent({ method: 'stdio', params: chunkToPayload('stderr', chunk) }); + return true; + }; } async runGlobalSetup(): Promise { @@ -78,6 +88,12 @@ class UIMode { this._stopTests(); if (method === 'watch') this._watchFile(params.fileName); + if (method === 'resizeTerminal') { + process.stdout.columns = params.cols; + process.stdout.rows = params.rows; + process.stderr.columns = params.cols; + process.stderr.columns = params.rows; + } if (method === 'exit') exitPromise.resolve(); }); @@ -86,7 +102,7 @@ class UIMode { private _dispatchEvent(message: any) { // eslint-disable-next-line no-console - this._page.mainFrame().evaluateExpression(dispatchFuncSource, true, message).catch(e => console.log(e)); + this._page.mainFrame().evaluateExpression(dispatchFuncSource, true, message).catch(e => this._originalStderr(String(e))); } private async _listTests() { @@ -156,3 +172,15 @@ export async function runUIMode(config: FullConfigInternal): Promise void = () => {}; let updateStepsProgress: () => void = () => {}; let runWatchedTests = () => {}; let runVisibleTests = () => {}; +const xtermDataSource: XtermDataSource = { + pending: [], + clear: () => {}, + write: data => xtermDataSource.pending.push(data), + resize: (cols: number, rows: number) => sendMessageNoReply('resizeTerminal', { cols, rows }), +}; + export const WatchModeView: React.FC<{}> = ({ }) => { const [projectNames, setProjectNames] = React.useState([]); @@ -64,54 +73,31 @@ export const WatchModeView: React.FC<{}> = ({ setProjectNames([rootSuite.value?.suites[0].title]); }, [projectNames, rootSuite]); - return - -
- -
setSettingsVisible(false)}>Tests
- - sendMessageNoReply('stop')} disabled={!isRunningTest}> - -
- { setSettingsVisible(!settingsVisible); }}> -
- { !settingsVisible && } - { settingsVisible &&
-
-
Projects
+ return
+ + +
+ +
setSettingsVisible(false)}>Tests
+ + sendMessageNoReply('stop')} disabled={!isRunningTest}> +
- setSettingsVisible(false)}> -
- {(rootSuite.value?.suites || []).map(suite => { - return
- { - const copy = [...projectNames]; - if (copy.includes(suite.title)) - copy.splice(copy.indexOf(suite.title), 1); - else - copy.push(suite.title); - setProjectNames(copy); - }} style={{ margin: '0 5px 0 10px' }} /> - -
; - })} -
Appearance
-
- toggleTheme()}>Toggle color mode -
-
} - {isRunningTest &&
+ { setSettingsVisible(!settingsVisible); }}> + + { !settingsVisible && } + {settingsVisible && setSettingsVisible(false)}>} +
+ +
Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed -
}
- ; +
; }; export const TestList: React.FC<{ @@ -243,7 +229,40 @@ export const TestList: React.FC<{ expandedItems.set(treeItem.id, true); setExpandedItems(new Map(expandedItems)); }} - noItemsMessage='No tests' />; + noItemsMessage='No tests' /> +
; +}; + +export const SettingsView: React.FC<{ + projectNames: string[], + setProjectNames: (projectNames: string[]) => void, + onClose: () => void, +}> = ({ projectNames, setProjectNames, onClose }) => { + return
+
+
Projects
+
+ +
+ {projectNames.map(projectName => { + return
+ { + const copy = [...projectNames]; + if (copy.includes(projectName)) + copy.splice(copy.indexOf(projectName), 1); + else + copy.push(projectName); + setProjectNames(copy); + }} style={{ margin: '0 5px 0 10px' }} /> + +
; + })} +
Appearance
+
+ toggleTheme()}>Toggle color mode +
; }; @@ -274,7 +293,10 @@ export const TraceView: React.FC<{ })(); }, [testItem, stepsProgress]); - return ; + const xterm = ; + return xtermDataSource.clear()}>, + ]}/>; }; declare global { @@ -325,10 +347,18 @@ const resetCollectingRootSuite = () => { }; (window as any).dispatch = (message: any) => { - if (message.method === 'fileChanged') + if (message.method === 'fileChanged') { runWatchedTests(); - else + } else if (message.method === 'stdio') { + if (message.params.buffer) { + const data = atob(message.params.buffer); + xtermDataSource.write(data); + } else { + xtermDataSource.write(message.params.text); + } + } else { receiver?.dispatch(message); + } }; const sendMessage = async (method: string, params: any) => { diff --git a/packages/trace-viewer/src/ui/workbench.css b/packages/trace-viewer/src/ui/workbench.css index 384dd26564..38d2289e52 100644 --- a/packages/trace-viewer/src/ui/workbench.css +++ b/packages/trace-viewer/src/ui/workbench.css @@ -103,7 +103,3 @@ .workbench .header .title { margin-left: 16px; } - -.workbench .spacer { - flex: auto; -} diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index aaa60c360c..7c906abe6f 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -26,17 +26,20 @@ import { NetworkTab } from './networkTab'; import { SnapshotTab } from './snapshotTab'; import { SourceTab } from './sourceTab'; import { TabbedPane } from '@web/components/tabbedPane'; +import type { TabbedPaneTabModel } from '@web/components/tabbedPane'; import { Timeline } from './timeline'; import './workbench.css'; import { MetadataView } from './metadataView'; export const Workbench: React.FunctionComponent<{ model?: MultiTraceModel, -}> = ({ model }) => { + output?: React.ReactElement, + rightToolbar?: React.ReactElement[], +}> = ({ model, output, rightToolbar }) => { const [selectedAction, setSelectedAction] = React.useState(); const [highlightedAction, setHighlightedAction] = React.useState(); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState('actions'); - const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState('logs'); + const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState(output ? 'output' : 'call'); const activeAction = model ? highlightedAction || selectedAction : undefined; const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 }; @@ -44,14 +47,15 @@ export const Workbench: React.FunctionComponent<{ const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0; const sdkLanguage = model?.sdkLanguage || 'javascript'; - const tabs = [ - { id: 'logs', title: 'Call', count: 0, render: () => }, + const tabs: TabbedPaneTabModel[] = [ + { id: 'call', title: 'Call', render: () => }, { id: 'console', title: 'Console', count: consoleCount, render: () => }, { id: 'network', title: 'Network', count: networkCount, render: () => }, + { id: 'source', title: 'Source', count: 0, render: () => }, ]; - if (model?.hasSource) - tabs.push({ id: 'source', title: 'Source', count: 0, render: () => }); + if (output) + tabs.unshift({ id: 'output', title: 'Output', component: output }); return
setSelectedAction(action)} /> - - + + - + { + setSelectedAction(action); + }} + onHighlighted={action => { + setHighlightedAction(action); + }} + revealConsole={() => setSelectedPropertiesTab('console')} + /> + }, + { + id: 'metadata', + title: 'Metadata', + count: 0, + component: + }, + ] + } selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/> - { - setSelectedAction(action); - }} - onHighlighted={action => { - setHighlightedAction(action); - }} - revealConsole={() => setSelectedPropertiesTab('console')} - /> - }, - { - id: 'metadata', - title: 'Metadata', - count: 0, - component: - }, - ] - } selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/> +
; }; diff --git a/packages/web/src/common.css b/packages/web/src/common.css index d15a5350de..83718ccc49 100644 --- a/packages/web/src/common.css +++ b/packages/web/src/common.css @@ -97,6 +97,10 @@ svg { position: relative; } +.spacer { + flex: auto; +} + .codicon-check { color: var(--green); } diff --git a/packages/web/src/components/listView.tsx b/packages/web/src/components/listView.tsx index 045b0cd740..5b809e1147 100644 --- a/packages/web/src/components/listView.tsx +++ b/packages/web/src/components/listView.tsx @@ -161,7 +161,11 @@ const ListItemView: React.FC<{ ref={divRef} > {indent ?
: undefined} - {hasIcons &&
} + {hasIcons &&
{ + e.stopPropagation(); + e.preventDefault(); + onIconClicked(); + }}>
} {typeof children === 'string' ?
{children}
: children} ; }; diff --git a/packages/web/src/components/tabbedPane.tsx b/packages/web/src/components/tabbedPane.tsx index 7e8d89239f..a6acc36977 100644 --- a/packages/web/src/components/tabbedPane.tsx +++ b/packages/web/src/components/tabbedPane.tsx @@ -45,6 +45,7 @@ export const TabbedPane: React.FunctionComponent<{ selected={selectedTab === tab.id} onSelect={setSelectedTab} >)), +
, ...rightToolbar || [], ]} { diff --git a/packages/web/src/components/toolbar.css b/packages/web/src/components/toolbar.css index 816c9a9f9f..9269634706 100644 --- a/packages/web/src/components/toolbar.css +++ b/packages/web/src/components/toolbar.css @@ -19,10 +19,11 @@ box-shadow: var(--box-shadow); background-color: var(--vscode-sideBar-background); color: var(--vscode-sideBarTitle-foreground); - min-height: 32px; + min-height: 35px; align-items: center; flex: none; z-index: 2; + margin: 0 5px; } .toolbar-linewrap { @@ -31,7 +32,7 @@ } .toolbar input { - padding: 0 10px; + padding: 0 5px; line-height: 24px; outline: none; margin: 0 4px; diff --git a/packages/web/src/components/xtermWrapper.tsx b/packages/web/src/components/xtermWrapper.tsx index 3618dfa88a..7447c64e18 100644 --- a/packages/web/src/components/xtermWrapper.tsx +++ b/packages/web/src/components/xtermWrapper.tsx @@ -20,13 +20,14 @@ import type { Terminal } from 'xterm'; import type { XtermModule } from './xtermModule'; import { isDarkTheme } from '@web/theme'; -export type XTermDataSource = { +export type XtermDataSource = { pending: (string | Uint8Array)[]; + clear: () => void, write: (data: string | Uint8Array) => void; resize: (cols: number, rows: number) => void; }; -export const XTermWrapper: React.FC<{ source: XTermDataSource }> = ({ +export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({ source }) => { const xtermElement = React.createRef(); @@ -55,8 +56,13 @@ export const XTermWrapper: React.FC<{ source: XTermDataSource }> = ({ for (const p of source.pending) newTerminal.write(p); source.write = (data => { + source.pending.push(data); newTerminal.write(data); }); + source.clear = () => { + source.pending = []; + newTerminal.clear(); + }; newTerminal.open(element); fitAddon.fit(); setTerminal(newTerminal);