feat(reporters): Add error position to JSON Report (#9151)
This commit is contained in:
parent
4e372dccb5
commit
fcb7d2b15a
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue