Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(@jest/reporters): improve annotation formatting of GitHubActionsReporter #12826

Merged
merged 22 commits into from May 9, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[@jest/reporters]` Improve `GitHubActionsReporter`s annotation format ([#12826](https://github.com/facebook/jest/pull/12826))

### Fixes

### Chore & Maintenance
Expand All @@ -12,7 +14,7 @@

### Features

- `[jest-circus]` Add `failing` test modifier that inverts the behaviour of tests ([#12610](https://github.com/facebook/jest/pull/12610))
- `[jest-circus]` Add `failing` test modifier that inverts the behavior of tests ([#12610](https://github.com/facebook/jest/pull/12610))
- `[jest-environment-node, jest-environment-jsdom]` Allow specifying `customExportConditions` ([#12774](https://github.com/facebook/jest/pull/12774))

### Fixes
Expand Down
1 change: 1 addition & 0 deletions packages/jest-reporters/package.json
Expand Up @@ -29,6 +29,7 @@
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.0",
"istanbul-reports": "^3.1.3",
"jest-message-util": "^28.1.0",
"jest-util": "^28.1.0",
"jest-worker": "^28.1.0",
"slash": "^3.0.0",
Expand Down
75 changes: 35 additions & 40 deletions packages/jest-reporters/src/GitHubActionsReporter.ts
Expand Up @@ -5,55 +5,50 @@
* LICENSE file in the root directory of this source tree.
*/

import {relative} from 'path';
import slash = require('slash');
import stripAnsi = require('strip-ansi');
import type {
AggregatedResult,
TestContext,
TestResult,
} from '@jest/test-result';
import type {Test, TestCaseResult} from '@jest/test-result';
import {
getStackTraceLines,
getTopFrame,
separateMessageFromStack,
} from 'jest-message-util';
import BaseReporter from './BaseReporter';

const lineAndColumnInStackTrace = /^.*?:([0-9]+):([0-9]+).*$/;

function replaceEntities(s: string): string {
// https://github.com/actions/toolkit/blob/b4639928698a6bfe1c4bdae4b2bfdad1cb75016d/packages/core/src/command.ts#L80-L85
const substitutions: Array<[RegExp, string]> = [
[/%/g, '%25'],
[/\r/g, '%0D'],
[/\n/g, '%0A'],
];
return substitutions.reduce((acc, sub) => acc.replace(...sub), s);
}
const errorTitleSeparator = ' \u203A ';

export default class GitHubActionsReporter extends BaseReporter {
static readonly filename = __filename;

override onRunComplete(
_testContexts?: Set<TestContext>,
aggregatedResults?: AggregatedResult,
override onTestCaseResult(
test: Test,
{failureMessages, ancestorTitles, title}: TestCaseResult,
): void {
const messages = getMessages(aggregatedResults?.testResults);

for (const message of messages) {
this.log(message);
}
failureMessages.forEach(failure => {
const {message, stack} = separateMessageFromStack(stripAnsi(failure));

const relativeTestPath = slash(relative('', test.path));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, rootDir is not pointing to the right place. See #12822 (comment).

In the other hand, in CI it feels right to keep paths relative to root of the repo.

const stackLines = getStackTraceLines(stack);
const normalizedStackLines = stackLines.map(line =>
line.replace(test.path, relativeTestPath),
);

const topFrame = getTopFrame(stackLines);

const errorTitle = [...ancestorTitles, title].join(errorTitleSeparator);
const errorMessage = normalizeMessage(
[message, ...normalizedStackLines].join('\n'),
);

this.log(
`\n::error file=${test.path},line=${topFrame?.line},title=${errorTitle}::${errorMessage}`,
);
});
}
}

function getMessages(results: Array<TestResult> | undefined) {
if (!results) return [];

return results.flatMap(({testFilePath, testResults}) =>
testResults
.filter(r => r.status === 'failed')
.flatMap(r => r.failureMessages)
.map(m => stripAnsi(m))
.map(m => replaceEntities(m))
.map(m => lineAndColumnInStackTrace.exec(m))
.filter((m): m is RegExpExecArray => m !== null)
.map(
([message, line, col]) =>
`\n::error file=${testFilePath},line=${line},col=${col}::${message}`,
),
);
// copied from: https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts
function normalizeMessage(input: string): string {
return input.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
}
118 changes: 0 additions & 118 deletions packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js

This file was deleted.

103 changes: 103 additions & 0 deletions packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts
@@ -0,0 +1,103 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type {Test, TestCaseResult} from '@jest/test-result';
import GitHubActionsReporter from '../GitHubActionsReporter';

jest.mock('path', () => ({
relative: () => '__tests__/example.test.js',
}));

jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn());

afterEach(() => {
jest.clearAllMocks();
});

const reporter = new GitHubActionsReporter();

const testMeta = {
context: {config: {rootDir: '/user/project'}},
path: '/user/project/__tests__/example.test.js',
} as Test;

const expectationsErrorMessage =
'Error: \x1B[2mexpect(\x1B[22m\x1B[31mreceived\x1B[39m\x1B[2m).\x1B[22mtoBe\x1B[2m(\x1B[22m\x1B[32mexpected\x1B[39m\x1B[2m) // Object.is equality\x1B[22m\n' +
'\n' +
'Expected: \x1B[32m1\x1B[39m\n' +
'Received: \x1B[31m10\x1B[39m\n' +
' at Object.toBe (/user/project/__tests__/example.test.js:20:14)\n' +
' at Promise.then.completed (/user/project/jest/packages/jest-circus/build/utils.js:333:28)\n' +
' at new Promise (<anonymous>)\n' +
' at callAsyncCircusFn (/user/project/jest/packages/jest-circus/build/utils.js:259:10)\n' +
' at _callCircusTest (/user/project/jest/packages/jest-circus/build/run.js:276:40)\n' +
' at processTicksAndRejections (node:internal/process/task_queues:95:5)\n' +
' at _runTest (/user/project/jest/packages/jest-circus/build/run.js:208:3)\n' +
' at _runTestsForDescribeBlock (/user/project/jest/packages/jest-circus/build/run.js:96:9)\n' +
' at run (/user/project/jest/packages/jest-circus/build/run.js:31:3)\n' +
' at runAndTransformResultsToJestFormat (/user/project/jest/packages/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:135:21)';

const referenceErrorMessage =
'ReferenceError: abc is not defined\n' +
' at Object.abc (/user/project/__tests__/example.test.js:25:12)\n' +
' at Promise.then.completed (/user/project/jest/packages/jest-circus/build/utils.js:333:28)\n' +
' at new Promise (<anonymous>)\n' +
' at callAsyncCircusFn (/user/project/jest/packages/jest-circus/build/utils.js:259:10)\n' +
' at _callCircusTest (/user/project/jest/packages/jest-circus/build/run.js:276:40)\n' +
' at processTicksAndRejections (node:internal/process/task_queues:95:5)\n' +
' at _runTest (/user/project/jest/packages/jest-circus/build/run.js:208:3)\n' +
' at _runTestsForDescribeBlock (/user/project/jest/packages/jest-circus/build/run.js:96:9)\n' +
' at _runTestsForDescribeBlock (/user/project/jest/packages/jest-circus/build/run.js:90:9)\n' +
' at run (/user/project/jest/packages/jest-circus/build/run.js:31:3)';

const testCaseResult = {
ancestorTitles: [] as Array<string>,
failureMessages: [expectationsErrorMessage],
title: 'some test',
} as TestCaseResult;

describe("passes test case report to '@actions/core'", () => {
test('when expect returns an error', () => {
reporter.onTestCaseResult(testMeta, {
...testCaseResult,
failureMessages: [expectationsErrorMessage],
});

expect(jest.mocked(process.stderr.write)).toBeCalledTimes(1);
expect(jest.mocked(process.stderr.write).mock.calls[0]).toMatchSnapshot();
});

test('when a test has reference error', () => {
reporter.onTestCaseResult(
{...testMeta, path: '/user/project/__tests__/example.test.js:25:12'},
{
...testCaseResult,
failureMessages: [referenceErrorMessage],
},
);

expect(jest.mocked(process.stderr.write)).toBeCalledTimes(1);
expect(jest.mocked(process.stderr.write).mock.calls[0]).toMatchSnapshot();
});

test('when test is wrapped in describe block', () => {
reporter.onTestCaseResult(testMeta, {
...testCaseResult,
ancestorTitles: ['describe'],
});

expect(jest.mocked(process.stderr.write)).toBeCalledTimes(1);
expect(jest.mocked(process.stderr.write).mock.calls[0]).toMatchSnapshot();
});

test('when test not is wrapped in describe block', () => {
reporter.onTestCaseResult(testMeta, testCaseResult);

expect(jest.mocked(process.stderr.write)).toBeCalledTimes(1);
expect(jest.mocked(process.stderr.write).mock.calls[0]).toMatchSnapshot();
});
});

This file was deleted.

@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`passes test case report to '@actions/core' when a test has reference error 1`] = `
Array [
"
::error file=/user/project/__tests__/example.test.js:25:12,line=25,title=some test::ReferenceError: abc is not defined%0A%0A at Object.abc (__tests__/example.test.js)
",
]
`;

exports[`passes test case report to '@actions/core' when expect returns an error 1`] = `
Array [
"
::error file=/user/project/__tests__/example.test.js,line=20,title=some test::expect(received).toBe(expected) // Object.is equality%0A%0AExpected: 1%0AReceived: 10%0A%0A at Object.toBe (__tests__/example.test.js:20:14)
",
]
`;

exports[`passes test case report to '@actions/core' when test is wrapped in describe block 1`] = `
Array [
"
::error file=/user/project/__tests__/example.test.js,line=20,title=describe › some test::expect(received).toBe(expected) // Object.is equality%0A%0AExpected: 1%0AReceived: 10%0A%0A at Object.toBe (__tests__/example.test.js:20:14)
",
]
`;

exports[`passes test case report to '@actions/core' when test not is wrapped in describe block 1`] = `
Array [
"
::error file=/user/project/__tests__/example.test.js,line=20,title=some test::expect(received).toBe(expected) // Object.is equality%0A%0AExpected: 1%0AReceived: 10%0A%0A at Object.toBe (__tests__/example.test.js:20:14)
",
]
`;
1 change: 1 addition & 0 deletions packages/jest-reporters/tsconfig.json
Expand Up @@ -8,6 +8,7 @@
"exclude": ["./**/__tests__/**/*"],
"references": [
{"path": "../jest-console"},
{"path": "../jest-message-util"},
{"path": "../jest-resolve"},
{"path": "../jest-test-result"},
{"path": "../jest-transform"},
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Expand Up @@ -2767,6 +2767,7 @@ __metadata:
istanbul-lib-report: ^3.0.0
istanbul-lib-source-maps: ^4.0.0
istanbul-reports: ^3.1.3
jest-message-util: ^28.1.0
jest-resolve: ^28.1.0
jest-util: ^28.1.0
jest-worker: ^28.1.0
Expand Down