Repository: jestjs/jest (TypeScript)

Issue:
feat: Added GithubActionsReporter
## Summary

This is based on recommendation and work done in https://github.com/stefanbuck/jest-matcher/pull/2

## Test plan

Testing is not implemented yet, this is just a placeholder pr



Hints:
Hi @mariusGundersen! 

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file.

In order for us to review and merge your code, please sign at <https://code.facebook.com/cla>. **If you are contributing on behalf of someone else (eg your employer)**, the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

If you have received this in error or have any questions, please contact us at [cla@fb.com](mailto:cla@fb.com?subject=CLA%20for%20facebook%2Fjest%20%2310741). Thanks!
👋  Any news on this? This would be a great addition to Jest 🎉 

@mariusGundersen, are you still working on this? Seems like there are a couple of style issues that have been flagged by the corresponding GH Action. It'd be great if these could be fixed and the PR could be set to 'Ready for review' 😊 🙏 
I think it should be enough to run eslint to fix those. I'm mainly waiting for tests 🙂 

Patch:
diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts
new file mode 100644
--- /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<Context>,
+    aggregatedResults?: AggregatedResult,
+  ): void {
+    const messages = getMessages(aggregatedResults?.testResults);
+
+    for (const message of messages) {
+      this.log(message);
+    }
+  }
+}
+
+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]) =>
+          `::error file=${testFilePath},line=${line},col=${col}::${message}`,
+      ),
+  );
+}
diff --git a/packages/jest-reporters/src/index.ts b/packages/jest-reporters/src/index.ts
--- 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,


Test patch:
diff --git a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js
new file mode 100644
--- /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.<anonymous> (/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 (<anonymous>)\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
--- /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.<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            
+"
+`;