diff --git a/CHANGELOG.md b/CHANGELOG.md index acaed9f5f311..bee7bad05887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - `[jest-mock]` [**BREAKING**] Improve the usage of `jest.fn` generic type argument ([#12489](https://github.com/facebook/jest/pull/12489)) - `[jest-mock]` Add support for auto-mocking async generator functions ([#11080](https://github.com/facebook/jest/pull/11080)) - `[jest-mock]` Add `contexts` member to mock functions ([#12601](https://github.com/facebook/jest/pull/12601)) +- `[jest-reporters]` Add GitHub Actions reporter ([#11320](https://github.com/facebook/jest/pull/11320)) - `[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)) - `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392)) - `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540)) diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts new file mode 100644 index 000000000000..dd63977df91d --- /dev/null +++ b/packages/jest-reporters/src/GitHubActionsReporter.ts @@ -0,0 +1,54 @@ +/** + * 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 stripAnsi = require('strip-ansi'); +import type {AggregatedResult, TestResult} from '@jest/test-result'; +import BaseReporter from './BaseReporter'; +import type {Context} from './types'; + +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); +} + +export default class GitHubActionsReporter extends BaseReporter { + onRunComplete( + _contexts?: Set, + aggregatedResults?: AggregatedResult, + ): void { + const messages = getMessages(aggregatedResults?.testResults); + + for (const message of messages) { + this.log(message); + } + } +} + +function getMessages(results: Array | 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]) => + `::error file=${testFilePath},line=${line},col=${col}::${message}`, + ), + ); +} diff --git a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js new file mode 100644 index 000000000000..b7939b579458 --- /dev/null +++ b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js @@ -0,0 +1,118 @@ +/** + * 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. + */ +'use strict'; + +let GitHubActionsReporter; + +const write = process.stderr.write; +const globalConfig = { + rootDir: 'root', + watch: false, +}; + +let results = []; + +function requireReporter() { + jest.isolateModules(() => { + GitHubActionsReporter = require('../GitHubActionsReporter').default; + }); +} + +beforeEach(() => { + process.stderr.write = result => results.push(result); +}); + +afterEach(() => { + results = []; + process.stderr.write = write; +}); + +const aggregatedResults = { + numFailedTestSuites: 1, + numFailedTests: 1, + numPassedTestSuites: 0, + numTotalTestSuites: 1, + numTotalTests: 1, + snapshot: { + added: 0, + didUpdate: false, + failure: false, + filesAdded: 0, + filesRemoved: 0, + filesRemovedList: [], + filesUnmatched: 0, + filesUpdated: 0, + matched: 0, + total: 0, + unchecked: 0, + uncheckedKeysByFile: [], + unmatched: 0, + updated: 0, + }, + startTime: 0, + success: false, + testResults: [ + { + numFailingTests: 1, + numPassingTests: 0, + numPendingTests: 0, + numTodoTests: 0, + openHandles: [], + perfStats: { + end: 1234, + runtime: 1234, + slow: false, + start: 0, + }, + skipped: false, + snapshot: { + added: 0, + fileDeleted: false, + matched: 0, + unchecked: 0, + uncheckedKeys: [], + unmatched: 0, + updated: 0, + }, + testFilePath: '/home/runner/work/jest/jest/some.test.js', + testResults: [ + { + ancestorTitles: [Array], + duration: 7, + failureDetails: [Array], + failureMessages: [ + ` + 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 + \n + Expected: \u001b[32m\"b\"\u001b[39m\n + Received: \u001b[31m\"a\"\u001b[39m\n + at Object. (/home/runner/work/jest/jest/some.test.js:4:17)\n + at Object.asyncJestTest (/home/runner/work/jest/jest/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:106:37)\n + at /home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:45:12\n + at new Promise ()\n + at mapper (/home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:28:19)\n + at /home/runner/work/jest/jest/node_modules/jest-jasmine2/build/queueRunner.js:75:41\n + at processTicksAndRejections (internal/process/task_queues.js:93:5) + `, + ], + fullName: 'asserts that a === b', + location: null, + numPassingAsserts: 0, + status: 'failed', + title: 'asserts that a === b', + }, + ], + }, + ], +}; + +test('reporter extracts the correct filename, line, and column', () => { + requireReporter(); + const testReporter = new GitHubActionsReporter(globalConfig); + testReporter.onRunComplete(new Set(), aggregatedResults); + expect(results.join('').replace(/\\/g, '/')).toMatchSnapshot(); +}); diff --git a/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.js.snap b/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.js.snap new file mode 100644 index 000000000000..c523c4dd3448 --- /dev/null +++ b/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.js.snap @@ -0,0 +1,6 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`reporter extracts the correct filename, line, and column 1`] = ` +"::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. (/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 ()%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 +" +`; diff --git a/packages/jest-reporters/src/index.ts b/packages/jest-reporters/src/index.ts index 9185df007f1b..3f8089c3b689 100644 --- a/packages/jest-reporters/src/index.ts +++ b/packages/jest-reporters/src/index.ts @@ -26,6 +26,7 @@ export {default as DefaultReporter} from './DefaultReporter'; export {default as NotifyReporter} from './NotifyReporter'; export {default as SummaryReporter} from './SummaryReporter'; export {default as VerboseReporter} from './VerboseReporter'; +export {default as GitHubActionsReporter} from './GitHubActionsReporter'; export type { Context, Reporter,