From d300a4dbca1d14e2e2faf9dbee543d976821bd99 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 22 Nov 2022 10:36:48 +0100 Subject: [PATCH 01/19] Port implementation for Github Actions logs folding Ported from https://github.com/MatteoH2O1999/github-actions-jest-reporter --- packages/jest-core/src/TestScheduler.ts | 13 +- .../src/GithubActionsLogsReporter.ts | 327 ++++++++++++++++++ packages/jest-reporters/src/index.ts | 1 + 3 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 packages/jest-reporters/src/GithubActionsLogsReporter.ts diff --git a/packages/jest-core/src/TestScheduler.ts b/packages/jest-core/src/TestScheduler.ts index ad96154c2fbb..58a090adbdbc 100644 --- a/packages/jest-core/src/TestScheduler.ts +++ b/packages/jest-core/src/TestScheduler.ts @@ -12,6 +12,7 @@ import { CoverageReporter, DefaultReporter, GitHubActionsReporter, + GithubActionsLogsReporter, BaseReporter as JestReporter, NotifyReporter, Reporter, @@ -342,9 +343,15 @@ class TestScheduler { switch (reporter) { case 'default': summary = true; - verbose - ? this.addReporter(new VerboseReporter(this._globalConfig)) - : this.addReporter(new DefaultReporter(this._globalConfig)); + if (verbose) { + this.addReporter(new VerboseReporter(this._globalConfig)); + } else { + GITHUB_ACTIONS + ? this.addReporter( + new GithubActionsLogsReporter(this._globalConfig), + ) + : this.addReporter(new DefaultReporter(this._globalConfig)); + } break; case 'github-actions': GITHUB_ACTIONS && this.addReporter(new GitHubActionsReporter()); diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts new file mode 100644 index 000000000000..7ff26254919d --- /dev/null +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -0,0 +1,327 @@ +import chalk = require('chalk'); +import type { + AggregatedResult, + AssertionResult, + Test, + TestContext, + TestResult, +} from '@jest/test-result'; +import DefaultReporter from './DefaultReporter'; + +type performaceInfo = { + end: number; + runtime: number; + slow: boolean; + start: number; +}; + +type resultTreeLeaf = { + name: string; + passed: boolean; + duration: number; + children: Array; +}; + +type resultTreeNode = { + name: string; + passed: boolean; + children: Array; +}; + +type resultTree = { + children: Array; + name: string; + passed: boolean; + performanceInfo: performaceInfo; +}; + +export default class GithubActionsLogsReporter extends DefaultReporter { + override onTestResult( + test: Test, + testResult: TestResult, + aggregatedResults: AggregatedResult, + ): void { + this.__printFullResult(test.context, testResult); + if (this.__isLastTestSuite(aggregatedResults)) { + this.log(''); + if (this.__printFailedTestLogs(test, aggregatedResults)) { + this.log(''); + } + } + } + + __isLastTestSuite(results: AggregatedResult): boolean { + const passedTestSuites = results.numPassedTestSuites; + const failedTestSuites = results.numFailedTestSuites; + const totalTestSuites = results.numTotalTestSuites; + const computedTotal = passedTestSuites + failedTestSuites; + if (computedTotal < totalTestSuites) { + return false; + } else if (computedTotal === totalTestSuites) { + return true; + } else { + throw new Error( + `Sum(${computedTotal}) of passed (${passedTestSuites}) and failed (${failedTestSuites}) test suites is greater than the total number of test suites (${totalTestSuites}). Please report the bug at https://github.com/MatteoH2O1999/github-actions-jest-reporter/issues`, + ); + } + } + + __printFullResult(context: TestContext, results: TestResult): void { + const rootDir = context.config.rootDir; + let testDir = results.testFilePath.replace(rootDir, ''); + testDir = testDir.slice(1, testDir.length); + const resultTree = this.__getResultTree( + results.testResults, + testDir, + results.perfStats, + ); + this.__printResultTree(resultTree); + } + + __arrayEqual(a1: Array, a2: Array): boolean { + if (a1.length !== a2.length) { + return false; + } + for (let index = 0; index < a1.length; index++) { + const element = a1[index]; + if (element !== a2[index]) { + return false; + } + } + return true; + } + + __arrayChild(a1: Array, a2: Array): boolean { + if (a1.length - a2.length !== 1) { + return false; + } + for (let index = 0; index < a2.length; index++) { + const element = a2[index]; + if (element !== a1[index]) { + return false; + } + } + return true; + } + + __getResultTree( + suiteResult: Array, + testPath: string, + suitePerf: performaceInfo, + ): resultTree { + const root: resultTree = { + children: [], + name: testPath, + passed: true, + performanceInfo: suitePerf, + }; + const branches: Array> = []; + suiteResult.forEach(element => { + if (element.ancestorTitles.length === 0) { + let passed = true; + if (element.status === 'failed') { + root.passed = false; + passed = false; + } else if (element.status !== 'passed') { + throw new Error( + `Expected status to be 'failed' or 'passed', got ${element.status}`, + ); + } + if (!element.duration || isNaN(element.duration)) { + throw new Error('Expected duration to be a number, got NaN'); + } + root.children.push({ + children: [], + duration: Math.max(element.duration, 1), + name: element.title, + passed, + }); + } else { + let alreadyInserted = false; + for (let index = 0; index < branches.length; index++) { + if ( + this.__arrayEqual( + branches[index], + element.ancestorTitles.slice(0, 1), + ) + ) { + alreadyInserted = true; + break; + } + } + if (!alreadyInserted) { + branches.push(element.ancestorTitles.slice(0, 1)); + } + } + }); + branches.forEach(element => { + const newChild = this.__getResultChildren(suiteResult, element); + if (!newChild.passed) { + root.passed = false; + } + root.children.push(newChild); + }); + return root; + } + + __getResultChildren( + suiteResult: Array, + ancestors: Array, + ): resultTreeNode { + const node: resultTreeNode = { + children: [], + name: ancestors[ancestors.length - 1], + passed: true, + }; + const branches: Array> = []; + suiteResult.forEach(element => { + let passed = true; + let duration = element.duration; + if (!duration || isNaN(duration)) { + duration = 1; + } + if (this.__arrayEqual(element.ancestorTitles, ancestors)) { + if (element.status === 'failed') { + node.passed = false; + passed = false; + } + node.children.push({ + children: [], + duration, + name: element.title, + passed, + }); + } else if ( + this.__arrayChild( + element.ancestorTitles.slice(0, ancestors.length + 1), + ancestors, + ) + ) { + let alreadyInserted = false; + for (let index = 0; index < branches.length; index++) { + if ( + this.__arrayEqual( + branches[index], + element.ancestorTitles.slice(0, ancestors.length + 1), + ) + ) { + alreadyInserted = true; + break; + } + } + if (!alreadyInserted) { + branches.push(element.ancestorTitles.slice(0, ancestors.length + 1)); + } + } + }); + branches.forEach(element => { + const newChild = this.__getResultChildren(suiteResult, element); + if (!newChild.passed) { + node.passed = false; + } + node.children.push(newChild); + }); + return node; + } + + __printResultTree(resultTree: resultTree): void { + let perfMs; + if (resultTree.performanceInfo.slow) { + perfMs = ` (${chalk.red.inverse( + `${resultTree.performanceInfo.runtime} ms`, + )})`; + } else { + perfMs = ` (${resultTree.performanceInfo.runtime} ms)`; + } + if (resultTree.passed) { + this.__startGroup( + `${chalk.bold.green.inverse('PASS')} ${resultTree.name}${perfMs}`, + ); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, true, 1); + }); + this.__endGroup(); + } else { + this.log( + ` ${chalk.bold.red.inverse('FAIL')} ${resultTree.name}${perfMs}`, + ); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, false, 1); + }); + } + } + + __recursivePrintResultTree( + resultTree: resultTreeNode | resultTreeLeaf, + alreadyGrouped: boolean, + depth: number, + ): void { + if (resultTree.children.length === 0) { + if (!('duration' in resultTree)) { + throw new Error('Expected a leaf. Got a node.'); + } + let numberSpaces = depth; + if (!alreadyGrouped) { + numberSpaces++; + } + const spaces = ' '.repeat(numberSpaces); + let resultSymbol; + if (resultTree.passed) { + resultSymbol = chalk.green('\u2713'); + } else { + resultSymbol = chalk.red('\u00D7'); + } + this.log( + `${spaces + resultSymbol} ${resultTree.name} (${ + resultTree.duration + } ms)`, + ); + } else { + if (resultTree.passed) { + if (alreadyGrouped) { + this.log(' '.repeat(depth) + resultTree.name); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, true, depth + 1); + }); + } else { + this.__startGroup(' '.repeat(depth) + resultTree.name); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, true, depth + 1); + }); + this.__endGroup(); + } + } else { + this.log(' '.repeat(depth + 1) + resultTree.name); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, false, depth + 1); + }); + } + } + } + + __printFailedTestLogs(context: Test, testResults: AggregatedResult): boolean { + const rootDir = context.context.config.rootDir; + const results = testResults.testResults; + let written = false; + results.forEach(result => { + let testDir = result.testFilePath; + testDir = testDir.replace(rootDir, ''); + testDir = testDir.slice(1, testDir.length); + if (result.failureMessage) { + written = true; + this.__startGroup(`Errors thrown in ${testDir}`); + this.log(result.failureMessage); + this.__endGroup(); + } + }); + return written; + } + + __startGroup(title: string): void { + this.log(`::group::${title}`); + } + + __endGroup(): void { + this.log('::endgroup::'); + } +} diff --git a/packages/jest-reporters/src/index.ts b/packages/jest-reporters/src/index.ts index 61f3cde17739..ef52469cfc6d 100644 --- a/packages/jest-reporters/src/index.ts +++ b/packages/jest-reporters/src/index.ts @@ -30,6 +30,7 @@ export {default as GitHubActionsReporter} from './GitHubActionsReporter'; export {default as NotifyReporter} from './NotifyReporter'; export {default as SummaryReporter} from './SummaryReporter'; export {default as VerboseReporter} from './VerboseReporter'; +export {default as GithubActionsLogsReporter} from './GithubActionsLogsReporter'; export type { Reporter, ReporterOnStartOptions, From 10bada4cda5bd247c44a610043ac614919bef132 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:20:01 +0100 Subject: [PATCH 02/19] Update thrown error --- packages/jest-reporters/src/GithubActionsLogsReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts index 7ff26254919d..b5eafdee641d 100644 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -61,7 +61,7 @@ export default class GithubActionsLogsReporter extends DefaultReporter { return true; } else { throw new Error( - `Sum(${computedTotal}) of passed (${passedTestSuites}) and failed (${failedTestSuites}) test suites is greater than the total number of test suites (${totalTestSuites}). Please report the bug at https://github.com/MatteoH2O1999/github-actions-jest-reporter/issues`, + `Sum(${computedTotal}) of passed (${passedTestSuites}) and failed (${failedTestSuites}) test suites is greater than the total number of test suites (${totalTestSuites}). Please report the bug at https://github.com/facebook/jest/issues`, ); } } From c8f61132ac0ead728d6ae2820edbbfbdf9cbe932 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:20:39 +0100 Subject: [PATCH 03/19] Override log to ensure \n after each call --- packages/jest-reporters/src/GithubActionsLogsReporter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts index b5eafdee641d..8618210027c8 100644 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -324,4 +324,8 @@ export default class GithubActionsLogsReporter extends DefaultReporter { __endGroup(): void { this.log('::endgroup::'); } + + override log(message: string): void { + super.log(`${message}\n`); + } } From b135cbef05777b6d8c7f0440f350903282a230dc Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:21:01 +0100 Subject: [PATCH 04/19] Ensure parent methods do nothing --- .../src/GithubActionsLogsReporter.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts index 8618210027c8..b65e05859158 100644 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -50,6 +50,31 @@ export default class GithubActionsLogsReporter extends DefaultReporter { } } + // eslint-disable-next-line @typescript-eslint/no-empty-function + override onRunComplete(): void {} + + override onRunStart( + _aggregatedResults: AggregatedResult, + _options: ReporterOnStartOptions, + // eslint-disable-next-line @typescript-eslint/no-empty-function + ): void {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + override onTestStart(_test: Test): void {} + + override onTestCaseResult( + _test: Test, + _testCaseResult: AssertionResult, + // eslint-disable-next-line @typescript-eslint/no-empty-function + ): void {} + + override testFinished( + _config: ProjectConfig, + _testResult: TestResult, + _aggregatedResults: AggregatedResult, + // eslint-disable-next-line @typescript-eslint/no-empty-function + ): void {} + __isLastTestSuite(results: AggregatedResult): boolean { const passedTestSuites = results.numPassedTestSuites; const failedTestSuites = results.numFailedTestSuites; From 927e062f7d4a49286ac5fe0a9f0f86adc1f9cb84 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:21:30 +0100 Subject: [PATCH 05/19] Update header and copied from --- .../jest-reporters/src/GithubActionsLogsReporter.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts index b65e05859158..498de96caef9 100644 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -1,3 +1,11 @@ +/** + * 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 {ProjectConfig} from '@jest/types/src/Config'; import chalk = require('chalk'); import type { AggregatedResult, @@ -7,6 +15,7 @@ import type { TestResult, } from '@jest/test-result'; import DefaultReporter from './DefaultReporter'; +import type {ReporterOnStartOptions} from './types'; type performaceInfo = { end: number; @@ -36,6 +45,7 @@ type resultTree = { }; export default class GithubActionsLogsReporter extends DefaultReporter { + // copied from https://github.com/MatteoH2O1999/github-actions-jest-reporter/blob/master/src/gha.reporter.js override onTestResult( test: Test, testResult: TestResult, From 1480f633d6323140d7bba080e703bb1748b2e18f Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:30:26 +0100 Subject: [PATCH 06/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f85d3f887a47..55678153ced7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `[jest-runtime]` Add `jest.isEnvironmentTornDown` function ([#13698](https://github.com/facebook/jest/pull/13698)) - `[jest-test-result]` Added `skipped` and `focused` status to `FormattedTestResult` ([#13700](https://github.com/facebook/jest/pull/13700)) - `[jest-transform]` Support for asynchronous `createTransformer` ([#13762](https://github.com/facebook/jest/pull/13762)) +- `[@jest/reporters]` Add new reporter for Github Actions runs with automatic log folding and use it as default when in a Github Actions environment ([#13626](https://github.com/facebook/jest/pull/13626)) ### Fixes From d9ed7747b70118dccf4c335c08fee565d0c63e6e Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:51:08 +0100 Subject: [PATCH 07/19] Add unit tests Copied from https://github.com/MatteoH2O1999/github-actions-jest-reporter --- .../GithubActionsLogsReporter.test.js | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js diff --git a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js new file mode 100644 index 000000000000..507c6ccc83ce --- /dev/null +++ b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js @@ -0,0 +1,416 @@ +/** + * 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. + */ + +// copied from https://github.com/MatteoH2O1999/github-actions-jest-reporter/blob/master/tests/gha.reporter.test.js + +import util from 'util'; +import chalk from 'chalk'; +import {beforeEach, describe, expect, jest, test} from '@jest/globals'; +import BaseReporter from '../BaseReporter'; +import GhaReporter from '../GithubActionsLogsReporter'; + +const xSymbol = '\u00D7'; +const ySymbol = '\u2713'; + +let consoleLog; +const mockLog = jest + .spyOn(BaseReporter.prototype, 'log') + .mockImplementation(message => { + consoleLog = consoleLog.concat(message); + }); + +beforeEach(() => { + consoleLog = ''; +}); + +test('can be instantiated', () => { + const gha = new GhaReporter(); + expect(gha).toBeTruthy(); + expect(gha).toBeInstanceOf(GhaReporter); +}); + +describe('Result tree generation', () => { + test('failed single test without describe', () => { + const testResults = [ + { + ancestorTitles: [], + duration: 10, + status: 'failed', + title: 'test', + }, + ]; + const testContext = {}; + const suitePerf = { + runtime: 20, + slow: false, + }; + const expectedResults = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const gha = new GhaReporter(); + + const generated = gha.__getResultTree(testResults, '/', suitePerf); + + expect(consoleLog).toBe(''); + expect(generated).toEqual(expectedResults); + }); + + test('passed single test without describe', () => { + const testResults = [ + { + ancestorTitles: [], + duration: 10, + status: 'passed', + title: 'test', + }, + ]; + const testContext = {}; + const suitePerf = { + runtime: 20, + slow: false, + }; + const expectedResults = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const gha = new GhaReporter(); + + const generated = gha.__getResultTree(testResults, '/', suitePerf); + + expect(consoleLog).toBe(''); + expect(generated).toEqual(expectedResults); + }); + + test('failed single test inside describe', () => { + const testResults = [ + { + ancestorTitles: ['Test describe'], + duration: 10, + status: 'failed', + title: 'test', + }, + ]; + const testContext = {}; + const suitePerf = { + runtime: 20, + slow: false, + }; + const expectedResults = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: 'Test describe', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const gha = new GhaReporter(); + + const generated = gha.__getResultTree(testResults, '/', suitePerf); + + expect(consoleLog).toBe(''); + expect(generated).toEqual(expectedResults); + }); + + test('passed single test inside describe', () => { + const testResults = [ + { + ancestorTitles: ['Test describe'], + duration: 10, + status: 'passed', + title: 'test', + }, + ]; + const testContext = {}; + const suitePerf = { + runtime: 20, + slow: false, + }; + const expectedResults = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: 'Test describe', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const gha = new GhaReporter(); + + const generated = gha.__getResultTree(testResults, '/', suitePerf); + + expect(consoleLog).toBe(''); + expect(generated).toEqual(expectedResults); + }); +}); + +describe('Result tree output', () => { + test('failed single test without describe', () => { + const generatedTree = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const testContext = {}; + const expectedOutput = ` ${chalk.bold.red.inverse( + 'FAIL', + )} / (20 ms)\n ${chalk.red(xSymbol)} test (10 ms)\n`; + const gha = new GhaReporter(); + + gha.__printResultTree(generatedTree); + + expect(consoleLog).toEqual(expectedOutput); + }); + + test('passed single test without describe', () => { + const generatedTree = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const testContext = {}; + const expectedOutput = `::group::${chalk.bold.green.inverse( + 'PASS', + )} / (20 ms)\n ${chalk.green(ySymbol)} test (10 ms)\n::endgroup::\n`; + const gha = new GhaReporter(); + + gha.__printResultTree(generatedTree); + + expect(consoleLog).toEqual(expectedOutput); + }); + + test('failed single test inside describe', () => { + const generatedTree = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: 'Test describe', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const testContext = {}; + const expectedOutput = + ` ${chalk.bold.red.inverse('FAIL')} / (20 ms)\n` + + ' Test describe\n' + + ` ${chalk.red(xSymbol)} test (10 ms)\n`; + const gha = new GhaReporter(); + + gha.__printResultTree(generatedTree); + + expect(consoleLog).toEqual(expectedOutput); + }); + + test('passed single test inside describe', () => { + const generatedTree = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: 'Test describe', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + runtime: 20, + slow: false, + }, + }; + const testContext = {}; + const expectedOutput = + `::group::${chalk.bold.green.inverse('PASS')} / (20 ms)\n` + + ' Test describe\n' + + ` ${chalk.green(ySymbol)} test (10 ms)\n` + + '::endgroup::\n'; + const gha = new GhaReporter(); + + gha.__printResultTree(generatedTree); + + expect(consoleLog).toEqual(expectedOutput); + }); +}); + +describe('Reporter interface', () => { + test('onTestResult not last', () => { + const mockTest = { + context: { + config: { + rootDir: '/testDir', + }, + }, + }; + const mockTestResult = { + perfStats: { + runtime: 20, + slow: false, + }, + testFilePath: '/testDir/test1.js', + testResults: [ + { + ancestorTitles: [], + duration: 10, + status: 'passed', + title: 'test1', + }, + ], + }; + const mockResults = { + numFailedTestSuites: 1, + numPassedTestSuites: 1, + numTotalTestSuites: 3, + }; + const expectedOutput = + `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)\n` + + ` ${chalk.green(ySymbol)} test1 (10 ms)\n` + + '::endgroup::\n'; + const gha = new GhaReporter(); + + gha.onTestResult(mockTest, mockTestResult, mockResults); + + expect(consoleLog).toEqual(expectedOutput); + }); + + test('onTestResult last', () => { + const mockTest = { + context: { + config: { + rootDir: '/testDir', + }, + }, + }; + const mockTestResult = { + failureMessage: 'Failure message', + perfStats: { + runtime: 20, + slow: false, + }, + testFilePath: '/testDir/test1.js', + testResults: [ + { + ancestorTitles: [], + duration: 10, + status: 'passed', + title: 'test1', + }, + ], + }; + const mockResults = { + numFailedTestSuites: 1, + numPassedTestSuites: 2, + numTotalTestSuites: 3, + testResults: [mockTestResult], + }; + const expectedOutput = + `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)\n` + + ` ${chalk.green(ySymbol)} test1 (10 ms)\n` + + '::endgroup::\n' + + '\n' + + '::group::Errors thrown in test1.js\n' + + 'Failure message\n' + + '::endgroup::\n' + + '\n'; + const gha = new GhaReporter(); + + gha.onTestResult(mockTest, mockTestResult, mockResults); + + expect(consoleLog).toEqual(expectedOutput); + }); +}); From 9004e0f01ca4e2a1de3eb72513d5ab912dce2f85 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Thu, 24 Nov 2022 11:50:41 +0100 Subject: [PATCH 08/19] Add unit tests for new default reporter --- .../src/__tests__/TestScheduler.test.js | 206 +++++++++++++++++- 1 file changed, 203 insertions(+), 3 deletions(-) diff --git a/packages/jest-core/src/__tests__/TestScheduler.test.js b/packages/jest-core/src/__tests__/TestScheduler.test.js index d50517f34104..8a5adc441361 100644 --- a/packages/jest-core/src/__tests__/TestScheduler.test.js +++ b/packages/jest-core/src/__tests__/TestScheduler.test.js @@ -10,6 +10,7 @@ import { CoverageReporter, DefaultReporter, GitHubActionsReporter, + GithubActionsLogsReporter, NotifyReporter, SummaryReporter, VerboseReporter, @@ -59,13 +60,203 @@ beforeEach(() => { spyShouldRunInBand.mockClear(); }); -describe('reporters', () => { +describe('reporters with GITHUB_ACTIONS = true', () => { const CustomReporter = require('/custom-reporter.js'); afterEach(() => { jest.clearAllMocks(); }); + test('works with default value', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: undefined, + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('does not enable any reporters, if empty list is passed', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: [], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(0); + }); + + test('sets up default reporters', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: [['default', {}]], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('sets up verbose reporter', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: [['default', {}]], + verbose: true, + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); + expect(VerboseReporter).toHaveBeenCalledTimes(1); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('sets up github actions reporter', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: [ + ['default', {}], + ['github-actions', {}], + ], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(1); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('sets up notify reporter', async () => { + await createTestScheduler( + makeGlobalConfig({ + notify: true, + reporters: [['default', {}]], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(1); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('sets up coverage reporter', async () => { + await createTestScheduler( + makeGlobalConfig({ + collectCoverage: true, + reporters: [['default', {}]], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(1); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('allows enabling summary reporter separately', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: [['summary', {}]], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + }); + + test('sets up custom reporter', async () => { + await createTestScheduler( + makeGlobalConfig({ + reporters: [ + ['default', {}], + ['/custom-reporter.js', {}], + ], + }), + {}, + {}, + ); + + expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); + expect(VerboseReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(NotifyReporter).toHaveBeenCalledTimes(0); + expect(CoverageReporter).toHaveBeenCalledTimes(0); + expect(SummaryReporter).toHaveBeenCalledTimes(1); + expect(CustomReporter).toHaveBeenCalledTimes(1); + }); +}); + +describe('reporters with GITHUB_ACTIONS = false', () => { + const CustomReporter = require('/custom-reporter.js'); + + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(() => { + const ci = require('ci-info'); + ci.GITHUB_ACTIONS = false; + }); + + afterAll(() => { + const ci = require('ci-info'); + ci.GITHUB_ACTIONS = true; + }); + test('works with default value', async () => { await createTestScheduler( makeGlobalConfig({ @@ -76,6 +267,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -93,6 +285,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -110,6 +303,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -128,6 +322,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(1); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -135,7 +330,7 @@ describe('reporters', () => { expect(SummaryReporter).toHaveBeenCalledTimes(1); }); - test('sets up github actions reporter', async () => { + test('does not set up github actions reporter', async () => { await createTestScheduler( makeGlobalConfig({ reporters: [ @@ -148,8 +343,9 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(1); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); expect(CoverageReporter).toHaveBeenCalledTimes(0); expect(SummaryReporter).toHaveBeenCalledTimes(1); @@ -166,6 +362,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(1); @@ -184,6 +381,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -201,6 +399,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(0); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -221,6 +420,7 @@ describe('reporters', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); + expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); From 560e072af6134b3b10d2e94ab3f70cee4028ad3b Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Mon, 12 Dec 2022 15:37:22 +0100 Subject: [PATCH 09/19] Remove log override from class and relative test correction --- .../src/GithubActionsLogsReporter.ts | 4 -- .../GithubActionsLogsReporter.test.js | 38 +++++++++---------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts index 498de96caef9..1ade28580da2 100644 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -359,8 +359,4 @@ export default class GithubActionsLogsReporter extends DefaultReporter { __endGroup(): void { this.log('::endgroup::'); } - - override log(message: string): void { - super.log(`${message}\n`); - } } diff --git a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js index 507c6ccc83ce..b91c0ead72c8 100644 --- a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js +++ b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js @@ -220,7 +220,7 @@ describe('Result tree output', () => { const testContext = {}; const expectedOutput = ` ${chalk.bold.red.inverse( 'FAIL', - )} / (20 ms)\n ${chalk.red(xSymbol)} test (10 ms)\n`; + )} / (20 ms) ${chalk.red(xSymbol)} test (10 ms)`; const gha = new GhaReporter(); gha.__printResultTree(generatedTree); @@ -248,7 +248,7 @@ describe('Result tree output', () => { const testContext = {}; const expectedOutput = `::group::${chalk.bold.green.inverse( 'PASS', - )} / (20 ms)\n ${chalk.green(ySymbol)} test (10 ms)\n::endgroup::\n`; + )} / (20 ms) ${chalk.green(ySymbol)} test (10 ms)::endgroup::`; const gha = new GhaReporter(); gha.__printResultTree(generatedTree); @@ -281,9 +281,9 @@ describe('Result tree output', () => { }; const testContext = {}; const expectedOutput = - ` ${chalk.bold.red.inverse('FAIL')} / (20 ms)\n` + - ' Test describe\n' + - ` ${chalk.red(xSymbol)} test (10 ms)\n`; + ` ${chalk.bold.red.inverse('FAIL')} / (20 ms)` + + ' Test describe' + + ` ${chalk.red(xSymbol)} test (10 ms)`; const gha = new GhaReporter(); gha.__printResultTree(generatedTree); @@ -316,10 +316,10 @@ describe('Result tree output', () => { }; const testContext = {}; const expectedOutput = - `::group::${chalk.bold.green.inverse('PASS')} / (20 ms)\n` + - ' Test describe\n' + - ` ${chalk.green(ySymbol)} test (10 ms)\n` + - '::endgroup::\n'; + `::group::${chalk.bold.green.inverse('PASS')} / (20 ms)` + + ' Test describe' + + ` ${chalk.green(ySymbol)} test (10 ms)` + + '::endgroup::'; const gha = new GhaReporter(); gha.__printResultTree(generatedTree); @@ -358,9 +358,9 @@ describe('Reporter interface', () => { numTotalTestSuites: 3, }; const expectedOutput = - `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)\n` + - ` ${chalk.green(ySymbol)} test1 (10 ms)\n` + - '::endgroup::\n'; + `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)` + + ` ${chalk.green(ySymbol)} test1 (10 ms)` + + '::endgroup::'; const gha = new GhaReporter(); gha.onTestResult(mockTest, mockTestResult, mockResults); @@ -399,14 +399,12 @@ describe('Reporter interface', () => { testResults: [mockTestResult], }; const expectedOutput = - `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)\n` + - ` ${chalk.green(ySymbol)} test1 (10 ms)\n` + - '::endgroup::\n' + - '\n' + - '::group::Errors thrown in test1.js\n' + - 'Failure message\n' + - '::endgroup::\n' + - '\n'; + `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)` + + ` ${chalk.green(ySymbol)} test1 (10 ms)` + + '::endgroup::' + + '::group::Errors thrown in test1.js' + + 'Failure message' + + '::endgroup::'; const gha = new GhaReporter(); gha.onTestResult(mockTest, mockTestResult, mockResults); From 2f14b3f10b79ffe10807d70ba8dbd1d4a7a729ab Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Mon, 12 Dec 2022 16:20:55 +0100 Subject: [PATCH 10/19] Fix import from src folder --- packages/jest-reporters/src/GithubActionsLogsReporter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts index 1ade28580da2..60057c5681a8 100644 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ b/packages/jest-reporters/src/GithubActionsLogsReporter.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import type {ProjectConfig} from '@jest/types/src/Config'; import chalk = require('chalk'); import type { AggregatedResult, @@ -14,6 +13,7 @@ import type { TestContext, TestResult, } from '@jest/test-result'; +import type {Config} from '@jest/types'; import DefaultReporter from './DefaultReporter'; import type {ReporterOnStartOptions} from './types'; @@ -79,7 +79,7 @@ export default class GithubActionsLogsReporter extends DefaultReporter { ): void {} override testFinished( - _config: ProjectConfig, + _config: Config.ProjectConfig, _testResult: TestResult, _aggregatedResults: AggregatedResult, // eslint-disable-next-line @typescript-eslint/no-empty-function From 3d7f86fd04d4ae621ac881005753cb6ab9d8cc3d Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:26:20 +0100 Subject: [PATCH 11/19] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55678153ced7..cff9acc68bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - `[jest-runtime]` Add `jest.isEnvironmentTornDown` function ([#13698](https://github.com/facebook/jest/pull/13698)) - `[jest-test-result]` Added `skipped` and `focused` status to `FormattedTestResult` ([#13700](https://github.com/facebook/jest/pull/13700)) - `[jest-transform]` Support for asynchronous `createTransformer` ([#13762](https://github.com/facebook/jest/pull/13762)) -- `[@jest/reporters]` Add new reporter for Github Actions runs with automatic log folding and use it as default when in a Github Actions environment ([#13626](https://github.com/facebook/jest/pull/13626)) +- `[@jest/reporters]` New functionality for Github Actions Reporter: automatic log folding ([#13626](https://github.com/facebook/jest/pull/13626)) ### Fixes From 08609a3c228bf1fa2c732af5e4567c51b9e6f83e Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:29:41 +0100 Subject: [PATCH 12/19] Update with new spec --- packages/jest-core/src/TestScheduler.ts | 13 +- .../src/__tests__/TestScheduler.test.js | 206 +--------- .../src/GitHubActionsReporter.ts | 321 +++++++++++++++- .../src/GithubActionsLogsReporter.ts | 362 ------------------ .../GithubActionsLogsReporter.test.js | 2 +- packages/jest-reporters/src/index.ts | 1 - 6 files changed, 327 insertions(+), 578 deletions(-) delete mode 100644 packages/jest-reporters/src/GithubActionsLogsReporter.ts diff --git a/packages/jest-core/src/TestScheduler.ts b/packages/jest-core/src/TestScheduler.ts index 58a090adbdbc..ad96154c2fbb 100644 --- a/packages/jest-core/src/TestScheduler.ts +++ b/packages/jest-core/src/TestScheduler.ts @@ -12,7 +12,6 @@ import { CoverageReporter, DefaultReporter, GitHubActionsReporter, - GithubActionsLogsReporter, BaseReporter as JestReporter, NotifyReporter, Reporter, @@ -343,15 +342,9 @@ class TestScheduler { switch (reporter) { case 'default': summary = true; - if (verbose) { - this.addReporter(new VerboseReporter(this._globalConfig)); - } else { - GITHUB_ACTIONS - ? this.addReporter( - new GithubActionsLogsReporter(this._globalConfig), - ) - : this.addReporter(new DefaultReporter(this._globalConfig)); - } + verbose + ? this.addReporter(new VerboseReporter(this._globalConfig)) + : this.addReporter(new DefaultReporter(this._globalConfig)); break; case 'github-actions': GITHUB_ACTIONS && this.addReporter(new GitHubActionsReporter()); diff --git a/packages/jest-core/src/__tests__/TestScheduler.test.js b/packages/jest-core/src/__tests__/TestScheduler.test.js index 8a5adc441361..d50517f34104 100644 --- a/packages/jest-core/src/__tests__/TestScheduler.test.js +++ b/packages/jest-core/src/__tests__/TestScheduler.test.js @@ -10,7 +10,6 @@ import { CoverageReporter, DefaultReporter, GitHubActionsReporter, - GithubActionsLogsReporter, NotifyReporter, SummaryReporter, VerboseReporter, @@ -60,203 +59,13 @@ beforeEach(() => { spyShouldRunInBand.mockClear(); }); -describe('reporters with GITHUB_ACTIONS = true', () => { +describe('reporters', () => { const CustomReporter = require('/custom-reporter.js'); afterEach(() => { jest.clearAllMocks(); }); - test('works with default value', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: undefined, - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('does not enable any reporters, if empty list is passed', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: [], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(0); - }); - - test('sets up default reporters', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: [['default', {}]], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('sets up verbose reporter', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: [['default', {}]], - verbose: true, - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); - expect(VerboseReporter).toHaveBeenCalledTimes(1); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('sets up github actions reporter', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: [ - ['default', {}], - ['github-actions', {}], - ], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(1); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('sets up notify reporter', async () => { - await createTestScheduler( - makeGlobalConfig({ - notify: true, - reporters: [['default', {}]], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(1); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('sets up coverage reporter', async () => { - await createTestScheduler( - makeGlobalConfig({ - collectCoverage: true, - reporters: [['default', {}]], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(1); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('allows enabling summary reporter separately', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: [['summary', {}]], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - }); - - test('sets up custom reporter', async () => { - await createTestScheduler( - makeGlobalConfig({ - reporters: [ - ['default', {}], - ['/custom-reporter.js', {}], - ], - }), - {}, - {}, - ); - - expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(1); - expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); - expect(NotifyReporter).toHaveBeenCalledTimes(0); - expect(CoverageReporter).toHaveBeenCalledTimes(0); - expect(SummaryReporter).toHaveBeenCalledTimes(1); - expect(CustomReporter).toHaveBeenCalledTimes(1); - }); -}); - -describe('reporters with GITHUB_ACTIONS = false', () => { - const CustomReporter = require('/custom-reporter.js'); - - afterEach(() => { - jest.clearAllMocks(); - }); - - beforeAll(() => { - const ci = require('ci-info'); - ci.GITHUB_ACTIONS = false; - }); - - afterAll(() => { - const ci = require('ci-info'); - ci.GITHUB_ACTIONS = true; - }); - test('works with default value', async () => { await createTestScheduler( makeGlobalConfig({ @@ -267,7 +76,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -285,7 +93,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -303,7 +110,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -322,7 +128,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(1); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -330,7 +135,7 @@ describe('reporters with GITHUB_ACTIONS = false', () => { expect(SummaryReporter).toHaveBeenCalledTimes(1); }); - test('does not set up github actions reporter', async () => { + test('sets up github actions reporter', async () => { await createTestScheduler( makeGlobalConfig({ reporters: [ @@ -343,9 +148,8 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); - expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); + expect(GitHubActionsReporter).toHaveBeenCalledTimes(1); expect(NotifyReporter).toHaveBeenCalledTimes(0); expect(CoverageReporter).toHaveBeenCalledTimes(0); expect(SummaryReporter).toHaveBeenCalledTimes(1); @@ -362,7 +166,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(1); @@ -381,7 +184,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -399,7 +201,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(0); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); @@ -420,7 +221,6 @@ describe('reporters with GITHUB_ACTIONS = false', () => { ); expect(DefaultReporter).toHaveBeenCalledTimes(1); - expect(GithubActionsLogsReporter).toHaveBeenCalledTimes(0); expect(VerboseReporter).toHaveBeenCalledTimes(0); expect(GitHubActionsReporter).toHaveBeenCalledTimes(0); expect(NotifyReporter).toHaveBeenCalledTimes(0); diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts index 7d244d728852..a9aa64e374e3 100644 --- a/packages/jest-reporters/src/GitHubActionsReporter.ts +++ b/packages/jest-reporters/src/GitHubActionsReporter.ts @@ -5,8 +5,15 @@ * LICENSE file in the root directory of this source tree. */ +import chalk = require('chalk'); import stripAnsi = require('strip-ansi'); -import type {Test, TestResult} from '@jest/test-result'; +import type { + AggregatedResult, + AssertionResult, + Test, + TestContext, + TestResult, +} from '@jest/test-result'; import type {Config} from '@jest/types'; import { formatPath, @@ -26,9 +33,50 @@ type AnnotationOptions = { const titleSeparator = ' \u203A '; +type performaceInfo = { + end: number; + runtime: number; + slow: boolean; + start: number; +}; + +type resultTreeLeaf = { + name: string; + passed: boolean; + duration: number; + children: Array; +}; + +type resultTreeNode = { + name: string; + passed: boolean; + children: Array; +}; + +type resultTree = { + children: Array; + name: string; + passed: boolean; + performanceInfo: performaceInfo; +}; + export default class GitHubActionsReporter extends BaseReporter { static readonly filename = __filename; + override onTestResult( + test: Test, + testResult: TestResult, + aggregatedResults: AggregatedResult, + ): void { + this.__printFullResult(test.context, testResult); + if (this.__isLastTestSuite(aggregatedResults)) { + this.log(''); + if (this.__printFailedTestLogs(test, aggregatedResults)) { + this.log(''); + } + } + } + onTestFileResult({context}: Test, {testResults}: TestResult): void { testResults.forEach(result => { const title = [...result.ancestorTitles, result.title].join( @@ -81,4 +129,275 @@ export default class GitHubActionsReporter extends BaseReporter { `\n::${type} file=${file},line=${line},title=${title}::${message}`, ); } + + __isLastTestSuite(results: AggregatedResult): boolean { + const passedTestSuites = results.numPassedTestSuites; + const failedTestSuites = results.numFailedTestSuites; + const totalTestSuites = results.numTotalTestSuites; + const computedTotal = passedTestSuites + failedTestSuites; + if (computedTotal < totalTestSuites) { + return false; + } else if (computedTotal === totalTestSuites) { + return true; + } else { + throw new Error( + `Sum(${computedTotal}) of passed (${passedTestSuites}) and failed (${failedTestSuites}) test suites is greater than the total number of test suites (${totalTestSuites}). Please report the bug at https://github.com/facebook/jest/issues`, + ); + } + } + + __printFullResult(context: TestContext, results: TestResult): void { + const rootDir = context.config.rootDir; + let testDir = results.testFilePath.replace(rootDir, ''); + testDir = testDir.slice(1, testDir.length); + const resultTree = this.__getResultTree( + results.testResults, + testDir, + results.perfStats, + ); + this.__printResultTree(resultTree); + } + + __arrayEqual(a1: Array, a2: Array): boolean { + if (a1.length !== a2.length) { + return false; + } + for (let index = 0; index < a1.length; index++) { + const element = a1[index]; + if (element !== a2[index]) { + return false; + } + } + return true; + } + + __arrayChild(a1: Array, a2: Array): boolean { + if (a1.length - a2.length !== 1) { + return false; + } + for (let index = 0; index < a2.length; index++) { + const element = a2[index]; + if (element !== a1[index]) { + return false; + } + } + return true; + } + + __getResultTree( + suiteResult: Array, + testPath: string, + suitePerf: performaceInfo, + ): resultTree { + const root: resultTree = { + children: [], + name: testPath, + passed: true, + performanceInfo: suitePerf, + }; + const branches: Array> = []; + suiteResult.forEach(element => { + if (element.ancestorTitles.length === 0) { + let passed = true; + if (element.status !== 'passed') { + root.passed = false; + passed = false; + } + if (!element.duration || isNaN(element.duration)) { + throw new Error('Expected duration to be a number, got NaN'); + } + root.children.push({ + children: [], + duration: Math.max(element.duration, 1), + name: element.title, + passed, + }); + } else { + let alreadyInserted = false; + for (let index = 0; index < branches.length; index++) { + if ( + this.__arrayEqual( + branches[index], + element.ancestorTitles.slice(0, 1), + ) + ) { + alreadyInserted = true; + break; + } + } + if (!alreadyInserted) { + branches.push(element.ancestorTitles.slice(0, 1)); + } + } + }); + branches.forEach(element => { + const newChild = this.__getResultChildren(suiteResult, element); + if (!newChild.passed) { + root.passed = false; + } + root.children.push(newChild); + }); + return root; + } + + __getResultChildren( + suiteResult: Array, + ancestors: Array, + ): resultTreeNode { + const node: resultTreeNode = { + children: [], + name: ancestors[ancestors.length - 1], + passed: true, + }; + const branches: Array> = []; + suiteResult.forEach(element => { + let passed = true; + let duration = element.duration; + if (!duration || isNaN(duration)) { + duration = 1; + } + if (this.__arrayEqual(element.ancestorTitles, ancestors)) { + if (element.status !== 'passed') { + node.passed = false; + passed = false; + } + node.children.push({ + children: [], + duration, + name: element.title, + passed, + }); + } else if ( + this.__arrayChild( + element.ancestorTitles.slice(0, ancestors.length + 1), + ancestors, + ) + ) { + let alreadyInserted = false; + for (let index = 0; index < branches.length; index++) { + if ( + this.__arrayEqual( + branches[index], + element.ancestorTitles.slice(0, ancestors.length + 1), + ) + ) { + alreadyInserted = true; + break; + } + } + if (!alreadyInserted) { + branches.push(element.ancestorTitles.slice(0, ancestors.length + 1)); + } + } + }); + branches.forEach(element => { + const newChild = this.__getResultChildren(suiteResult, element); + if (!newChild.passed) { + node.passed = false; + } + node.children.push(newChild); + }); + return node; + } + + __printResultTree(resultTree: resultTree): void { + let perfMs; + if (resultTree.performanceInfo.slow) { + perfMs = ` (${chalk.red.inverse( + `${resultTree.performanceInfo.runtime} ms`, + )})`; + } else { + perfMs = ` (${resultTree.performanceInfo.runtime} ms)`; + } + if (resultTree.passed) { + this.__startGroup( + `${chalk.bold.green.inverse('PASS')} ${resultTree.name}${perfMs}`, + ); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, true, 1); + }); + this.__endGroup(); + } else { + this.log( + ` ${chalk.bold.red.inverse('FAIL')} ${resultTree.name}${perfMs}`, + ); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, false, 1); + }); + } + } + + __recursivePrintResultTree( + resultTree: resultTreeNode | resultTreeLeaf, + alreadyGrouped: boolean, + depth: number, + ): void { + if (resultTree.children.length === 0) { + if (!('duration' in resultTree)) { + throw new Error('Expected a leaf. Got a node.'); + } + let numberSpaces = depth; + if (!alreadyGrouped) { + numberSpaces++; + } + const spaces = ' '.repeat(numberSpaces); + let resultSymbol; + if (resultTree.passed) { + resultSymbol = chalk.green('\u2713'); + } else { + resultSymbol = chalk.red('\u00D7'); + } + this.log( + `${spaces + resultSymbol} ${resultTree.name} (${ + resultTree.duration + } ms)`, + ); + } else { + if (resultTree.passed) { + if (alreadyGrouped) { + this.log(' '.repeat(depth) + resultTree.name); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, true, depth + 1); + }); + } else { + this.__startGroup(' '.repeat(depth) + resultTree.name); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, true, depth + 1); + }); + this.__endGroup(); + } + } else { + this.log(' '.repeat(depth + 1) + resultTree.name); + resultTree.children.forEach(child => { + this.__recursivePrintResultTree(child, false, depth + 1); + }); + } + } + } + + __printFailedTestLogs(context: Test, testResults: AggregatedResult): boolean { + const rootDir = context.context.config.rootDir; + const results = testResults.testResults; + let written = false; + results.forEach(result => { + let testDir = result.testFilePath; + testDir = testDir.replace(rootDir, ''); + testDir = testDir.slice(1, testDir.length); + if (result.failureMessage) { + written = true; + this.__startGroup(`Errors thrown in ${testDir}`); + this.log(result.failureMessage); + this.__endGroup(); + } + }); + return written; + } + + __startGroup(title: string): void { + this.log(`::group::${title}`); + } + + __endGroup(): void { + this.log('::endgroup::'); + } } diff --git a/packages/jest-reporters/src/GithubActionsLogsReporter.ts b/packages/jest-reporters/src/GithubActionsLogsReporter.ts deleted file mode 100644 index 60057c5681a8..000000000000 --- a/packages/jest-reporters/src/GithubActionsLogsReporter.ts +++ /dev/null @@ -1,362 +0,0 @@ -/** - * 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 chalk = require('chalk'); -import type { - AggregatedResult, - AssertionResult, - Test, - TestContext, - TestResult, -} from '@jest/test-result'; -import type {Config} from '@jest/types'; -import DefaultReporter from './DefaultReporter'; -import type {ReporterOnStartOptions} from './types'; - -type performaceInfo = { - end: number; - runtime: number; - slow: boolean; - start: number; -}; - -type resultTreeLeaf = { - name: string; - passed: boolean; - duration: number; - children: Array; -}; - -type resultTreeNode = { - name: string; - passed: boolean; - children: Array; -}; - -type resultTree = { - children: Array; - name: string; - passed: boolean; - performanceInfo: performaceInfo; -}; - -export default class GithubActionsLogsReporter extends DefaultReporter { - // copied from https://github.com/MatteoH2O1999/github-actions-jest-reporter/blob/master/src/gha.reporter.js - override onTestResult( - test: Test, - testResult: TestResult, - aggregatedResults: AggregatedResult, - ): void { - this.__printFullResult(test.context, testResult); - if (this.__isLastTestSuite(aggregatedResults)) { - this.log(''); - if (this.__printFailedTestLogs(test, aggregatedResults)) { - this.log(''); - } - } - } - - // eslint-disable-next-line @typescript-eslint/no-empty-function - override onRunComplete(): void {} - - override onRunStart( - _aggregatedResults: AggregatedResult, - _options: ReporterOnStartOptions, - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): void {} - - // eslint-disable-next-line @typescript-eslint/no-empty-function - override onTestStart(_test: Test): void {} - - override onTestCaseResult( - _test: Test, - _testCaseResult: AssertionResult, - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): void {} - - override testFinished( - _config: Config.ProjectConfig, - _testResult: TestResult, - _aggregatedResults: AggregatedResult, - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): void {} - - __isLastTestSuite(results: AggregatedResult): boolean { - const passedTestSuites = results.numPassedTestSuites; - const failedTestSuites = results.numFailedTestSuites; - const totalTestSuites = results.numTotalTestSuites; - const computedTotal = passedTestSuites + failedTestSuites; - if (computedTotal < totalTestSuites) { - return false; - } else if (computedTotal === totalTestSuites) { - return true; - } else { - throw new Error( - `Sum(${computedTotal}) of passed (${passedTestSuites}) and failed (${failedTestSuites}) test suites is greater than the total number of test suites (${totalTestSuites}). Please report the bug at https://github.com/facebook/jest/issues`, - ); - } - } - - __printFullResult(context: TestContext, results: TestResult): void { - const rootDir = context.config.rootDir; - let testDir = results.testFilePath.replace(rootDir, ''); - testDir = testDir.slice(1, testDir.length); - const resultTree = this.__getResultTree( - results.testResults, - testDir, - results.perfStats, - ); - this.__printResultTree(resultTree); - } - - __arrayEqual(a1: Array, a2: Array): boolean { - if (a1.length !== a2.length) { - return false; - } - for (let index = 0; index < a1.length; index++) { - const element = a1[index]; - if (element !== a2[index]) { - return false; - } - } - return true; - } - - __arrayChild(a1: Array, a2: Array): boolean { - if (a1.length - a2.length !== 1) { - return false; - } - for (let index = 0; index < a2.length; index++) { - const element = a2[index]; - if (element !== a1[index]) { - return false; - } - } - return true; - } - - __getResultTree( - suiteResult: Array, - testPath: string, - suitePerf: performaceInfo, - ): resultTree { - const root: resultTree = { - children: [], - name: testPath, - passed: true, - performanceInfo: suitePerf, - }; - const branches: Array> = []; - suiteResult.forEach(element => { - if (element.ancestorTitles.length === 0) { - let passed = true; - if (element.status === 'failed') { - root.passed = false; - passed = false; - } else if (element.status !== 'passed') { - throw new Error( - `Expected status to be 'failed' or 'passed', got ${element.status}`, - ); - } - if (!element.duration || isNaN(element.duration)) { - throw new Error('Expected duration to be a number, got NaN'); - } - root.children.push({ - children: [], - duration: Math.max(element.duration, 1), - name: element.title, - passed, - }); - } else { - let alreadyInserted = false; - for (let index = 0; index < branches.length; index++) { - if ( - this.__arrayEqual( - branches[index], - element.ancestorTitles.slice(0, 1), - ) - ) { - alreadyInserted = true; - break; - } - } - if (!alreadyInserted) { - branches.push(element.ancestorTitles.slice(0, 1)); - } - } - }); - branches.forEach(element => { - const newChild = this.__getResultChildren(suiteResult, element); - if (!newChild.passed) { - root.passed = false; - } - root.children.push(newChild); - }); - return root; - } - - __getResultChildren( - suiteResult: Array, - ancestors: Array, - ): resultTreeNode { - const node: resultTreeNode = { - children: [], - name: ancestors[ancestors.length - 1], - passed: true, - }; - const branches: Array> = []; - suiteResult.forEach(element => { - let passed = true; - let duration = element.duration; - if (!duration || isNaN(duration)) { - duration = 1; - } - if (this.__arrayEqual(element.ancestorTitles, ancestors)) { - if (element.status === 'failed') { - node.passed = false; - passed = false; - } - node.children.push({ - children: [], - duration, - name: element.title, - passed, - }); - } else if ( - this.__arrayChild( - element.ancestorTitles.slice(0, ancestors.length + 1), - ancestors, - ) - ) { - let alreadyInserted = false; - for (let index = 0; index < branches.length; index++) { - if ( - this.__arrayEqual( - branches[index], - element.ancestorTitles.slice(0, ancestors.length + 1), - ) - ) { - alreadyInserted = true; - break; - } - } - if (!alreadyInserted) { - branches.push(element.ancestorTitles.slice(0, ancestors.length + 1)); - } - } - }); - branches.forEach(element => { - const newChild = this.__getResultChildren(suiteResult, element); - if (!newChild.passed) { - node.passed = false; - } - node.children.push(newChild); - }); - return node; - } - - __printResultTree(resultTree: resultTree): void { - let perfMs; - if (resultTree.performanceInfo.slow) { - perfMs = ` (${chalk.red.inverse( - `${resultTree.performanceInfo.runtime} ms`, - )})`; - } else { - perfMs = ` (${resultTree.performanceInfo.runtime} ms)`; - } - if (resultTree.passed) { - this.__startGroup( - `${chalk.bold.green.inverse('PASS')} ${resultTree.name}${perfMs}`, - ); - resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, true, 1); - }); - this.__endGroup(); - } else { - this.log( - ` ${chalk.bold.red.inverse('FAIL')} ${resultTree.name}${perfMs}`, - ); - resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, false, 1); - }); - } - } - - __recursivePrintResultTree( - resultTree: resultTreeNode | resultTreeLeaf, - alreadyGrouped: boolean, - depth: number, - ): void { - if (resultTree.children.length === 0) { - if (!('duration' in resultTree)) { - throw new Error('Expected a leaf. Got a node.'); - } - let numberSpaces = depth; - if (!alreadyGrouped) { - numberSpaces++; - } - const spaces = ' '.repeat(numberSpaces); - let resultSymbol; - if (resultTree.passed) { - resultSymbol = chalk.green('\u2713'); - } else { - resultSymbol = chalk.red('\u00D7'); - } - this.log( - `${spaces + resultSymbol} ${resultTree.name} (${ - resultTree.duration - } ms)`, - ); - } else { - if (resultTree.passed) { - if (alreadyGrouped) { - this.log(' '.repeat(depth) + resultTree.name); - resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, true, depth + 1); - }); - } else { - this.__startGroup(' '.repeat(depth) + resultTree.name); - resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, true, depth + 1); - }); - this.__endGroup(); - } - } else { - this.log(' '.repeat(depth + 1) + resultTree.name); - resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, false, depth + 1); - }); - } - } - } - - __printFailedTestLogs(context: Test, testResults: AggregatedResult): boolean { - const rootDir = context.context.config.rootDir; - const results = testResults.testResults; - let written = false; - results.forEach(result => { - let testDir = result.testFilePath; - testDir = testDir.replace(rootDir, ''); - testDir = testDir.slice(1, testDir.length); - if (result.failureMessage) { - written = true; - this.__startGroup(`Errors thrown in ${testDir}`); - this.log(result.failureMessage); - this.__endGroup(); - } - }); - return written; - } - - __startGroup(title: string): void { - this.log(`::group::${title}`); - } - - __endGroup(): void { - this.log('::endgroup::'); - } -} diff --git a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js index b91c0ead72c8..ca805d828662 100644 --- a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js +++ b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js @@ -11,7 +11,7 @@ import util from 'util'; import chalk from 'chalk'; import {beforeEach, describe, expect, jest, test} from '@jest/globals'; import BaseReporter from '../BaseReporter'; -import GhaReporter from '../GithubActionsLogsReporter'; +import GhaReporter from '../GitHubActionsReporter'; const xSymbol = '\u00D7'; const ySymbol = '\u2713'; diff --git a/packages/jest-reporters/src/index.ts b/packages/jest-reporters/src/index.ts index ef52469cfc6d..61f3cde17739 100644 --- a/packages/jest-reporters/src/index.ts +++ b/packages/jest-reporters/src/index.ts @@ -30,7 +30,6 @@ export {default as GitHubActionsReporter} from './GitHubActionsReporter'; export {default as NotifyReporter} from './NotifyReporter'; export {default as SummaryReporter} from './SummaryReporter'; export {default as VerboseReporter} from './VerboseReporter'; -export {default as GithubActionsLogsReporter} from './GithubActionsLogsReporter'; export type { Reporter, ReporterOnStartOptions, From 146a9e02bd0b7285dbbba9cca697305a8f11d48c Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:40:12 +0100 Subject: [PATCH 13/19] Update documentation --- docs/Configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 0a071c26c713..4891bb52c66e 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1278,12 +1278,12 @@ export default config; #### GitHub Actions Reporter -If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages: +If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages and print logs with github group features for easy navigation. Note that `'default'` should not be used in this case as `'github-actions'` will handle that already, so remember to also include `'summary'`: ```js tab /** @type {import('jest').Config} */ const config = { - reporters: ['default', 'github-actions'], + reporters: ['github-actions', 'summary'], }; module.exports = config; @@ -1293,7 +1293,7 @@ module.exports = config; import type {Config} from 'jest'; const config: Config = { - reporters: ['default', 'github-actions'], + reporters: ['github-actions', 'summary'], }; export default config; From 82fb3b5575a021ac78423c25c295b3372f41c105 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 12:47:32 +0100 Subject: [PATCH 14/19] Hide internals and add silent option --- docs/Configuration.md | 6 +- .../src/GitHubActionsReporter.ts | 96 ++++++++++++------- .../GithubActionsLogsReporter.test.js | 16 ++-- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 4891bb52c66e..8260dbab97aa 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1278,12 +1278,12 @@ export default config; #### GitHub Actions Reporter -If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages and print logs with github group features for easy navigation. Note that `'default'` should not be used in this case as `'github-actions'` will handle that already, so remember to also include `'summary'`: +If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages and print logs with github group features for easy navigation. Note that `'default'` should not be used in this case as `'github-actions'` will handle that already, so remember to also include `'summary'`. If you wish to use it only for annotations simply pass `'silent: true'` as option (default is `false`): ```js tab /** @type {import('jest').Config} */ const config = { - reporters: ['github-actions', 'summary'], + reporters: [['github-actions', {silent: false}], 'summary'], }; module.exports = config; @@ -1293,7 +1293,7 @@ module.exports = config; import type {Config} from 'jest'; const config: Config = { - reporters: ['github-actions', 'summary'], + reporters: [['github-actions', {silent: false}], 'summary'], }; export default config; diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts index a9aa64e374e3..8d660bf665ec 100644 --- a/packages/jest-reporters/src/GitHubActionsReporter.ts +++ b/packages/jest-reporters/src/GitHubActionsReporter.ts @@ -62,16 +62,40 @@ type resultTree = { export default class GitHubActionsReporter extends BaseReporter { static readonly filename = __filename; + private readonly silent: boolean; + + constructor( + _globalConfig: Config.GlobalConfig, + reporterOptions: Config.ReporterConfig, + ) { + super(); + if (reporterOptions === null || reporterOptions === undefined) { + reporterOptions = ['github-actions', {silent: false}]; + } + let options = reporterOptions[1]; + if (options === null || options === undefined) { + options = {silent: false}; + } + const silentOption = options.silent; + if (silentOption !== null && silentOption !== undefined) { + this.silent = silentOption as boolean; + } else { + this.silent = false; + } + } override onTestResult( test: Test, testResult: TestResult, aggregatedResults: AggregatedResult, ): void { - this.__printFullResult(test.context, testResult); - if (this.__isLastTestSuite(aggregatedResults)) { + if (this.silent) { + return; + } + this.printFullResult(test.context, testResult); + if (this.isLastTestSuite(aggregatedResults)) { this.log(''); - if (this.__printFailedTestLogs(test, aggregatedResults)) { + if (this.printFailedTestLogs(test, aggregatedResults)) { this.log(''); } } @@ -130,7 +154,7 @@ export default class GitHubActionsReporter extends BaseReporter { ); } - __isLastTestSuite(results: AggregatedResult): boolean { + private isLastTestSuite(results: AggregatedResult): boolean { const passedTestSuites = results.numPassedTestSuites; const failedTestSuites = results.numFailedTestSuites; const totalTestSuites = results.numTotalTestSuites; @@ -146,19 +170,19 @@ export default class GitHubActionsReporter extends BaseReporter { } } - __printFullResult(context: TestContext, results: TestResult): void { + private printFullResult(context: TestContext, results: TestResult): void { const rootDir = context.config.rootDir; let testDir = results.testFilePath.replace(rootDir, ''); testDir = testDir.slice(1, testDir.length); - const resultTree = this.__getResultTree( + const resultTree = this.getResultTree( results.testResults, testDir, results.perfStats, ); - this.__printResultTree(resultTree); + this.printResultTree(resultTree); } - __arrayEqual(a1: Array, a2: Array): boolean { + private arrayEqual(a1: Array, a2: Array): boolean { if (a1.length !== a2.length) { return false; } @@ -171,7 +195,7 @@ export default class GitHubActionsReporter extends BaseReporter { return true; } - __arrayChild(a1: Array, a2: Array): boolean { + private arrayChild(a1: Array, a2: Array): boolean { if (a1.length - a2.length !== 1) { return false; } @@ -184,7 +208,7 @@ export default class GitHubActionsReporter extends BaseReporter { return true; } - __getResultTree( + private getResultTree( suiteResult: Array, testPath: string, suitePerf: performaceInfo, @@ -216,10 +240,7 @@ export default class GitHubActionsReporter extends BaseReporter { let alreadyInserted = false; for (let index = 0; index < branches.length; index++) { if ( - this.__arrayEqual( - branches[index], - element.ancestorTitles.slice(0, 1), - ) + this.arrayEqual(branches[index], element.ancestorTitles.slice(0, 1)) ) { alreadyInserted = true; break; @@ -231,7 +252,7 @@ export default class GitHubActionsReporter extends BaseReporter { } }); branches.forEach(element => { - const newChild = this.__getResultChildren(suiteResult, element); + const newChild = this.getResultChildren(suiteResult, element); if (!newChild.passed) { root.passed = false; } @@ -240,7 +261,7 @@ export default class GitHubActionsReporter extends BaseReporter { return root; } - __getResultChildren( + private getResultChildren( suiteResult: Array, ancestors: Array, ): resultTreeNode { @@ -256,7 +277,7 @@ export default class GitHubActionsReporter extends BaseReporter { if (!duration || isNaN(duration)) { duration = 1; } - if (this.__arrayEqual(element.ancestorTitles, ancestors)) { + if (this.arrayEqual(element.ancestorTitles, ancestors)) { if (element.status !== 'passed') { node.passed = false; passed = false; @@ -268,7 +289,7 @@ export default class GitHubActionsReporter extends BaseReporter { passed, }); } else if ( - this.__arrayChild( + this.arrayChild( element.ancestorTitles.slice(0, ancestors.length + 1), ancestors, ) @@ -276,7 +297,7 @@ export default class GitHubActionsReporter extends BaseReporter { let alreadyInserted = false; for (let index = 0; index < branches.length; index++) { if ( - this.__arrayEqual( + this.arrayEqual( branches[index], element.ancestorTitles.slice(0, ancestors.length + 1), ) @@ -291,7 +312,7 @@ export default class GitHubActionsReporter extends BaseReporter { } }); branches.forEach(element => { - const newChild = this.__getResultChildren(suiteResult, element); + const newChild = this.getResultChildren(suiteResult, element); if (!newChild.passed) { node.passed = false; } @@ -300,7 +321,7 @@ export default class GitHubActionsReporter extends BaseReporter { return node; } - __printResultTree(resultTree: resultTree): void { + private printResultTree(resultTree: resultTree): void { let perfMs; if (resultTree.performanceInfo.slow) { perfMs = ` (${chalk.red.inverse( @@ -310,24 +331,24 @@ export default class GitHubActionsReporter extends BaseReporter { perfMs = ` (${resultTree.performanceInfo.runtime} ms)`; } if (resultTree.passed) { - this.__startGroup( + this.startGroup( `${chalk.bold.green.inverse('PASS')} ${resultTree.name}${perfMs}`, ); resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, true, 1); + this.recursivePrintResultTree(child, true, 1); }); - this.__endGroup(); + this.endGroup(); } else { this.log( ` ${chalk.bold.red.inverse('FAIL')} ${resultTree.name}${perfMs}`, ); resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, false, 1); + this.recursivePrintResultTree(child, false, 1); }); } } - __recursivePrintResultTree( + private recursivePrintResultTree( resultTree: resultTreeNode | resultTreeLeaf, alreadyGrouped: boolean, depth: number, @@ -357,25 +378,28 @@ export default class GitHubActionsReporter extends BaseReporter { if (alreadyGrouped) { this.log(' '.repeat(depth) + resultTree.name); resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, true, depth + 1); + this.recursivePrintResultTree(child, true, depth + 1); }); } else { - this.__startGroup(' '.repeat(depth) + resultTree.name); + this.startGroup(' '.repeat(depth) + resultTree.name); resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, true, depth + 1); + this.recursivePrintResultTree(child, true, depth + 1); }); - this.__endGroup(); + this.endGroup(); } } else { this.log(' '.repeat(depth + 1) + resultTree.name); resultTree.children.forEach(child => { - this.__recursivePrintResultTree(child, false, depth + 1); + this.recursivePrintResultTree(child, false, depth + 1); }); } } } - __printFailedTestLogs(context: Test, testResults: AggregatedResult): boolean { + private printFailedTestLogs( + context: Test, + testResults: AggregatedResult, + ): boolean { const rootDir = context.context.config.rootDir; const results = testResults.testResults; let written = false; @@ -385,19 +409,19 @@ export default class GitHubActionsReporter extends BaseReporter { testDir = testDir.slice(1, testDir.length); if (result.failureMessage) { written = true; - this.__startGroup(`Errors thrown in ${testDir}`); + this.startGroup(`Errors thrown in ${testDir}`); this.log(result.failureMessage); - this.__endGroup(); + this.endGroup(); } }); return written; } - __startGroup(title: string): void { + private startGroup(title: string): void { this.log(`::group::${title}`); } - __endGroup(): void { + private endGroup(): void { this.log('::endgroup::'); } } diff --git a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js index ca805d828662..e2427f217e56 100644 --- a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js +++ b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js @@ -66,7 +66,7 @@ describe('Result tree generation', () => { }; const gha = new GhaReporter(); - const generated = gha.__getResultTree(testResults, '/', suitePerf); + const generated = gha.getResultTree(testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -104,7 +104,7 @@ describe('Result tree generation', () => { }; const gha = new GhaReporter(); - const generated = gha.__getResultTree(testResults, '/', suitePerf); + const generated = gha.getResultTree(testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -148,7 +148,7 @@ describe('Result tree generation', () => { }; const gha = new GhaReporter(); - const generated = gha.__getResultTree(testResults, '/', suitePerf); + const generated = gha.getResultTree(testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -192,7 +192,7 @@ describe('Result tree generation', () => { }; const gha = new GhaReporter(); - const generated = gha.__getResultTree(testResults, '/', suitePerf); + const generated = gha.getResultTree(testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -223,7 +223,7 @@ describe('Result tree output', () => { )} / (20 ms) ${chalk.red(xSymbol)} test (10 ms)`; const gha = new GhaReporter(); - gha.__printResultTree(generatedTree); + gha.printResultTree(generatedTree); expect(consoleLog).toEqual(expectedOutput); }); @@ -251,7 +251,7 @@ describe('Result tree output', () => { )} / (20 ms) ${chalk.green(ySymbol)} test (10 ms)::endgroup::`; const gha = new GhaReporter(); - gha.__printResultTree(generatedTree); + gha.printResultTree(generatedTree); expect(consoleLog).toEqual(expectedOutput); }); @@ -286,7 +286,7 @@ describe('Result tree output', () => { ` ${chalk.red(xSymbol)} test (10 ms)`; const gha = new GhaReporter(); - gha.__printResultTree(generatedTree); + gha.printResultTree(generatedTree); expect(consoleLog).toEqual(expectedOutput); }); @@ -322,7 +322,7 @@ describe('Result tree output', () => { '::endgroup::'; const gha = new GhaReporter(); - gha.__printResultTree(generatedTree); + gha.printResultTree(generatedTree); expect(consoleLog).toEqual(expectedOutput); }); From 1f2c3e62e4a2bd092e74c01c4b0e6d8dc21fde92 Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:31:48 +0100 Subject: [PATCH 15/19] Update type CamelCase and fix silent option --- packages/jest-core/src/TestScheduler.ts | 5 ++- .../src/GitHubActionsReporter.ts | 42 ++++++++----------- .../__tests__/GitHubActionsReporter.test.ts | 11 ++--- .../GithubActionsLogsReporter.test.js | 2 + 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/jest-core/src/TestScheduler.ts b/packages/jest-core/src/TestScheduler.ts index ad96154c2fbb..38dc47adedd4 100644 --- a/packages/jest-core/src/TestScheduler.ts +++ b/packages/jest-core/src/TestScheduler.ts @@ -347,7 +347,10 @@ class TestScheduler { : this.addReporter(new DefaultReporter(this._globalConfig)); break; case 'github-actions': - GITHUB_ACTIONS && this.addReporter(new GitHubActionsReporter()); + GITHUB_ACTIONS && + this.addReporter( + new GitHubActionsReporter(this._globalConfig, options), + ); break; case 'summary': summary = true; diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts index 8d660bf665ec..0a8e4768f62c 100644 --- a/packages/jest-reporters/src/GitHubActionsReporter.ts +++ b/packages/jest-reporters/src/GitHubActionsReporter.ts @@ -33,31 +33,31 @@ type AnnotationOptions = { const titleSeparator = ' \u203A '; -type performaceInfo = { +type PerformanceInfo = { end: number; runtime: number; slow: boolean; start: number; }; -type resultTreeLeaf = { +type ResultTreeLeaf = { name: string; passed: boolean; duration: number; children: Array; }; -type resultTreeNode = { +type ResultTreeNode = { name: string; passed: boolean; - children: Array; + children: Array; }; -type resultTree = { - children: Array; +type ResultTree = { + children: Array; name: string; passed: boolean; - performanceInfo: performaceInfo; + performanceInfo: PerformanceInfo; }; export default class GitHubActionsReporter extends BaseReporter { @@ -66,17 +66,10 @@ export default class GitHubActionsReporter extends BaseReporter { constructor( _globalConfig: Config.GlobalConfig, - reporterOptions: Config.ReporterConfig, + reporterOptions: Record = {silent: false}, ) { super(); - if (reporterOptions === null || reporterOptions === undefined) { - reporterOptions = ['github-actions', {silent: false}]; - } - let options = reporterOptions[1]; - if (options === null || options === undefined) { - options = {silent: false}; - } - const silentOption = options.silent; + const silentOption = reporterOptions.silent; if (silentOption !== null && silentOption !== undefined) { this.silent = silentOption as boolean; } else { @@ -89,6 +82,7 @@ export default class GitHubActionsReporter extends BaseReporter { testResult: TestResult, aggregatedResults: AggregatedResult, ): void { + this.generateAnnotations(test, testResult); if (this.silent) { return; } @@ -101,7 +95,7 @@ export default class GitHubActionsReporter extends BaseReporter { } } - onTestFileResult({context}: Test, {testResults}: TestResult): void { + generateAnnotations({context}: Test, {testResults}: TestResult): void { testResults.forEach(result => { const title = [...result.ancestorTitles, result.title].join( titleSeparator, @@ -211,9 +205,9 @@ export default class GitHubActionsReporter extends BaseReporter { private getResultTree( suiteResult: Array, testPath: string, - suitePerf: performaceInfo, - ): resultTree { - const root: resultTree = { + suitePerf: PerformanceInfo, + ): ResultTree { + const root: ResultTree = { children: [], name: testPath, passed: true, @@ -264,8 +258,8 @@ export default class GitHubActionsReporter extends BaseReporter { private getResultChildren( suiteResult: Array, ancestors: Array, - ): resultTreeNode { - const node: resultTreeNode = { + ): ResultTreeNode { + const node: ResultTreeNode = { children: [], name: ancestors[ancestors.length - 1], passed: true, @@ -321,7 +315,7 @@ export default class GitHubActionsReporter extends BaseReporter { return node; } - private printResultTree(resultTree: resultTree): void { + private printResultTree(resultTree: ResultTree): void { let perfMs; if (resultTree.performanceInfo.slow) { perfMs = ` (${chalk.red.inverse( @@ -349,7 +343,7 @@ export default class GitHubActionsReporter extends BaseReporter { } private recursivePrintResultTree( - resultTree: resultTreeNode | resultTreeLeaf, + resultTree: ResultTreeNode | ResultTreeLeaf, alreadyGrouped: boolean, depth: number, ): void { diff --git a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts index 19ced0720956..9d4bb73e68ba 100644 --- a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts +++ b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts @@ -6,6 +6,7 @@ */ import type {Test, TestCaseResult, TestResult} from '@jest/test-result'; +import type {Config} from '@jest/types'; import GitHubActionsReporter from '../GitHubActionsReporter'; const mockedStderrWrite = jest @@ -16,7 +17,7 @@ afterEach(() => { jest.clearAllMocks(); }); -const reporter = new GitHubActionsReporter(); +const reporter = new GitHubActionsReporter({} as Config.GlobalConfig); const testMeta = { context: {config: {rootDir: '/user/project'}}, @@ -74,7 +75,7 @@ const testCaseResult = { describe('logs error annotation', () => { test('when an expectation fails to pass', () => { - reporter.onTestFileResult(testMeta, { + reporter.generateAnnotations(testMeta, { testResults: [ { ...testCaseResult, @@ -88,7 +89,7 @@ describe('logs error annotation', () => { }); test('when a test has reference error', () => { - reporter.onTestFileResult( + reporter.generateAnnotations( {...testMeta, path: '/user/project/__tests__/example.test.js:25:12'}, { testResults: [ @@ -105,7 +106,7 @@ describe('logs error annotation', () => { }); test('when test is wrapped in describe block', () => { - reporter.onTestFileResult(testMeta, { + reporter.generateAnnotations(testMeta, { testResults: [ { ...testCaseResult, @@ -121,7 +122,7 @@ describe('logs error annotation', () => { describe('logs warning annotation before logging errors', () => { test('when test result includes retry reasons', () => { - reporter.onTestFileResult(testMeta, { + reporter.generateAnnotations(testMeta, { testResults: [ { ...testCaseResult, diff --git a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js index e2427f217e56..f3a5428f1b60 100644 --- a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js +++ b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js @@ -362,6 +362,7 @@ describe('Reporter interface', () => { ` ${chalk.green(ySymbol)} test1 (10 ms)` + '::endgroup::'; const gha = new GhaReporter(); + gha.generateAnnotations = jest.fn(); gha.onTestResult(mockTest, mockTestResult, mockResults); @@ -406,6 +407,7 @@ describe('Reporter interface', () => { 'Failure message' + '::endgroup::'; const gha = new GhaReporter(); + gha.generateAnnotations = jest.fn(); gha.onTestResult(mockTest, mockTestResult, mockResults); From 266974bb02a9bc82ea710b967e0da30524b59def Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:49:28 +0100 Subject: [PATCH 16/19] Update silent option --- .../src/GitHubActionsReporter.ts | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts index 0a8e4768f62c..2b5243de5d04 100644 --- a/packages/jest-reporters/src/GitHubActionsReporter.ts +++ b/packages/jest-reporters/src/GitHubActionsReporter.ts @@ -62,19 +62,14 @@ type ResultTree = { export default class GitHubActionsReporter extends BaseReporter { static readonly filename = __filename; - private readonly silent: boolean; + private readonly options: {silent: boolean}; constructor( _globalConfig: Config.GlobalConfig, - reporterOptions: Record = {silent: false}, + reporterOptions: {silent?: boolean} = {silent: false}, ) { super(); - const silentOption = reporterOptions.silent; - if (silentOption !== null && silentOption !== undefined) { - this.silent = silentOption as boolean; - } else { - this.silent = false; - } + this.options = {silent: reporterOptions.silent || false}; } override onTestResult( @@ -83,19 +78,18 @@ export default class GitHubActionsReporter extends BaseReporter { aggregatedResults: AggregatedResult, ): void { this.generateAnnotations(test, testResult); - if (this.silent) { - return; + if (!this.options.silent) { + this.printFullResult(test.context, testResult); } - this.printFullResult(test.context, testResult); if (this.isLastTestSuite(aggregatedResults)) { - this.log(''); - if (this.printFailedTestLogs(test, aggregatedResults)) { - this.log(''); - } + this.printFailedTestLogs(test, aggregatedResults); } } - generateAnnotations({context}: Test, {testResults}: TestResult): void { + private generateAnnotations( + {context}: Test, + {testResults}: TestResult, + ): void { testResults.forEach(result => { const title = [...result.ancestorTitles, result.title].join( titleSeparator, @@ -402,7 +396,10 @@ export default class GitHubActionsReporter extends BaseReporter { testDir = testDir.replace(rootDir, ''); testDir = testDir.slice(1, testDir.length); if (result.failureMessage) { - written = true; + if (!written) { + this.log(''); + written = true; + } this.startGroup(`Errors thrown in ${testDir}`); this.log(result.failureMessage); this.endGroup(); From 6d5719982ec123a75ac586e33ce25f5df84ebb2d Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:49:50 +0100 Subject: [PATCH 17/19] Merge test files and update snapshots --- .../__tests__/GitHubActionsReporter.test.ts | 600 +++++++++++++++--- .../GithubActionsLogsReporter.test.js | 416 ------------ .../GitHubActionsReporter.test.ts.snap | 126 +++- 3 files changed, 620 insertions(+), 522 deletions(-) delete mode 100644 packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js diff --git a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts index 9d4bb73e68ba..415589dded1e 100644 --- a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts +++ b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts @@ -5,134 +5,530 @@ * LICENSE file in the root directory of this source tree. */ -import type {Test, TestCaseResult, TestResult} from '@jest/test-result'; +import type { + AggregatedResult, + AssertionResult, + Test, + TestCaseResult, + TestResult, +} from '@jest/test-result'; import type {Config} from '@jest/types'; import GitHubActionsReporter from '../GitHubActionsReporter'; +afterEach(() => { + jest.clearAllMocks(); +}); + const mockedStderrWrite = jest .spyOn(process.stderr, 'write') .mockImplementation(() => true); -afterEach(() => { - jest.clearAllMocks(); -}); +describe('annotations', () => { + const reporter = new GitHubActionsReporter({} as Config.GlobalConfig); + + const testMeta = { + context: {config: {rootDir: '/user/project'}}, + } 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 ()\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 ()\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 retryErrorMessage = + 'Error: \x1B[2mexpect(\x1B[22m\x1B[31mreceived\x1B[39m\x1B[2m).\x1B[22mtoBeFalsy\x1B[2m()\x1B[22m\n' + + '\n' + + 'Received: \x1B[31mtrue\x1B[39m\n' + + ' at Object.toBeFalsy (/user/project/__tests__/example.test.js:19:20)\n' + + ' at Promise.then.completed (/user/project/jest/packages/jest-circus/build/utils.js:333:28)\n' + + ' at new Promise ()\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 _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)\n' + + ' at runAndTransformResultsToJestFormat (/user/project/jest/packages/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:135:21)'; + + const testCaseResult = { + ancestorTitles: [] as Array, + failureMessages: [expectationsErrorMessage], + title: 'example test', + } as TestCaseResult; + + describe('logs error annotation', () => { + test('when an expectation fails to pass', () => { + reporter['generateAnnotations'](testMeta, { + testResults: [ + { + ...testCaseResult, + failureMessages: [expectationsErrorMessage], + }, + ], + } as TestResult); -const reporter = new GitHubActionsReporter({} as Config.GlobalConfig); - -const testMeta = { - context: {config: {rootDir: '/user/project'}}, -} 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 ()\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 ()\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 retryErrorMessage = - 'Error: \x1B[2mexpect(\x1B[22m\x1B[31mreceived\x1B[39m\x1B[2m).\x1B[22mtoBeFalsy\x1B[2m()\x1B[22m\n' + - '\n' + - 'Received: \x1B[31mtrue\x1B[39m\n' + - ' at Object.toBeFalsy (/user/project/__tests__/example.test.js:19:20)\n' + - ' at Promise.then.completed (/user/project/jest/packages/jest-circus/build/utils.js:333:28)\n' + - ' at new Promise ()\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 _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)\n' + - ' at runAndTransformResultsToJestFormat (/user/project/jest/packages/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:135:21)'; - -const testCaseResult = { - ancestorTitles: [] as Array, - failureMessages: [expectationsErrorMessage], - title: 'example test', -} as TestCaseResult; - -describe('logs error annotation', () => { - test('when an expectation fails to pass', () => { - reporter.generateAnnotations(testMeta, { - testResults: [ + expect(mockedStderrWrite).toHaveBeenCalledTimes(1); + expect(mockedStderrWrite.mock.calls[0]).toMatchSnapshot(); + }); + + test('when a test has reference error', () => { + reporter['generateAnnotations']( + {...testMeta, path: '/user/project/__tests__/example.test.js:25:12'}, { - ...testCaseResult, - failureMessages: [expectationsErrorMessage], - }, - ], - } as TestResult); + testResults: [ + { + ...testCaseResult, + failureMessages: [referenceErrorMessage], + }, + ], + } as TestResult, + ); + + expect(mockedStderrWrite).toHaveBeenCalledTimes(1); + expect(mockedStderrWrite.mock.calls[0]).toMatchSnapshot(); + }); - expect(mockedStderrWrite).toHaveBeenCalledTimes(1); - expect(mockedStderrWrite.mock.calls[0]).toMatchSnapshot(); + test('when test is wrapped in describe block', () => { + reporter['generateAnnotations'](testMeta, { + testResults: [ + { + ...testCaseResult, + ancestorTitles: ['describe'], + }, + ], + } as TestResult); + + expect(mockedStderrWrite).toHaveBeenCalledTimes(1); + expect(mockedStderrWrite.mock.calls[0]).toMatchSnapshot(); + }); }); - test('when a test has reference error', () => { - reporter.generateAnnotations( - {...testMeta, path: '/user/project/__tests__/example.test.js:25:12'}, - { + describe('logs warning annotation before logging errors', () => { + test('when test result includes retry reasons', () => { + reporter['generateAnnotations'](testMeta, { testResults: [ { ...testCaseResult, - failureMessages: [referenceErrorMessage], + failureMessages: [retryErrorMessage], + retryReasons: [retryErrorMessage], }, ], - } as TestResult, - ); + } as TestResult); - expect(mockedStderrWrite).toHaveBeenCalledTimes(1); - expect(mockedStderrWrite.mock.calls[0]).toMatchSnapshot(); + expect(mockedStderrWrite).toHaveBeenCalledTimes(2); + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); }); +}); - test('when test is wrapped in describe block', () => { - reporter.generateAnnotations(testMeta, { - testResults: [ +describe('logs', () => { + test('can be instantiated', () => { + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + expect(gha).toBeTruthy(); + expect(gha).toBeInstanceOf(GitHubActionsReporter); + }); + + describe('Result tree generation', () => { + test('failed single test without describe', () => { + const testResults = [ { - ...testCaseResult, - ancestorTitles: ['describe'], + ancestorTitles: [], + duration: 10, + status: 'failed', + title: 'test', }, - ], - } as TestResult); + ] as unknown as Array; + const suitePerf = { + end: 30, + runtime: 20, + slow: false, + start: 10, + }; + const expectedResults = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); - expect(mockedStderrWrite).toHaveBeenCalledTimes(1); - expect(mockedStderrWrite.mock.calls[0]).toMatchSnapshot(); - }); -}); + const generated = gha['getResultTree'](testResults, '/', suitePerf); + + expect(mockedStderrWrite).not.toHaveBeenCalled(); + expect(generated).toEqual(expectedResults); + }); + + test('passed single test without describe', () => { + const testResults = [ + { + ancestorTitles: [], + duration: 10, + status: 'passed', + title: 'test', + }, + ] as unknown as Array; + const suitePerf = { + end: 30, + runtime: 20, + slow: false, + start: 10, + }; + const expectedResults = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + const generated = gha['getResultTree'](testResults, '/', suitePerf); + + expect(mockedStderrWrite).not.toHaveBeenCalled(); + expect(generated).toEqual(expectedResults); + }); + + test('failed single test inside describe', () => { + const testResults = [ + { + ancestorTitles: ['Test describe'], + duration: 10, + status: 'failed', + title: 'test', + }, + ] as unknown as Array; + const suitePerf = { + end: 30, + runtime: 20, + slow: false, + start: 10, + }; + const expectedResults = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: 'Test describe', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + const generated = gha['getResultTree'](testResults, '/', suitePerf); + + expect(mockedStderrWrite).not.toHaveBeenCalled(); + expect(generated).toEqual(expectedResults); + }); -describe('logs warning annotation before logging errors', () => { - test('when test result includes retry reasons', () => { - reporter.generateAnnotations(testMeta, { - testResults: [ + test('passed single test inside describe', () => { + const testResults = [ { - ...testCaseResult, - failureMessages: [retryErrorMessage], - retryReasons: [retryErrorMessage], + ancestorTitles: ['Test describe'], + duration: 10, + status: 'passed', + title: 'test', }, - ], - } as TestResult); + ] as unknown as Array; + const suitePerf = { + end: 30, + runtime: 20, + slow: false, + start: 10, + }; + const expectedResults = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: 'Test describe', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + const generated = gha['getResultTree'](testResults, '/', suitePerf); + + expect(mockedStderrWrite).not.toHaveBeenCalled(); + expect(generated).toEqual(expectedResults); + }); + }); + + describe('Result tree output', () => { + test('failed single test without describe', () => { + const generatedTree = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + gha['printResultTree'](generatedTree); + + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); + + test('passed single test without describe', () => { + const generatedTree = { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + gha['printResultTree'](generatedTree); + + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); + + test('failed single test inside describe', () => { + const generatedTree = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: false, + }, + ], + name: 'Test describe', + passed: false, + }, + ], + name: '/', + passed: false, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + gha['printResultTree'](generatedTree); + + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); + + test('passed single test inside describe', () => { + const generatedTree = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + passed: true, + }, + ], + name: 'Test describe', + passed: true, + }, + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10, + }, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + + gha['printResultTree'](generatedTree); + + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); + }); + + describe('Reporter interface', () => { + test('onTestResult not last', () => { + const mockTest = { + context: { + config: { + rootDir: '/testDir', + }, + }, + }; + const mockTestResult = { + perfStats: { + runtime: 20, + slow: false, + }, + testFilePath: '/testDir/test1.js', + testResults: [ + { + ancestorTitles: [], + duration: 10, + status: 'passed', + title: 'test1', + }, + ], + }; + const mockResults = { + numFailedTestSuites: 1, + numPassedTestSuites: 1, + numTotalTestSuites: 3, + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + gha['generateAnnotations'] = jest.fn(); + + gha.onTestResult( + mockTest as Test, + mockTestResult as unknown as TestResult, + mockResults as AggregatedResult, + ); + + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); + + test('onTestResult last', () => { + const mockTest = { + context: { + config: { + rootDir: '/testDir', + }, + }, + }; + const mockTestResult = { + failureMessage: 'Failure message', + perfStats: { + runtime: 20, + slow: false, + }, + testFilePath: '/testDir/test1.js', + testResults: [ + { + ancestorTitles: [], + duration: 10, + status: 'passed', + title: 'test1', + }, + ], + }; + const mockResults = { + numFailedTestSuites: 1, + numPassedTestSuites: 2, + numTotalTestSuites: 3, + testResults: [mockTestResult], + }; + const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + gha['generateAnnotations'] = jest.fn(); + + gha.onTestResult( + mockTest as Test, + mockTestResult as unknown as TestResult, + mockResults as unknown as AggregatedResult, + ); - expect(mockedStderrWrite).toHaveBeenCalledTimes(2); - expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + expect(mockedStderrWrite.mock.calls).toMatchSnapshot(); + }); }); }); diff --git a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js b/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js deleted file mode 100644 index f3a5428f1b60..000000000000 --- a/packages/jest-reporters/src/__tests__/GithubActionsLogsReporter.test.js +++ /dev/null @@ -1,416 +0,0 @@ -/** - * 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. - */ - -// copied from https://github.com/MatteoH2O1999/github-actions-jest-reporter/blob/master/tests/gha.reporter.test.js - -import util from 'util'; -import chalk from 'chalk'; -import {beforeEach, describe, expect, jest, test} from '@jest/globals'; -import BaseReporter from '../BaseReporter'; -import GhaReporter from '../GitHubActionsReporter'; - -const xSymbol = '\u00D7'; -const ySymbol = '\u2713'; - -let consoleLog; -const mockLog = jest - .spyOn(BaseReporter.prototype, 'log') - .mockImplementation(message => { - consoleLog = consoleLog.concat(message); - }); - -beforeEach(() => { - consoleLog = ''; -}); - -test('can be instantiated', () => { - const gha = new GhaReporter(); - expect(gha).toBeTruthy(); - expect(gha).toBeInstanceOf(GhaReporter); -}); - -describe('Result tree generation', () => { - test('failed single test without describe', () => { - const testResults = [ - { - ancestorTitles: [], - duration: 10, - status: 'failed', - title: 'test', - }, - ]; - const testContext = {}; - const suitePerf = { - runtime: 20, - slow: false, - }; - const expectedResults = { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: false, - }, - ], - name: '/', - passed: false, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const gha = new GhaReporter(); - - const generated = gha.getResultTree(testResults, '/', suitePerf); - - expect(consoleLog).toBe(''); - expect(generated).toEqual(expectedResults); - }); - - test('passed single test without describe', () => { - const testResults = [ - { - ancestorTitles: [], - duration: 10, - status: 'passed', - title: 'test', - }, - ]; - const testContext = {}; - const suitePerf = { - runtime: 20, - slow: false, - }; - const expectedResults = { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: true, - }, - ], - name: '/', - passed: true, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const gha = new GhaReporter(); - - const generated = gha.getResultTree(testResults, '/', suitePerf); - - expect(consoleLog).toBe(''); - expect(generated).toEqual(expectedResults); - }); - - test('failed single test inside describe', () => { - const testResults = [ - { - ancestorTitles: ['Test describe'], - duration: 10, - status: 'failed', - title: 'test', - }, - ]; - const testContext = {}; - const suitePerf = { - runtime: 20, - slow: false, - }; - const expectedResults = { - children: [ - { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: false, - }, - ], - name: 'Test describe', - passed: false, - }, - ], - name: '/', - passed: false, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const gha = new GhaReporter(); - - const generated = gha.getResultTree(testResults, '/', suitePerf); - - expect(consoleLog).toBe(''); - expect(generated).toEqual(expectedResults); - }); - - test('passed single test inside describe', () => { - const testResults = [ - { - ancestorTitles: ['Test describe'], - duration: 10, - status: 'passed', - title: 'test', - }, - ]; - const testContext = {}; - const suitePerf = { - runtime: 20, - slow: false, - }; - const expectedResults = { - children: [ - { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: true, - }, - ], - name: 'Test describe', - passed: true, - }, - ], - name: '/', - passed: true, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const gha = new GhaReporter(); - - const generated = gha.getResultTree(testResults, '/', suitePerf); - - expect(consoleLog).toBe(''); - expect(generated).toEqual(expectedResults); - }); -}); - -describe('Result tree output', () => { - test('failed single test without describe', () => { - const generatedTree = { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: false, - }, - ], - name: '/', - passed: false, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const testContext = {}; - const expectedOutput = ` ${chalk.bold.red.inverse( - 'FAIL', - )} / (20 ms) ${chalk.red(xSymbol)} test (10 ms)`; - const gha = new GhaReporter(); - - gha.printResultTree(generatedTree); - - expect(consoleLog).toEqual(expectedOutput); - }); - - test('passed single test without describe', () => { - const generatedTree = { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: true, - }, - ], - name: '/', - passed: true, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const testContext = {}; - const expectedOutput = `::group::${chalk.bold.green.inverse( - 'PASS', - )} / (20 ms) ${chalk.green(ySymbol)} test (10 ms)::endgroup::`; - const gha = new GhaReporter(); - - gha.printResultTree(generatedTree); - - expect(consoleLog).toEqual(expectedOutput); - }); - - test('failed single test inside describe', () => { - const generatedTree = { - children: [ - { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: false, - }, - ], - name: 'Test describe', - passed: false, - }, - ], - name: '/', - passed: false, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const testContext = {}; - const expectedOutput = - ` ${chalk.bold.red.inverse('FAIL')} / (20 ms)` + - ' Test describe' + - ` ${chalk.red(xSymbol)} test (10 ms)`; - const gha = new GhaReporter(); - - gha.printResultTree(generatedTree); - - expect(consoleLog).toEqual(expectedOutput); - }); - - test('passed single test inside describe', () => { - const generatedTree = { - children: [ - { - children: [ - { - children: [], - duration: 10, - name: 'test', - passed: true, - }, - ], - name: 'Test describe', - passed: true, - }, - ], - name: '/', - passed: true, - performanceInfo: { - runtime: 20, - slow: false, - }, - }; - const testContext = {}; - const expectedOutput = - `::group::${chalk.bold.green.inverse('PASS')} / (20 ms)` + - ' Test describe' + - ` ${chalk.green(ySymbol)} test (10 ms)` + - '::endgroup::'; - const gha = new GhaReporter(); - - gha.printResultTree(generatedTree); - - expect(consoleLog).toEqual(expectedOutput); - }); -}); - -describe('Reporter interface', () => { - test('onTestResult not last', () => { - const mockTest = { - context: { - config: { - rootDir: '/testDir', - }, - }, - }; - const mockTestResult = { - perfStats: { - runtime: 20, - slow: false, - }, - testFilePath: '/testDir/test1.js', - testResults: [ - { - ancestorTitles: [], - duration: 10, - status: 'passed', - title: 'test1', - }, - ], - }; - const mockResults = { - numFailedTestSuites: 1, - numPassedTestSuites: 1, - numTotalTestSuites: 3, - }; - const expectedOutput = - `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)` + - ` ${chalk.green(ySymbol)} test1 (10 ms)` + - '::endgroup::'; - const gha = new GhaReporter(); - gha.generateAnnotations = jest.fn(); - - gha.onTestResult(mockTest, mockTestResult, mockResults); - - expect(consoleLog).toEqual(expectedOutput); - }); - - test('onTestResult last', () => { - const mockTest = { - context: { - config: { - rootDir: '/testDir', - }, - }, - }; - const mockTestResult = { - failureMessage: 'Failure message', - perfStats: { - runtime: 20, - slow: false, - }, - testFilePath: '/testDir/test1.js', - testResults: [ - { - ancestorTitles: [], - duration: 10, - status: 'passed', - title: 'test1', - }, - ], - }; - const mockResults = { - numFailedTestSuites: 1, - numPassedTestSuites: 2, - numTotalTestSuites: 3, - testResults: [mockTestResult], - }; - const expectedOutput = - `::group::${chalk.bold.green.inverse('PASS')} test1.js (20 ms)` + - ` ${chalk.green(ySymbol)} test1 (10 ms)` + - '::endgroup::' + - '::group::Errors thrown in test1.js' + - 'Failure message' + - '::endgroup::'; - const gha = new GhaReporter(); - gha.generateAnnotations = jest.fn(); - - gha.onTestResult(mockTest, mockTestResult, mockResults); - - expect(consoleLog).toEqual(expectedOutput); - }); -}); diff --git a/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.ts.snap b/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.ts.snap index 4201d36f388d..1010136fe994 100644 --- a/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.ts.snap +++ b/packages/jest-reporters/src/__tests__/__snapshots__/GitHubActionsReporter.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`logs error annotation when a test has reference error 1`] = ` +exports[`annotations logs error annotation when a test has reference error 1`] = ` Array [ " ::error file=/user/project/__tests__/example.test.js,line=25,title=example test::ReferenceError: abc is not defined%0A%0A at Object.abc (__tests__/example.test.js:25:12) @@ -8,7 +8,7 @@ Array [ ] `; -exports[`logs error annotation when an expectation fails to pass 1`] = ` +exports[`annotations logs error annotation when an expectation fails to pass 1`] = ` Array [ " ::error file=/user/project/__tests__/example.test.js,line=20,title=example test::expect(received).toBe(expected) // Object.is equality%0A%0AExpected: 1%0AReceived: 10%0A%0A at Object.toBe (__tests__/example.test.js:20:14) @@ -16,7 +16,7 @@ Array [ ] `; -exports[`logs error annotation when test is wrapped in describe block 1`] = ` +exports[`annotations logs error annotation when test is wrapped in describe block 1`] = ` Array [ " ::error file=/user/project/__tests__/example.test.js,line=20,title=describe › example test::expect(received).toBe(expected) // Object.is equality%0A%0AExpected: 1%0AReceived: 10%0A%0A at Object.toBe (__tests__/example.test.js:20:14) @@ -24,7 +24,7 @@ Array [ ] `; -exports[`logs warning annotation before logging errors when test result includes retry reasons 1`] = ` +exports[`annotations logs warning annotation before logging errors when test result includes retry reasons 1`] = ` Array [ Array [ " @@ -38,3 +38,121 @@ Array [ ], ] `; + +exports[`logs Reporter interface onTestResult last 1`] = ` +Array [ + Array [ + "::group::PASS test1.js (20 ms) +", + ], + Array [ + " ✓ test1 (10 ms) +", + ], + Array [ + "::endgroup:: +", + ], + Array [ + " +", + ], + Array [ + "::group::Errors thrown in test1.js +", + ], + Array [ + "Failure message +", + ], + Array [ + "::endgroup:: +", + ], +] +`; + +exports[`logs Reporter interface onTestResult not last 1`] = ` +Array [ + Array [ + "::group::PASS test1.js (20 ms) +", + ], + Array [ + " ✓ test1 (10 ms) +", + ], + Array [ + "::endgroup:: +", + ], +] +`; + +exports[`logs Result tree output failed single test inside describe 1`] = ` +Array [ + Array [ + " FAIL / (20 ms) +", + ], + Array [ + " Test describe +", + ], + Array [ + " × test (10 ms) +", + ], +] +`; + +exports[`logs Result tree output failed single test without describe 1`] = ` +Array [ + Array [ + " FAIL / (20 ms) +", + ], + Array [ + " × test (10 ms) +", + ], +] +`; + +exports[`logs Result tree output passed single test inside describe 1`] = ` +Array [ + Array [ + "::group::PASS / (20 ms) +", + ], + Array [ + " Test describe +", + ], + Array [ + " ✓ test (10 ms) +", + ], + Array [ + "::endgroup:: +", + ], +] +`; + +exports[`logs Result tree output passed single test without describe 1`] = ` +Array [ + Array [ + "::group::PASS / (20 ms) +", + ], + Array [ + " ✓ test (10 ms) +", + ], + Array [ + "::endgroup:: +", + ], +] +`; From f7ea251f8793061aa868413d5b5227f51f22118c Mon Sep 17 00:00:00 2001 From: Matteo Dell'Acqua <82184604+MatteoH2O1999@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:26:29 +0100 Subject: [PATCH 18/19] Flip silent default value and fix worker shutdown --- docs/Configuration.md | 2 +- .../src/GitHubActionsReporter.ts | 15 ++++--- .../__tests__/GitHubActionsReporter.test.ts | 40 ++++++++++++++----- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 8260dbab97aa..eebddd813855 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1278,7 +1278,7 @@ export default config; #### GitHub Actions Reporter -If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages and print logs with github group features for easy navigation. Note that `'default'` should not be used in this case as `'github-actions'` will handle that already, so remember to also include `'summary'`. If you wish to use it only for annotations simply pass `'silent: true'` as option (default is `false`): +If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages and (if used with `'silent: false'`) print logs with github group features for easy navigation. Note that `'default'` should not be used in this case as `'github-actions'` will handle that already, so remember to also include `'summary'`. If you wish to use it only for annotations simply leave only the reporter without options as the default value of `'silent'` is `'true'`: ```js tab /** @type {import('jest').Config} */ diff --git a/packages/jest-reporters/src/GitHubActionsReporter.ts b/packages/jest-reporters/src/GitHubActionsReporter.ts index 2b5243de5d04..7d439b182111 100644 --- a/packages/jest-reporters/src/GitHubActionsReporter.ts +++ b/packages/jest-reporters/src/GitHubActionsReporter.ts @@ -66,10 +66,15 @@ export default class GitHubActionsReporter extends BaseReporter { constructor( _globalConfig: Config.GlobalConfig, - reporterOptions: {silent?: boolean} = {silent: false}, + reporterOptions: {silent?: boolean} = {silent: true}, ) { super(); - this.options = {silent: reporterOptions.silent || false}; + this.options = { + silent: + typeof reporterOptions.silent === 'boolean' + ? reporterOptions.silent + : true, + }; } override onTestResult( @@ -215,12 +220,10 @@ export default class GitHubActionsReporter extends BaseReporter { root.passed = false; passed = false; } - if (!element.duration || isNaN(element.duration)) { - throw new Error('Expected duration to be a number, got NaN'); - } + const duration = element.duration || 1; root.children.push({ children: [], - duration: Math.max(element.duration, 1), + duration, name: element.title, passed, }); diff --git a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts index 415589dded1e..a78c9aa00db2 100644 --- a/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts +++ b/packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.ts @@ -186,7 +186,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); const generated = gha['getResultTree'](testResults, '/', suitePerf); @@ -227,7 +229,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); const generated = gha['getResultTree'](testResults, '/', suitePerf); @@ -274,7 +278,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); const generated = gha['getResultTree'](testResults, '/', suitePerf); @@ -321,7 +327,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); const generated = gha['getResultTree'](testResults, '/', suitePerf); @@ -350,7 +358,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); gha['printResultTree'](generatedTree); @@ -376,7 +386,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); gha['printResultTree'](generatedTree); @@ -408,7 +420,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); gha['printResultTree'](generatedTree); @@ -440,7 +454,9 @@ describe('logs', () => { start: 10, }, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); gha['printResultTree'](generatedTree); @@ -477,7 +493,9 @@ describe('logs', () => { numPassedTestSuites: 1, numTotalTestSuites: 3, }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); gha['generateAnnotations'] = jest.fn(); gha.onTestResult( @@ -519,7 +537,9 @@ describe('logs', () => { numTotalTestSuites: 3, testResults: [mockTestResult], }; - const gha = new GitHubActionsReporter({} as Config.GlobalConfig); + const gha = new GitHubActionsReporter({} as Config.GlobalConfig, { + silent: false, + }); gha['generateAnnotations'] = jest.fn(); gha.onTestResult( From 1c12eb387ea041b8fc511a901d40d48f518fb138 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 26 Jan 2023 15:34:59 +0100 Subject: [PATCH 19/19] move changelog entry and copy doc change --- CHANGELOG.md | 2 +- website/versioned_docs/version-29.4/Configuration.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6f4845a19c..aa0a6e2d00f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[expect, jest-circus, @jest/types]` Implement `numPassingAsserts` of testResults to track the number of passing asserts in a test ([#13795](https://github.com/facebook/jest/pull/13795)) - `[jest-core]` Add newlines to JSON output ([#13817](https://github.com/facebook/jest/pull/13817)) +- `[@jest/reporters]` New functionality for Github Actions Reporter: automatic log folding ([#13626](https://github.com/facebook/jest/pull/13626)) ### Fixes @@ -27,7 +28,6 @@ - `[jest-runtime]` Add `jest.isEnvironmentTornDown` function ([#13741](https://github.com/facebook/jest/pull/13741)) - `[jest-test-result]` Added `skipped` and `focused` status to `FormattedTestResult` ([#13700](https://github.com/facebook/jest/pull/13700)) - `[jest-transform]` Support for asynchronous `createTransformer` ([#13762](https://github.com/facebook/jest/pull/13762)) -- `[@jest/reporters]` New functionality for Github Actions Reporter: automatic log folding ([#13626](https://github.com/facebook/jest/pull/13626)) ### Fixes diff --git a/website/versioned_docs/version-29.4/Configuration.md b/website/versioned_docs/version-29.4/Configuration.md index 0a071c26c713..eebddd813855 100644 --- a/website/versioned_docs/version-29.4/Configuration.md +++ b/website/versioned_docs/version-29.4/Configuration.md @@ -1278,12 +1278,12 @@ export default config; #### GitHub Actions Reporter -If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages: +If included in the list, the built-in GitHub Actions Reporter will annotate changed files with test failure messages and (if used with `'silent: false'`) print logs with github group features for easy navigation. Note that `'default'` should not be used in this case as `'github-actions'` will handle that already, so remember to also include `'summary'`. If you wish to use it only for annotations simply leave only the reporter without options as the default value of `'silent'` is `'true'`: ```js tab /** @type {import('jest').Config} */ const config = { - reporters: ['default', 'github-actions'], + reporters: [['github-actions', {silent: false}], 'summary'], }; module.exports = config; @@ -1293,7 +1293,7 @@ module.exports = config; import type {Config} from 'jest'; const config: Config = { - reporters: ['default', 'github-actions'], + reporters: [['github-actions', {silent: false}], 'summary'], }; export default config;