feat(reporters): Add error position to JSON Report (#9151)

This commit is contained in:
Sidharth Vinod 2021-10-01 02:48:36 +05:30 committed by GitHub
parent 4e372dccb5
commit fcb7d2b15a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 83 additions and 21 deletions

View file

@ -26,6 +26,7 @@ import { FullConfig, TestCase, Suite, TestResult, TestError, Reporter, FullResul
const stackUtils = new StackUtils(); const stackUtils = new StackUtils();
type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
export type PositionInFile = { column: number; line: number };
const kOutputSymbol = Symbol('output'); const kOutputSymbol = Symbol('output');
export class BaseReporter implements Reporter { export class BaseReporter implements Reporter {
@ -240,28 +241,24 @@ function formatTestHeader(config: FullConfig, test: TestCase, indent: string, in
export function formatError(error: TestError, file?: string) { export function formatError(error: TestError, file?: string) {
const stack = error.stack; const stack = error.stack;
const tokens = []; const tokens = [''];
if (stack) { if (stack) {
tokens.push(''); const { message, stackLines, position } = prepareErrorStack(
const lines = stack.split('\n'); stack,
let firstStackLine = lines.findIndex(line => line.startsWith(' at ')); file
if (firstStackLine === -1) );
firstStackLine = lines.length; tokens.push(message);
tokens.push(lines.slice(0, firstStackLine).join('\n'));
const stackLines = lines.slice(firstStackLine); const codeFrame = generateCodeFrame(file, position);
const position = file ? positionInFile(stackLines, file) : null; if (codeFrame) {
if (position) {
const source = fs.readFileSync(file!, 'utf8');
tokens.push(''); tokens.push('');
tokens.push(codeFrameColumns(source, { start: position }, { highlightCode: colors.enabled })); tokens.push(codeFrame);
} }
tokens.push(''); tokens.push('');
tokens.push(colors.dim(stackLines.join('\n'))); tokens.push(colors.dim(stackLines.join('\n')));
} else if (error.message) { } else if (error.message) {
tokens.push('');
tokens.push(error.message); tokens.push(error.message);
} else { } else if (error.value) {
tokens.push('');
tokens.push(error.value); tokens.push(error.value);
} }
return tokens.join('\n'); return tokens.join('\n');
@ -277,6 +274,38 @@ function indent(lines: string, tab: string) {
return lines.replace(/^(?=.+$)/gm, tab); return lines.replace(/^(?=.+$)/gm, tab);
} }
function generateCodeFrame(file?: string, position?: PositionInFile): string | undefined {
if (!position || !file)
return;
const source = fs.readFileSync(file!, 'utf8');
const codeFrame = codeFrameColumns(
source,
{ start: position },
{ highlightCode: colors.enabled }
);
return codeFrame;
}
export function prepareErrorStack(stack: string, file?: string): {
message: string;
stackLines: string[];
position?: PositionInFile;
} {
const lines = stack.split('\n');
let firstStackLine = lines.findIndex(line => line.startsWith(' at '));
if (firstStackLine === -1) firstStackLine = lines.length;
const message = lines.slice(0, firstStackLine).join('\n');
const stackLines = lines.slice(firstStackLine);
const position = file ? positionInFile(stackLines, file) : undefined;
return {
message,
stackLines,
position,
};
}
function positionInFile(stackLines: string[], file: string): { column: number; line: number; } | undefined { function positionInFile(stackLines: string[], file: string): { column: number; line: number; } | undefined {
// Stack will have /private/var/folders instead of /var/folders on Mac. // Stack will have /private/var/folders instead of /var/folders on Mac.
file = fs.realpathSync(file); file = fs.realpathSync(file);

View file

@ -17,6 +17,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, TestStatus, Location, Reporter } from '../../../types/testReporter'; import { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, TestStatus, Location, Reporter } from '../../../types/testReporter';
import { PositionInFile, prepareErrorStack } from './base';
export interface JSONReport { export interface JSONReport {
config: Omit<FullConfig, 'projects'> & { config: Omit<FullConfig, 'projects'> & {
@ -65,11 +66,17 @@ export interface JSONReportTestResult {
status: TestStatus | undefined; status: TestStatus | undefined;
duration: number; duration: number;
error: TestError | undefined; error: TestError | undefined;
stdout: JSONReportSTDIOEntry[], stdout: JSONReportSTDIOEntry[];
stderr: JSONReportSTDIOEntry[], stderr: JSONReportSTDIOEntry[];
retry: number; retry: number;
steps?: JSONReportTestStep[]; steps?: JSONReportTestStep[];
attachments: { name: string, path?: string, body?: string, contentType: string }[]; attachments: {
name: string;
path?: string;
body?: string;
contentType: string;
}[];
errorLocation?: PositionInFile
} }
export interface JSONReportTestStep { export interface JSONReportTestStep {
title: string; title: string;
@ -216,14 +223,14 @@ class JSONReporter implements Reporter {
annotations: test.annotations, annotations: test.annotations,
expectedStatus: test.expectedStatus, expectedStatus: test.expectedStatus,
projectName: test.titlePath()[1], projectName: test.titlePath()[1],
results: test.results.map(r => this._serializeTestResult(r)), results: test.results.map(r => this._serializeTestResult(r, test.location.file)),
status: test.outcome(), status: test.outcome(),
}; };
} }
private _serializeTestResult(result: TestResult): JSONReportTestResult { private _serializeTestResult(result: TestResult, file: string): JSONReportTestResult {
const steps = result.steps.filter(s => s.category === 'test.step'); const steps = result.steps.filter(s => s.category === 'test.step');
return { const jsonResult: JSONReportTestResult = {
workerIndex: result.workerIndex, workerIndex: result.workerIndex,
status: result.status, status: result.status,
duration: result.duration, duration: result.duration,
@ -239,6 +246,15 @@ class JSONReporter implements Reporter {
body: a.body?.toString('base64') body: a.body?.toString('base64')
})), })),
}; };
if (result.error?.stack) {
const { position } = prepareErrorStack(
result.error.stack,
file
);
if (position)
jsonResult.errorLocation = position;
}
return jsonResult;
} }
private _serializeTestStep(step: TestStep): JSONReportTestStep { private _serializeTestStep(step: TestStep): JSONReportTestStep {

View file

@ -183,3 +183,20 @@ test('should have relative always-posix paths', async ({ runInlineTest }) => {
expect(result.report.suites[0].specs[0].line).toBe(6); expect(result.report.suites[0].specs[0].line).toBe(6);
expect(result.report.suites[0].specs[0].column).toBe(7); expect(result.report.suites[0].specs[0].column).toBe(7);
}); });
test('should have error position in results', async ({
runInlineTest,
}) => {
const result = await runInlineTest({
'a.test.js': `
const { test } = pwt;
test('math works!', async ({}) => {
expect(1 + 1).toBe(3);
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.report.suites[0].specs[0].file).toBe('a.test.js');
expect(result.report.suites[0].specs[0].tests[0].results[0].errorLocation.line).toBe(7);
expect(result.report.suites[0].specs[0].tests[0].results[0].errorLocation.column).toBe(23);
});