/** * 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'; let rootSuite: Suite | undefined; let updateList: () => void = () => {}; let updateProgress: () => void = () => {}; type Entry = { test?: TestCase, fileSuite: Suite }; export const WatchModeView: React.FC<{}> = ({ }) => { const [updateCounter, setUpdateCounter] = React.useState(0); updateList = () => setUpdateCounter(updateCounter + 1); const [selectedFileSuite, setSelectedFileSuite] = React.useState(); const [selectedTest, setSelectedTest] = React.useState(); const [isRunningTest, setIsRunningTest] = React.useState(false); const [expandedFiles] = React.useState(new Map()); const [filterText, setFilterText] = React.useState(''); const inputRef = React.useRef(null); React.useEffect(() => { inputRef.current?.focus(); }, []); const selectedOrDefaultFileSuite = selectedFileSuite || rootSuite?.suites?.[0]?.suites?.[0]; const tests: TestCase[] = []; const fileSuites: Suite[] = []; for (const projectSuite of rootSuite?.suites || []) { for (const fileSuite of projectSuite.suites) { if (fileSuite === selectedOrDefaultFileSuite) tests.push(...fileSuite.allTests()); fileSuites.push(fileSuite); } } const explicitlyOrAutoExpandedFiles = new Set(); const entries = new Map(); const trimmedFilterText = filterText.trim(); const filterTokens = trimmedFilterText.split(' '); for (const fileSuite of fileSuites) { const hasMatch = !trimmedFilterText || fileSuite.allTests().some(test => { const fullTitle = test.titlePath().join(' '); return !filterTokens.some(token => !fullTitle.includes(token)); }); if (hasMatch) entries.set(fileSuite, { fileSuite }); const expandState = expandedFiles.get(fileSuite); const autoExpandMatches = entries.size < 100 && (trimmedFilterText && hasMatch && expandState !== false); if (expandState === true || autoExpandMatches) { explicitlyOrAutoExpandedFiles.add(fileSuite); for (const test of fileSuite.allTests()) { const fullTitle = test.titlePath().join(' '); if (!filterTokens.some(token => !fullTitle.includes(token))) entries.set(test, { test, fileSuite }); } } } const selectedEntry = selectedTest ? entries.get(selectedTest) : selectedOrDefaultFileSuite ? entries.get(selectedOrDefaultFileSuite) : undefined; return
{ setFilterText(e.target.value); }} onKeyDown={e => { }}>
entry.test ? entry.test!.id : entry.fileSuite.title } itemRender={(entry: Entry) => entry.test ? entry.test!.titlePath().slice(3).join(' › ') : entry.fileSuite.title } itemIcon={(entry: Entry) => { if (entry.test) { if (entry.test.results.length && entry.test.results[0].duration) return entry.test.ok() ? 'codicon-check' : 'codicon-error'; if (entry.test.results.length) return 'codicon-loading'; } else { if (explicitlyOrAutoExpandedFiles.has(entry.fileSuite)) return 'codicon-chevron-down'; return 'codicon-chevron-right'; } }} itemIndent={(entry: Entry) => entry.test ? 1 : 0} selectedItem={selectedEntry} onAccepted={(entry: Entry) => { if (entry.test) { setSelectedTest(entry.test); setIsRunningTest(true); runTests(entry.test ? entry.test.location.file + ':' + entry.test.location.line : entry.fileSuite.title).then(() => { setIsRunningTest(false); }); } }} onLeftArrow={(entry: Entry) => { expandedFiles.set(entry.fileSuite, false); setSelectedTest(undefined); setSelectedFileSuite(entry.fileSuite); updateList(); }} onRightArrow={(entry: Entry) => { expandedFiles.set(entry.fileSuite, true); updateList(); }} onSelected={(entry: Entry) => { if (entry.test) { setSelectedFileSuite(undefined); setSelectedTest(entry.test!); } else { setSelectedTest(undefined); setSelectedFileSuite(entry.fileSuite); } }} onIconClicked={(entry: Entry) => { if (explicitlyOrAutoExpandedFiles.has(entry.fileSuite)) expandedFiles.set(entry.fileSuite, false); else expandedFiles.set(entry.fileSuite, true); updateList(); }} showNoItemsMessage={true}>
; }; export const ProgressView: React.FC<{ test: TestCase | undefined, }> = ({ test, }) => { const [updateCounter, setUpdateCounter] = React.useState(0); updateProgress = () => setUpdateCounter(updateCounter + 1); const steps: (TestCase | TestStep)[] = []; for (const result of test?.results || []) steps.push(...result.steps); return step.title} itemIcon={(step: TestStep) => step.error ? 'codicon-error' : 'codicon-check'} >; }; export const TraceView: React.FC<{ test: TestCase | undefined, isRunningTest: boolean, }> = ({ test, isRunningTest }) => { const [model, setModel] = React.useState(); React.useEffect(() => { (async () => { if (!test) { setModel(undefined); return; } for (const result of test.results) { const attachment = result.attachments.find(a => a.name === 'trace'); if (attachment && attachment.path) { setModel(await loadSingleTraceFile(attachment.path)); return; } } setModel(undefined); })(); }, [test, 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; } } const receiver = new TeleReporterReceiver({ onBegin: (config: FullConfig, suite: Suite) => { if (!rootSuite) rootSuite = suite; updateList(); }, onTestBegin: () => { updateList(); }, onTestEnd: () => { updateList(); }, onStepBegin: () => { updateProgress(); }, onStepEnd: () => { updateProgress(); }, }); (window as any).dispatch = (message: any) => { receiver.dispatch(message); }; async function runTests(location: string): Promise { await (window as any).binding({ method: 'run', params: { location } }); }