Skip to content

Commit

Permalink
chore: only highlight uncaught errors in source
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Dec 22, 2023
1 parent eeb9e06 commit 4b652f3
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 33 deletions.
33 changes: 4 additions & 29 deletions packages/trace-viewer/src/ui/errorsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,39 +31,14 @@ type ErrorsTabModel = {
errors: Map<string, ErrorDescription>;
};

function errorsFromActions(model: modelUtil.MultiTraceModel): Map<string, ErrorDescription> {
const errors = new Map<string, ErrorDescription>();
for (const action of model.actions || []) {
// Overwrite errors with the last one.
if (!action.error?.message || errors.has(action.error.message))
continue;
errors.set(action.error.message, {
action,
stack: action.stack,
});
}
return errors;
}

function errorsFromTestRunner(model: modelUtil.MultiTraceModel): Map<string, ErrorDescription> {
const actionErrors = errorsFromActions(model);
const errors = new Map<string, ErrorDescription>();
for (const error of model.errors || []) {
if (!error.message || errors.has(error.message))
continue;
errors.set(error.message, actionErrors.get(error.message) || error);
}
return errors;
}

export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined): ErrorsTabModel {
return React.useMemo(() => {
if (!model)
return { errors: new Map() };
// Feature detection: if there is test runner info, pick errors from the 'error' trace events.
// If there are no test errors, but there are action errors - render those instead.
const testHasErrors = !!model.errors.length;
return { errors: testHasErrors ? errorsFromTestRunner(model) : errorsFromActions(model) };
const errors = new Map<string, ErrorDescription>();
for (const error of model.errorDescriptors)
errors.set(error.message, error);
return { errors };
}, [model]);
}

Expand Down
54 changes: 50 additions & 4 deletions packages/trace-viewer/src/ui/modelUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ResourceSnapshot } from '@trace/snapshot';
import type * as trace from '@trace/trace';
import type { ActionTraceEvent } from '@trace/trace';
import type { ContextEntry, PageEntry } from '../entries';
import type { StackFrame } from '@protocol/channels';

const contextSymbol = Symbol('context');
const nextInContextSymbol = Symbol('next');
Expand Down Expand Up @@ -48,6 +49,12 @@ export type ActionTreeItem = {
action?: ActionTraceEventInContext;
};

type ErrorDescription = {
action?: ActionTraceEventInContext;
stack?: StackFrame[];
message: string;
};

export class MultiTraceModel {
readonly startTime: number;
readonly endTime: number;
Expand All @@ -62,7 +69,9 @@ export class MultiTraceModel {
readonly events: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[];
readonly stdio: trace.StdioTraceEvent[];
readonly errors: trace.ErrorTraceEvent[];
readonly errorDescriptors: ErrorDescription[];
readonly hasSource: boolean;
readonly hasStepData: boolean;
readonly sdkLanguage: Language | undefined;
readonly testIdAttributeName: string | undefined;
readonly sources: Map<string, SourceModel>;
Expand All @@ -89,17 +98,46 @@ export class MultiTraceModel {
this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio));
this.errors = ([] as trace.ErrorTraceEvent[]).concat(...contexts.map(c => c.errors));
this.hasSource = contexts.some(c => c.hasSource);
this.hasStepData = contexts.some(context => !context.isPrimary);
this.resources = [...contexts.map(c => c.resources)].flat();

this.events.sort((a1, a2) => a1.time - a2.time);
this.resources.sort((a1, a2) => a1._monotonicTime! - a2._monotonicTime!);
this.sources = collectSources(this.actions);
this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions();
this.sources = collectSources(this.actions, this.errorDescriptors);
}

failedAction() {
// This find innermost action for nested ones.
return this.actions.findLast(a => a.error);
}

private _errorDescriptorsFromActions(): ErrorDescription[] {
const errors: ErrorDescription[] = [];
for (const action of this.actions || []) {
if (!action.error?.message)
continue;
errors.push({
action,
stack: action.stack,
message: action.error.message,
});
}
return errors;
}

private _errorDescriptorsFromTestRunner(): ErrorDescription[] {
const errors: ErrorDescription[] = [];
for (const error of this.errors || []) {
if (!error.message)
continue;
errors.push({
stack: error.stack,
message: error.message
});
}
return errors;
}
}

function indexModel(context: ContextEntry) {
Expand Down Expand Up @@ -248,7 +286,7 @@ export function eventsForAction(action: ActionTraceEvent): (trace.EventTraceEven
return result;
}

function collectSources(actions: trace.ActionTraceEvent[]): Map<string, SourceModel> {
function collectSources(actions: trace.ActionTraceEvent[], errorDescriptors: ErrorDescription[]): Map<string, SourceModel> {
const result = new Map<string, SourceModel>();
for (const action of actions) {
for (const frame of action.stack || []) {
Expand All @@ -258,8 +296,16 @@ function collectSources(actions: trace.ActionTraceEvent[]): Map<string, SourceMo
result.set(frame.file, source);
}
}
if (action.error && action.stack?.[0])
result.get(action.stack[0].file)!.errors.push({ line: action.stack?.[0].line || 0, message: action.error.message });
}

for (const error of errorDescriptors) {
const { action, stack, message } = error;
if (!action || !stack)
continue;
result.get(stack[0].file)?.errors.push({
line: stack[0].line || 0,
message
});
}
return result;
}
32 changes: 32 additions & 0 deletions tests/playwright-test/ui-mode-trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,35 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => {
/After Hooks/,
]);
});

test('should not show caught errors in the errors tab', async ({ runUITest }, testInfo) => {
const { page } = await runUITest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({ page }, testInfo) => {
await page.setContent("<input id='checkbox' type='checkbox'></input>");
await expect(page.locator('input')).toBeChecked({ timeout: 1 }).catch(() => {});
});
`,
});

await page.getByText('pass').dblclick();
const listItem = page.getByTestId('actions-tree').getByRole('listitem');

await expect(
listItem,
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
/page.setContent/,
/expect.toBeCheckedlocator.*[\d.]+m?s/,
/After Hooks/,
]);

await page.getByText('Source', { exact: true }).click();
await expect(page.locator('.source-line-running')).toContainText('toBeChecked');
await expect(page.locator('.CodeMirror-linewidget')).toHaveCount(0);

await page.getByText('Errors', { exact: true }).click();
await expect(page.locator('.tab-errors')).toHaveText('No errors');
});

0 comments on commit 4b652f3

Please sign in to comment.