Skip to content

Commit b9af518

Browse files
ockhammariusGundersenStefan Buck
authored
feat: Add GitHub Actions Reporter (#11320)
Co-authored-by: Marius Gundersen <[email protected]> Co-authored-by: Stefan Buck <[email protected]>
1 parent 1ba867b commit b9af518

File tree

5 files changed

+180
-0
lines changed

5 files changed

+180
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- `[jest-mock]` [**BREAKING**] Improve the usage of `jest.fn` generic type argument ([#12489](https://github.com/facebook/jest/pull/12489))
3131
- `[jest-mock]` Add support for auto-mocking async generator functions ([#11080](https://github.com/facebook/jest/pull/11080))
3232
- `[jest-mock]` Add `contexts` member to mock functions ([#12601](https://github.com/facebook/jest/pull/12601))
33+
- `[jest-reporters]` Add GitHub Actions reporter ([#11320](https://github.com/facebook/jest/pull/11320))
3334
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
3435
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
3536
- `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540))
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import stripAnsi = require('strip-ansi');
9+
import type {AggregatedResult, TestResult} from '@jest/test-result';
10+
import BaseReporter from './BaseReporter';
11+
import type {Context} from './types';
12+
13+
const lineAndColumnInStackTrace = /^.*?:([0-9]+):([0-9]+).*$/;
14+
15+
function replaceEntities(s: string): string {
16+
// https://github.com/actions/toolkit/blob/b4639928698a6bfe1c4bdae4b2bfdad1cb75016d/packages/core/src/command.ts#L80-L85
17+
const substitutions: Array<[RegExp, string]> = [
18+
[/%/g, '%25'],
19+
[/\r/g, '%0D'],
20+
[/\n/g, '%0A'],
21+
];
22+
return substitutions.reduce((acc, sub) => acc.replace(...sub), s);
23+
}
24+
25+
export default class GitHubActionsReporter extends BaseReporter {
26+
onRunComplete(
27+
_contexts?: Set<Context>,
28+
aggregatedResults?: AggregatedResult,
29+
): void {
30+
const messages = getMessages(aggregatedResults?.testResults);
31+
32+
for (const message of messages) {
33+
this.log(message);
34+
}
35+
}
36+
}
37+
38+
function getMessages(results: Array<TestResult> | undefined) {
39+
if (!results) return [];
40+
41+
return results.flatMap(({testFilePath, testResults}) =>
42+
testResults
43+
.filter(r => r.status === 'failed')
44+
.flatMap(r => r.failureMessages)
45+
.map(m => stripAnsi(m))
46+
.map(m => replaceEntities(m))
47+
.map(m => lineAndColumnInStackTrace.exec(m))
48+
.filter((m): m is RegExpExecArray => m !== null)
49+
.map(
50+
([message, line, col]) =>
51+
`::error file=${testFilePath},line=${line},col=${col}::${message}`,
52+
),
53+
);
54+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
'use strict';
8+
9+
let GitHubActionsReporter;
10+
11+
const write = process.stderr.write;
12+
const globalConfig = {
13+
rootDir: 'root',
14+
watch: false,
15+
};
16+
17+
let results = [];
18+
19+
function requireReporter() {
20+
jest.isolateModules(() => {
21+
GitHubActionsReporter = require('../GitHubActionsReporter').default;
22+
});
23+
}
24+
25+
beforeEach(() => {
26+
process.stderr.write = result => results.push(result);
27+
});
28+
29+
afterEach(() => {
30+
results = [];
31+
process.stderr.write = write;
32+
});
33+
34+
const aggregatedResults = {
35+
numFailedTestSuites: 1,
36+
numFailedTests: 1,
37+
numPassedTestSuites: 0,
38+
numTotalTestSuites: 1,
39+
numTotalTests: 1,
40+
snapshot: {
41+
added: 0,
42+
didUpdate: false,
43+
failure: false,
44+
filesAdded: 0,
45+
filesRemoved: 0,
46+
filesRemovedList: [],
47+
filesUnmatched: 0,
48+
filesUpdated: 0,
49+
matched: 0,
50+
total: 0,
51+
unchecked: 0,
52+
uncheckedKeysByFile: [],
53+
unmatched: 0,
54+
updated: 0,
55+
},
56+
startTime: 0,
57+
success: false,
58+
testResults: [
59+
{
60+
numFailingTests: 1,
61+
numPassingTests: 0,
62+
numPendingTests: 0,
63+
numTodoTests: 0,
64+
openHandles: [],
65+
perfStats: {
66+
end: 1234,
67+
runtime: 1234,
68+
slow: false,
69+
start: 0,
70+
},
71+
skipped: false,
72+
snapshot: {
73+
added: 0,
74+
fileDeleted: false,
75+
matched: 0,
76+
unchecked: 0,
77+
uncheckedKeys: [],
78+
unmatched: 0,
79+
updated: 0,
80+
},
81+
testFilePath: '/home/runner/work/jest/jest/some.test.js',
82+
testResults: [
83+
{
84+
ancestorTitles: [Array],
85+
duration: 7,
86+
failureDetails: [Array],
87+
failureMessages: [
88+
`
89+
Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n
90+
\n
91+
Expected: \u001b[32m\"b\"\u001b[39m\n
92+
Received: \u001b[31m\"a\"\u001b[39m\n
93+
at Object.<anonymous> (/home/runner/work/jest/jest/some.test.js:4:17)\n
94+
at Object.asyncJestTest (/home/runner/work/jest/jest/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:106:37)\n
95+
at /home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:45:12\n
96+
at new Promise (<anonymous>)\n
97+
at mapper (/home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:28:19)\n
98+
at /home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:75:41\n
99+
at processTicksAndRejections (internal/process/task_queues.js:93:5)
100+
`,
101+
],
102+
fullName: 'asserts that a === b',
103+
location: null,
104+
numPassingAsserts: 0,
105+
status: 'failed',
106+
title: 'asserts that a === b',
107+
},
108+
],
109+
},
110+
],
111+
};
112+
113+
test('reporter extracts the correct filename, line, and column', () => {
114+
requireReporter();
115+
const testReporter = new GitHubActionsReporter(globalConfig);
116+
testReporter.onRunComplete(new Set(), aggregatedResults);
117+
expect(results.join('').replace(/\\/g, '/')).toMatchSnapshot();
118+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`reporter extracts the correct filename, line, and column 1`] = `
4+
"::error file=/home/runner/work/jest/jest/some.test.js,line=4,col=17::%0A Error: expect(received).toBe(expected) // Object.is equality%0A%0A %0A%0A Expected: "b"%0A%0A Received: "a"%0A%0A at Object.<anonymous> (/home/runner/work/jest/jest/some.test.js:4:17)%0A%0A at Object.asyncJestTest (/home/runner/work/jest/jest/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:106:37)%0A%0A at /home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:45:12%0A%0A at new Promise (<anonymous>)%0A%0A at mapper (/home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:28:19)%0A%0A at /home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:75:41%0A%0A at processTicksAndRejections (internal/process/task_queues.js:93:5)%0A
5+
"
6+
`;

packages/jest-reporters/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {default as DefaultReporter} from './DefaultReporter';
2626
export {default as NotifyReporter} from './NotifyReporter';
2727
export {default as SummaryReporter} from './SummaryReporter';
2828
export {default as VerboseReporter} from './VerboseReporter';
29+
export {default as GitHubActionsReporter} from './GitHubActionsReporter';
2930
export type {
3031
Context,
3132
Reporter,

0 commit comments

Comments
 (0)