mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
chore: render parse errors in the UI mode (#22888)
Fixes: https://github.com/microsoft/playwright/issues/22863
This commit is contained in:
parent
5fb426e7db
commit
b10cc03314
@ -22,7 +22,7 @@ import { serializeError } from '../util';
|
||||
import type { InternalReporter } from '../reporters/internalReporter';
|
||||
|
||||
type TaskTeardown = () => Promise<any> | undefined;
|
||||
export type Task<Context> = (context: Context, errors: TestError[]) => Promise<TaskTeardown | void> | undefined;
|
||||
export type Task<Context> = (context: Context, errors: TestError[], softErrors: TestError[]) => Promise<TaskTeardown | void> | undefined;
|
||||
|
||||
export class TaskRunner<Context> {
|
||||
private _tasks: { name: string, task: Task<Context> }[] = [];
|
||||
@ -62,15 +62,16 @@ export class TaskRunner<Context> {
|
||||
break;
|
||||
debug('pw:test:task')(`"${name}" started`);
|
||||
const errors: TestError[] = [];
|
||||
const softErrors: TestError[] = [];
|
||||
try {
|
||||
const teardown = await task(context, errors);
|
||||
const teardown = await task(context, errors, softErrors);
|
||||
if (teardown)
|
||||
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: teardown });
|
||||
} catch (e) {
|
||||
debug('pw:test:task')(`error in "${name}": `, e);
|
||||
errors.push(serializeError(e));
|
||||
} finally {
|
||||
for (const error of errors)
|
||||
for (const error of [...softErrors, ...errors])
|
||||
this._reporter.onError?.(error);
|
||||
if (errors.length) {
|
||||
if (!this._isTearDown)
|
||||
|
@ -63,7 +63,7 @@ export class TestRun {
|
||||
export function createTaskRunner(config: FullConfigInternal, reporter: InternalReporter): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
|
||||
addGlobalSetupTasks(taskRunner, config);
|
||||
taskRunner.addTask('load tests', createLoadTask('in-process', true));
|
||||
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }));
|
||||
addRunTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
@ -76,7 +76,7 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report
|
||||
|
||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: InternalReporter, additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', true, additionalFileMatcher));
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, additionalFileMatcher }));
|
||||
addRunTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
@ -104,7 +104,7 @@ function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal
|
||||
|
||||
export function createTaskRunnerForList(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process'): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
|
||||
taskRunner.addTask('load tests', createLoadTask(mode, false));
|
||||
taskRunner.addTask('load tests', createLoadTask(mode, { filterOnly: false, failOnLoadErrors: false }));
|
||||
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
|
||||
reporter.onBegin(config.config, rootSuite!);
|
||||
return () => reporter.onEnd();
|
||||
@ -166,11 +166,11 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
|
||||
};
|
||||
}
|
||||
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: boolean, additionalFileMatcher?: Matcher): Task<TestRun> {
|
||||
return async (testRun, errors) => {
|
||||
await collectProjectsAndTestFiles(testRun, additionalFileMatcher);
|
||||
await loadFileSuites(testRun, mode, errors);
|
||||
testRun.rootSuite = await createRootSuite(testRun, errors, shouldFilterOnly);
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
|
||||
return async (testRun, errors, softErrors) => {
|
||||
await collectProjectsAndTestFiles(testRun, options.additionalFileMatcher);
|
||||
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
|
||||
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
|
||||
// Fail when no tests.
|
||||
if (!testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard)
|
||||
throw new Error(`No tests found`);
|
||||
|
@ -19,7 +19,6 @@ import type { ResourceSnapshot } from '@trace/snapshot';
|
||||
import type * as trace from '@trace/trace';
|
||||
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
|
||||
import type { ContextEntry, PageEntry } from '../entries';
|
||||
import type { SerializedError, StackFrame } from '@protocol/channels';
|
||||
|
||||
const contextSymbol = Symbol('context');
|
||||
const nextInContextSymbol = Symbol('next');
|
||||
@ -27,8 +26,14 @@ const prevInListSymbol = Symbol('prev');
|
||||
const eventsSymbol = Symbol('events');
|
||||
const resourcesSymbol = Symbol('resources');
|
||||
|
||||
export type SourceLocation = {
|
||||
file: string;
|
||||
line: number;
|
||||
source: SourceModel;
|
||||
};
|
||||
|
||||
export type SourceModel = {
|
||||
errors: { error: SerializedError['error'], location: StackFrame }[];
|
||||
errors: { line: number, message: string }[];
|
||||
content: string | undefined;
|
||||
};
|
||||
|
||||
@ -203,7 +208,7 @@ function collectSources(actions: trace.ActionTraceEvent[]): Map<string, SourceMo
|
||||
}
|
||||
}
|
||||
if (action.error && action.stack?.[0])
|
||||
result.get(action.stack[0].file)!.errors.push({ error: action.error, location: action.stack?.[0] });
|
||||
result.get(action.stack[0].file)!.errors.push({ line: action.stack?.[0].line || 0, message: action.error.message });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -22,15 +22,14 @@ import './sourceTab.css';
|
||||
import { StackTraceView } from './stackTrace';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
|
||||
import type { SourceModel } from './modelUtil';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
import type { SourceLocation, SourceModel } from './modelUtil';
|
||||
|
||||
export const SourceTab: React.FunctionComponent<{
|
||||
action: ActionTraceEvent | undefined,
|
||||
sources: Map<string, SourceModel>,
|
||||
hideStackFrames?: boolean,
|
||||
rootDir?: string,
|
||||
fallbackLocation?: StackFrame,
|
||||
fallbackLocation?: SourceLocation,
|
||||
}> = ({ action, sources, hideStackFrames, rootDir, fallbackLocation }) => {
|
||||
const [lastAction, setLastAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
||||
@ -43,31 +42,34 @@ export const SourceTab: React.FunctionComponent<{
|
||||
}, [action, lastAction, setLastAction, setSelectedFrame]);
|
||||
|
||||
const { source, highlight, targetLine, fileName } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[] }>(async () => {
|
||||
const location = action?.stack?.[selectedFrame] || fallbackLocation;
|
||||
if (!location?.file)
|
||||
return { source: { errors: [], content: undefined }, targetLine: 0, highlight: [] };
|
||||
const actionLocation = action?.stack?.[selectedFrame];
|
||||
const shouldUseFallback = !actionLocation?.file;
|
||||
if (shouldUseFallback && !shouldUseFallback)
|
||||
return { source: { file: '', errors: [], content: undefined }, targetLine: 0, highlight: [] };
|
||||
|
||||
let source = sources.get(location.file);
|
||||
const file = shouldUseFallback ? fallbackLocation!.file : actionLocation.file;
|
||||
let source = sources.get(file);
|
||||
// Fallback location can fall outside the sources model.
|
||||
if (!source) {
|
||||
source = { errors: [], content: undefined };
|
||||
sources.set(location.file, source);
|
||||
source = { errors: fallbackLocation?.source?.errors || [], content: undefined };
|
||||
sources.set(file, source);
|
||||
}
|
||||
|
||||
const targetLine = location.line || 0;
|
||||
const fileName = rootDir && location.file.startsWith(rootDir) ? location.file.substring(rootDir.length + 1) : location.file;
|
||||
const highlight: SourceHighlight[] = source.errors.map(e => ({ type: 'error', line: e.location.line, message: e.error!.message }));
|
||||
const targetLine = shouldUseFallback ? fallbackLocation?.line || source.errors[0]?.line || 0 : actionLocation.line;
|
||||
const fileName = rootDir && file.startsWith(rootDir) ? file.substring(rootDir.length + 1) : file;
|
||||
const highlight: SourceHighlight[] = source.errors.map(e => ({ type: 'error', line: e.line, message: e.message }));
|
||||
highlight.push({ line: targetLine, type: 'running' });
|
||||
|
||||
if (source.content === undefined || fallbackLocation) {
|
||||
const sha1 = await calculateSha1(location.file);
|
||||
// After the source update, but before the test run, don't trust the cache.
|
||||
if (source.content === undefined || shouldUseFallback) {
|
||||
const sha1 = await calculateSha1(file);
|
||||
try {
|
||||
let response = await fetch(`sha1/src@${sha1}.txt`);
|
||||
if (response.status === 404)
|
||||
response = await fetch(`file?path=${location.file}`);
|
||||
response = await fetch(`file?path=${file}`);
|
||||
source.content = await response.text();
|
||||
} catch {
|
||||
source.content = `<Unable to read "${location.file}">`;
|
||||
source.content = `<Unable to read "${file}">`;
|
||||
}
|
||||
}
|
||||
return { source, highlight, targetLine, fileName };
|
||||
|
@ -25,6 +25,7 @@ import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
|
||||
import type { FullConfig, Suite, TestCase, Location, TestError } from '@playwright/test/types/testReporter';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import { idForAction, MultiTraceModel } from './modelUtil';
|
||||
import type { SourceLocation } from './modelUtil';
|
||||
import './uiModeView.css';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { Toolbar } from '@web/components/toolbar';
|
||||
@ -37,7 +38,7 @@ import { artifactsFolderName } from '@testIsomorphic/folders';
|
||||
import { msToString, settings, useSetting } from '@web/uiUtils';
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
|
||||
let updateRootSuite: (config: FullConfig, rootSuite: Suite, progress: Progress | undefined) => void = () => {};
|
||||
let updateRootSuite: (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress | undefined) => void = () => {};
|
||||
let runWatchedTests = (fileNames: string[]) => {};
|
||||
let xtermSize = { cols: 80, rows: 24 };
|
||||
|
||||
@ -54,6 +55,7 @@ const xtermDataSource: XtermDataSource = {
|
||||
type TestModel = {
|
||||
config: FullConfig | undefined;
|
||||
rootSuite: Suite | undefined;
|
||||
loadErrors: TestError[];
|
||||
};
|
||||
|
||||
export const UIModeView: React.FC<{}> = ({
|
||||
@ -67,9 +69,9 @@ export const UIModeView: React.FC<{}> = ({
|
||||
['skipped', false],
|
||||
]));
|
||||
const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map());
|
||||
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined });
|
||||
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined, loadErrors: [] });
|
||||
const [progress, setProgress] = React.useState<Progress & { total: number } | undefined>();
|
||||
const [selectedItem, setSelectedItem] = React.useState<{ location?: Location, testCase?: TestCase }>({});
|
||||
const [selectedItem, setSelectedItem] = React.useState<{ testFile?: SourceLocation, testCase?: TestCase }>({});
|
||||
const [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
||||
@ -81,21 +83,21 @@ export const UIModeView: React.FC<{}> = ({
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const reloadTests = () => {
|
||||
const reloadTests = React.useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setWatchedTreeIds({ value: new Set() });
|
||||
updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), undefined);
|
||||
updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), [], undefined);
|
||||
refreshRootSuite(true).then(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
reloadTests();
|
||||
}, []);
|
||||
}, [reloadTests]);
|
||||
|
||||
updateRootSuite = (config: FullConfig, rootSuite: Suite, newProgress: Progress | undefined) => {
|
||||
updateRootSuite = React.useCallback((config: FullConfig, rootSuite: Suite, loadErrors: TestError[], newProgress: Progress | undefined) => {
|
||||
const selectedProjects = config.configFile ? settings.getObject<string[] | undefined>(config.configFile + ':projects', undefined) : undefined;
|
||||
for (const projectName of projectFilters.keys()) {
|
||||
if (!rootSuite.suites.find(s => s.title === projectName))
|
||||
@ -108,13 +110,13 @@ export const UIModeView: React.FC<{}> = ({
|
||||
if (!selectedProjects && projectFilters.size && ![...projectFilters.values()].includes(true))
|
||||
projectFilters.set(projectFilters.entries().next().value[0], true);
|
||||
|
||||
setTestModel({ config, rootSuite });
|
||||
setTestModel({ config, rootSuite, loadErrors });
|
||||
setProjectFilters(new Map(projectFilters));
|
||||
if (runningState && newProgress)
|
||||
setProgress({ ...newProgress, total: runningState.testIds.size });
|
||||
else if (!newProgress)
|
||||
setProgress(undefined);
|
||||
};
|
||||
}, [projectFilters, runningState]);
|
||||
|
||||
const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set<string>) => {
|
||||
if (mode === 'bounce-if-busy' && runningState)
|
||||
@ -300,7 +302,7 @@ const TestList: React.FC<{
|
||||
statusFilters: Map<string, boolean>,
|
||||
projectFilters: Map<string, boolean>,
|
||||
filterText: string,
|
||||
testModel: { rootSuite: Suite | undefined, config: FullConfig | undefined },
|
||||
testModel: TestModel,
|
||||
runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set<string>) => void,
|
||||
runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean },
|
||||
watchAll: boolean,
|
||||
@ -308,7 +310,7 @@ const TestList: React.FC<{
|
||||
setWatchedTreeIds: (ids: { value: Set<string> }) => void,
|
||||
isLoading?: boolean,
|
||||
setVisibleTestIds: (testIds: Set<string>) => void,
|
||||
onItemSelected: (item: { testCase?: TestCase, location?: Location }) => void,
|
||||
onItemSelected: (item: { testCase?: TestCase, testFile?: SourceLocation }) => void,
|
||||
requestedCollapseAllCount: number,
|
||||
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds, requestedCollapseAllCount }) => {
|
||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||
@ -317,7 +319,7 @@ const TestList: React.FC<{
|
||||
|
||||
// Build the test tree.
|
||||
const { rootItem, treeItemMap, fileNames } = React.useMemo(() => {
|
||||
let rootItem = createTree(testModel.rootSuite, projectFilters);
|
||||
let rootItem = createTree(testModel.rootSuite, testModel.loadErrors, projectFilters);
|
||||
filterTree(rootItem, filterText, statusFilters, runningState?.testIds);
|
||||
sortAndPropagateStatus(rootItem);
|
||||
rootItem = shortenRoot(rootItem);
|
||||
@ -375,15 +377,25 @@ const TestList: React.FC<{
|
||||
// Compute selected item.
|
||||
const { selectedTreeItem } = React.useMemo(() => {
|
||||
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
||||
const location = selectedTreeItem?.location;
|
||||
let testFile: SourceLocation | undefined;
|
||||
if (selectedTreeItem) {
|
||||
testFile = {
|
||||
file: selectedTreeItem.location.file,
|
||||
line: selectedTreeItem.location.line,
|
||||
source: {
|
||||
errors: testModel.loadErrors.filter(e => e.location?.file === selectedTreeItem.location.file).map(e => ({ line: e.location!.line, message: e.message! })),
|
||||
content: undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
let selectedTest: TestCase | undefined;
|
||||
if (selectedTreeItem?.kind === 'test')
|
||||
selectedTest = selectedTreeItem.test;
|
||||
else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1)
|
||||
selectedTest = selectedTreeItem.tests[0];
|
||||
onItemSelected({ testCase: selectedTest, location });
|
||||
onItemSelected({ testCase: selectedTest, testFile });
|
||||
return { selectedTreeItem };
|
||||
}, [onItemSelected, selectedTreeItemId, treeItemMap]);
|
||||
}, [onItemSelected, selectedTreeItemId, testModel, treeItemMap]);
|
||||
|
||||
// Update watch all.
|
||||
React.useEffect(() => {
|
||||
@ -471,12 +483,13 @@ const TestList: React.FC<{
|
||||
runningState.itemSelectedByUser = true;
|
||||
setSelectedTreeItemId(treeItem.id);
|
||||
}}
|
||||
isError={treeItem => treeItem.kind === 'group' ? treeItem.hasLoadErrors : false}
|
||||
autoExpandDepth={filterText ? 5 : 1}
|
||||
noItemsMessage={isLoading ? 'Loading\u2026' : 'No tests'} />;
|
||||
};
|
||||
|
||||
const TraceView: React.FC<{
|
||||
item: { location?: Location, testCase?: TestCase },
|
||||
item: { testFile?: SourceLocation, testCase?: TestCase },
|
||||
rootDir?: string,
|
||||
}> = ({ item, rootDir }) => {
|
||||
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
||||
@ -543,7 +556,7 @@ const TraceView: React.FC<{
|
||||
rootDir={rootDir}
|
||||
initialSelection={initialSelection}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
defaultSourceLocation={item.location} />;
|
||||
fallbackLocation={item.testFile} />;
|
||||
};
|
||||
|
||||
declare global {
|
||||
@ -555,15 +568,15 @@ declare global {
|
||||
let receiver: TeleReporterReceiver | undefined;
|
||||
|
||||
let throttleTimer: NodeJS.Timeout | undefined;
|
||||
let throttleData: { config: FullConfig, rootSuite: Suite, progress: Progress } | undefined;
|
||||
let throttleData: { config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress } | undefined;
|
||||
const throttledAction = () => {
|
||||
clearTimeout(throttleTimer);
|
||||
throttleTimer = undefined;
|
||||
updateRootSuite(throttleData!.config, throttleData!.rootSuite, throttleData!.progress);
|
||||
updateRootSuite(throttleData!.config, throttleData!.rootSuite, throttleData!.loadErrors, throttleData!.progress);
|
||||
};
|
||||
|
||||
const throttleUpdateRootSuite = (config: FullConfig, rootSuite: Suite, progress: Progress, immediate = false) => {
|
||||
throttleData = { config, rootSuite, progress };
|
||||
const throttleUpdateRootSuite = (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress, immediate = false) => {
|
||||
throttleData = { config, rootSuite, loadErrors, progress };
|
||||
if (immediate)
|
||||
throttledAction();
|
||||
else if (!throttleTimer)
|
||||
@ -575,6 +588,7 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
|
||||
return sendMessage('list', {});
|
||||
|
||||
let rootSuite: Suite;
|
||||
let loadErrors: TestError[];
|
||||
const progress: Progress = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
@ -583,21 +597,23 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
|
||||
let config: FullConfig;
|
||||
receiver = new TeleReporterReceiver(pathSeparator, {
|
||||
onBegin: (c: FullConfig, suite: Suite) => {
|
||||
if (!rootSuite)
|
||||
if (!rootSuite) {
|
||||
rootSuite = suite;
|
||||
loadErrors = [];
|
||||
}
|
||||
config = c;
|
||||
progress.passed = 0;
|
||||
progress.failed = 0;
|
||||
progress.skipped = 0;
|
||||
throttleUpdateRootSuite(config, rootSuite, progress, true);
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress, true);
|
||||
},
|
||||
|
||||
onEnd: () => {
|
||||
throttleUpdateRootSuite(config, rootSuite, progress, true);
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress, true);
|
||||
},
|
||||
|
||||
onTestBegin: () => {
|
||||
throttleUpdateRootSuite(config, rootSuite, progress);
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
|
||||
},
|
||||
|
||||
onTestEnd: (test: TestCase) => {
|
||||
@ -607,11 +623,13 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
|
||||
++progress.failed;
|
||||
else
|
||||
++progress.passed;
|
||||
throttleUpdateRootSuite(config, rootSuite, progress);
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
|
||||
},
|
||||
|
||||
onError: (error: TestError) => {
|
||||
xtermDataSource.write((error.stack || error.value || '') + '\n');
|
||||
loadErrors.push(error);
|
||||
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
|
||||
},
|
||||
});
|
||||
receiver._setClearPreviousResultsWhenTestBegins();
|
||||
@ -708,6 +726,7 @@ type TreeItemBase = {
|
||||
type GroupItem = TreeItemBase & {
|
||||
kind: 'group';
|
||||
subKind: 'folder' | 'file' | 'describe';
|
||||
hasLoadErrors: boolean;
|
||||
children: (TestCaseItem | GroupItem)[];
|
||||
};
|
||||
|
||||
@ -743,13 +762,14 @@ function getFileItem(rootItem: GroupItem, filePath: string[], isFile: boolean, f
|
||||
parent: parentFileItem,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
parentFileItem.children.push(fileItem);
|
||||
fileItems.set(fileName, fileItem);
|
||||
return fileItem;
|
||||
}
|
||||
|
||||
function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, boolean>): GroupItem {
|
||||
function createTree(rootSuite: Suite | undefined, loadErrors: TestError[], projectFilters: Map<string, boolean>): GroupItem {
|
||||
const filterProjects = [...projectFilters.values()].some(Boolean);
|
||||
const rootItem: GroupItem = {
|
||||
kind: 'group',
|
||||
@ -761,6 +781,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
parent: undefined,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
|
||||
const visitSuite = (projectName: string, parentSuite: Suite, parentGroup: GroupItem) => {
|
||||
@ -778,6 +799,7 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
parent: parentGroup,
|
||||
children: [],
|
||||
status: 'none',
|
||||
hasLoadErrors: false,
|
||||
};
|
||||
parentGroup.children.push(group);
|
||||
}
|
||||
@ -842,6 +864,12 @@ function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, bo
|
||||
const fileItem = getFileItem(rootItem, fileSuite.location!.file.split(pathSeparator), true, fileMap);
|
||||
visitSuite(projectSuite.title, fileSuite, fileItem);
|
||||
}
|
||||
for (const loadError of loadErrors) {
|
||||
if (!loadError.location)
|
||||
continue;
|
||||
const fileItem = getFileItem(rootItem, loadError.location.file.split(pathSeparator), true, fileMap);
|
||||
fileItem.hasLoadErrors = true;
|
||||
}
|
||||
}
|
||||
return rootItem;
|
||||
}
|
||||
@ -869,7 +897,7 @@ function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map<
|
||||
newChildren.push(child);
|
||||
} else {
|
||||
visit(child);
|
||||
if (child.children.length)
|
||||
if (child.children.length || child.hasLoadErrors)
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
|
||||
import { Timeline } from './timeline';
|
||||
import './workbench.css';
|
||||
import { MetadataView } from './metadataView';
|
||||
import type { Location } from '../../../playwright-test/types/testReporter';
|
||||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
model?: MultiTraceModel,
|
||||
@ -38,10 +37,10 @@ export const Workbench: React.FunctionComponent<{
|
||||
hideStackFrames?: boolean,
|
||||
showSourcesFirst?: boolean,
|
||||
rootDir?: string,
|
||||
defaultSourceLocation?: Location,
|
||||
fallbackLocation?: modelUtil.SourceLocation,
|
||||
initialSelection?: ActionTraceEvent,
|
||||
onSelectionChanged?: (action: ActionTraceEvent) => void,
|
||||
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst, rootDir, defaultSourceLocation, initialSelection, onSelectionChanged }) => {
|
||||
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged }) => {
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(undefined);
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||
@ -85,7 +84,7 @@ export const Workbench: React.FunctionComponent<{
|
||||
sources={sources}
|
||||
hideStackFrames={hideStackFrames}
|
||||
rootDir={rootDir}
|
||||
fallbackLocation={defaultSourceLocation} />
|
||||
fallbackLocation={fallbackLocation} />
|
||||
};
|
||||
const consoleTab: TabbedPaneTabModel = {
|
||||
id: 'console',
|
||||
|
@ -61,10 +61,6 @@
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.list-view-content:focus .list-view-entry.error.selected {
|
||||
outline: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
}
|
||||
|
||||
.list-view-content:focus .list-view-entry.selected .codicon {
|
||||
color: var(--vscode-list-activeSelectionForeground) !important;
|
||||
}
|
||||
@ -78,5 +74,4 @@
|
||||
|
||||
.list-view-entry.error {
|
||||
color: var(--vscode-list-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
}
|
||||
|
@ -64,3 +64,39 @@ test('should show selected test in sources', async ({ runUITest }) => {
|
||||
page.locator('.CodeMirror .source-line-running'),
|
||||
).toHaveText(`3 test('third', () => {});`);
|
||||
});
|
||||
|
||||
test('should show syntax errors in file', async ({ runUITest }) => {
|
||||
const { page } = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
const a = 1;
|
||||
a = 2;
|
||||
test('first', () => {});
|
||||
test('second', () => {});
|
||||
`,
|
||||
'b.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test('third', () => {});
|
||||
`,
|
||||
});
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
◯ a.test.ts
|
||||
▼ ◯ b.test.ts
|
||||
◯ third
|
||||
`);
|
||||
|
||||
await page.getByTestId('test-tree').getByText('a.test.ts').click();
|
||||
await expect(
|
||||
page.getByTestId('source-code').locator('.source-tab-file-name')
|
||||
).toHaveText('a.test.ts');
|
||||
await expect(
|
||||
page.locator('.CodeMirror .source-line-running'),
|
||||
).toHaveText(`4 a = 2;`);
|
||||
|
||||
await expect(
|
||||
page.locator('.CodeMirror-linewidget')
|
||||
).toHaveText([
|
||||
' ',
|
||||
'Assignment to constant variable.'
|
||||
]);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user