/** * 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/third_party/vscode/codicon.css'; import { loadSingleTraceFile, Workbench } from './workbench'; import '@web/common.css'; import React from 'react'; import { ListView } from '@web/components/listView'; import { TeleReporterReceiver } from '../../../playwright-test/src/isomorphic/teleReceiver'; import type { FullConfig, Suite, TestCase, TestStep } from '../../../playwright-test/types/testReporter'; import { SplitView } from '@web/components/splitView'; import type { MultiTraceModel } from './modelUtil'; import './watchMode.css'; import { ToolbarButton } from '@web/components/toolbarButton'; import { Toolbar } from '@web/components/toolbar'; let updateRootSuite: (rootSuite: Suite) => void = () => {}; let updateProgress: () => void = () => {}; let runWatchedTests = () => {}; export const WatchModeView: React.FC<{}> = ({ }) => { const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined }); updateRootSuite = (rootSuite: Suite) => setRootSuite({ value: rootSuite }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); const [isRunningTest, setIsRunningTest] = React.useState(false); const [filterText, setFilterText] = React.useState(''); const [projectNames, setProjectNames] = React.useState([]); const [expandedItems, setExpandedItems] = React.useState>(new Map()); const inputRef = React.useRef(null); React.useEffect(() => { inputRef.current?.focus(); sendMessageNoReply('list'); }, []); React.useEffect(() => { if (projectNames.length === 0 && rootSuite.value?.suites.length) setProjectNames([rootSuite.value?.suites[0].title]); }, [projectNames, rootSuite]); const { filteredItems, treeItemMap, visibleTestIds } = React.useMemo(() => { const treeItems = createTree(rootSuite.value, projectNames); const filteredItems = filterTree(treeItems, filterText); const treeItemMap = new Map(); const visibleTestIds = new Set(); const visit = (treeItem: TreeItem) => { if (treeItem.kind === 'test') visibleTestIds.add(treeItem.id); treeItem.children?.forEach(visit); treeItemMap.set(treeItem.id, treeItem); }; filteredItems.forEach(visit); return { treeItemMap, visibleTestIds, filteredItems }; }, [filterText, rootSuite, projectNames]); const { listItems } = React.useMemo(() => { const listItems = flattenTree(filteredItems, expandedItems, !!filterText.trim()); return { listItems }; }, [filteredItems, filterText, expandedItems]); const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined; React.useEffect(() => { sendMessageNoReply('watch', { fileName: fileName(selectedTreeItem) }); }, [selectedTreeItem, treeItemMap]); const runTreeItem = (treeItem: TreeItem) => { expandedItems.set(treeItem.id, true); setSelectedTreeItemId(treeItem.id); runTests(collectTestIds(treeItem)); }; runWatchedTests = () => { runTests(collectTestIds(selectedTreeItem)); }; const runTests = (testIds: string[] | undefined) => { setIsRunningTest(true); sendMessage('run', { testIds }).then(() => { setIsRunningTest(false); }); }; return
{ setFilterText(e.target.value); }} onKeyDown={e => { if (e.key === 'Enter') runTests([...visibleTestIds]); }}> runTests([...visibleTestIds])} disabled={isRunningTest}> sendMessageNoReply('stop')} disabled={!isRunningTest}> treeItem.id } itemRender={(treeItem: TreeItem) => { return
{treeItem.title}
runTreeItem(treeItem)} disabled={isRunningTest}>
; }} itemIcon={(treeItem: TreeItem) => { if (treeItem.kind === 'case' && treeItem.children?.length === 1) treeItem = treeItem.children[0]; if (treeItem.kind === 'test') { const ok = treeItem.test.outcome() === 'expected'; const failed = treeItem.test.results.length && treeItem.test.outcome() !== 'expected'; const running = treeItem.test.results.some(r => r.duration === -1); if (running) return 'codicon-loading'; if (ok) return 'codicon-check'; if (failed) return 'codicon-error'; } else { return treeItem.expanded ? 'codicon-chevron-down' : 'codicon-chevron-right'; } }} itemIndent={(treeItem: TreeItem) => treeItem.kind === 'file' ? 0 : treeItem.kind === 'case' ? 1 : 2} selectedItem={selectedTreeItem} onAccepted={runTreeItem} onLeftArrow={(treeItem: TreeItem) => { if (treeItem.children && treeItem.expanded) { expandedItems.set(treeItem.id, false); setExpandedItems(new Map(expandedItems)); } else { setSelectedTreeItemId(treeItem.parent?.id); } }} onRightArrow={(treeItem: TreeItem) => { if (treeItem.children) { expandedItems.set(treeItem.id, true); setExpandedItems(new Map(expandedItems)); } setRootSuite({ ...rootSuite }); }} onSelected={(treeItem: TreeItem) => { setSelectedTreeItemId(treeItem.id); }} onIconClicked={(treeItem: TreeItem) => { if (treeItem.kind === 'test') return; if (treeItem.expanded) expandedItems.set(treeItem.id, false); else expandedItems.set(treeItem.id, true); setExpandedItems(new Map(expandedItems)); }} showNoItemsMessage={true}>
{(rootSuite.value?.suites.length || 0) > 1 &&
{ const copy = [...projectNames]; copy.includes(suite.title) ? copy.splice(copy.indexOf(suite.title), 1) : copy.push(suite.title); setProjectNames(copy); }} itemRender={(suite: Suite) => { return ; }} />
}
; }; export const ProgressView: React.FC<{ testItem: TestItem | undefined, }> = ({ testItem, }) => { const [updateCounter, setUpdateCounter] = React.useState(0); updateProgress = () => setUpdateCounter(updateCounter + 1); const steps: (TestCase | TestStep)[] = []; for (const result of testItem?.test.results || []) steps.push(...result.steps); return step.title} itemIcon={(step: TestStep) => step.error ? 'codicon-error' : 'codicon-check'} >; }; export const TraceView: React.FC<{ testItem: TestItem | undefined, isRunningTest: boolean, }> = ({ testItem, isRunningTest }) => { const [model, setModel] = React.useState(); React.useEffect(() => { (async () => { if (!testItem) { setModel(undefined); return; } for (const result of testItem?.test.results || []) { const attachment = result.attachments.find(a => a.name === 'trace'); if (attachment && attachment.path) { setModel(await loadSingleTraceFile(attachment.path)); return; } } setModel(undefined); })(); }, [testItem, isRunningTest]); if (isRunningTest) return ; if (!model) { return
Run test to see the trace
Double click a test or hit Enter
; } return ; }; declare global { interface Window { binding(data: any): Promise; } } { let rootSuite: Suite; const receiver = new TeleReporterReceiver({ onBegin: (config: FullConfig, suite: Suite) => { if (!rootSuite) rootSuite = suite; updateRootSuite(rootSuite); }, onTestBegin: () => { updateRootSuite(rootSuite); }, onTestEnd: () => { updateRootSuite(rootSuite); }, onStepBegin: () => { updateProgress(); }, onStepEnd: () => { updateProgress(); }, }); (window as any).dispatch = (message: any) => { if (message.method === 'fileChanged') runWatchedTests(); else receiver.dispatch(message); }; } const sendMessage = async (method: string, params: any) => { await (window as any).sendMessage({ method, params }); }; const sendMessageNoReply = (method: string, params?: any) => { sendMessage(method, params).catch((e: Error) => { // eslint-disable-next-line no-console console.error(e); }); }; const fileName = (treeItem?: TreeItem): string | undefined => { if (!treeItem) return; if (treeItem.kind === 'file') return treeItem.file; return fileName(treeItem.parent || undefined); }; const collectTestIds = (treeItem?: TreeItem): string[] => { if (!treeItem) return []; const testIds: string[] = []; const visit = (treeItem: TreeItem) => { if (treeItem.kind === 'test') testIds.push(treeItem.id); treeItem.children?.forEach(visit); }; visit(treeItem); return testIds; }; type TreeItemBase = { kind: 'file' | 'case' | 'test', id: string; title: string; parent: TreeItem | null; children?: TreeItem[]; expanded?: boolean; }; type FileItem = TreeItemBase & { kind: 'file', file: string; }; type TestCaseItem = TreeItemBase & { kind: 'case', }; type TestItem = TreeItemBase & { kind: 'test', test: TestCase; }; type TreeItem = FileItem | TestCaseItem | TestItem; function createTree(rootSuite: Suite | undefined, projectNames: string[]): FileItem[] { const fileItems = new Map(); for (const projectSuite of rootSuite?.suites || []) { if (!projectNames.includes(projectSuite.title)) continue; for (const fileSuite of projectSuite.suites) { const file = fileSuite.location!.file; let fileItem = fileItems.get(file); if (!fileItem) { fileItem = { kind: 'file', id: fileSuite.title, title: fileSuite.title, file, parent: null, children: [], expanded: false, }; fileItems.set(fileSuite.location!.file, fileItem); } for (const test of fileSuite.allTests()) { const title = test.titlePath().slice(3).join(' › '); let testCaseItem = fileItem.children!.find(t => t.title === title); if (!testCaseItem) { testCaseItem = { kind: 'case', id: fileItem.id + ' / ' + title, title, parent: fileItem, children: [], expanded: false, }; fileItem.children!.push(testCaseItem); } testCaseItem.children!.push({ kind: 'test', id: test.id, title: projectSuite.title, parent: testCaseItem, test, }); } } } return [...fileItems.values()]; } function filterTree(fileItems: FileItem[], filterText: string): FileItem[] { const trimmedFilterText = filterText.trim(); const filterTokens = trimmedFilterText.toLowerCase().split(' '); const result: FileItem[] = []; for (const fileItem of fileItems) { if (trimmedFilterText) { const filteredCases: TreeItem[] = []; for (const testCaseItem of fileItem.children!) { const fullTitle = (fileItem.title + ' ' + testCaseItem.title).toLowerCase(); if (filterTokens.every(token => fullTitle.includes(token))) filteredCases.push(testCaseItem); } fileItem.children = filteredCases; } if (fileItem.children!.length) result.push(fileItem); } return result; } function flattenTree(fileItems: FileItem[], expandedItems: Map, hasFilter: boolean): TreeItem[] { const result: TreeItem[] = []; for (const fileItem of fileItems) { result.push(fileItem); const expandState = expandedItems.get(fileItem.id); const autoExpandMatches = result.length < 100 && (hasFilter && expandState !== false); fileItem.expanded = expandState || autoExpandMatches; if (fileItem.expanded) { for (const testCaseItem of fileItem.children!) { result.push(testCaseItem); testCaseItem.expanded = !!expandedItems.get(testCaseItem.id); if (testCaseItem.expanded && testCaseItem.children!.length > 1) result.push(...testCaseItem.children!); } } } return result; }