From 7fce125877de295508b29b55beebf515d0cebc29 Mon Sep 17 00:00:00 2001 From: MatteoH2O1999 Date: Sat, 10 Feb 2024 16:13:48 +0100 Subject: [PATCH] Add skipped and todo test handling --- .eslintrc.json | 2 +- package-lock.json | 9 +- package.json | 7 +- src/gha.reporter.ts | 168 +++++++----- tests/__snapshots__/gha.reporter.test.ts.snap | 21 +- tests/gha.reporter.test.ts | 247 +++++++++++++----- 6 files changed, 304 insertions(+), 150 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 01a408c..464dae2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,7 +17,7 @@ "@typescript-eslint/no-shadow": "error", "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], "@typescript-eslint/no-require-imports": "error", - "@typescript-eslint/array-type": "error", + "@typescript-eslint/array-type": ["error", {"default": "generic"}], "@typescript-eslint/await-thenable": "error", "@typescript-eslint/ban-ts-comment": "error", "camelcase": "off", diff --git a/package-lock.json b/package-lock.json index 3b125ea..cfa57d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@matteoh2o1999/github-actions-jest-reporter", - "version": "2.0.0", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@matteoh2o1999/github-actions-jest-reporter", - "version": "2.0.0", + "version": "3.0.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.0", "chalk": "^4.0.0", - "jest": "^29.3.1" + "jest": "^29.3.1", + "jest-util": "^29.7.0" }, "devDependencies": { "@babel/preset-env": "^7.19.4", @@ -26,7 +27,7 @@ "typescript": "^5.0.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 87ee523..2acbb34 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,11 @@ "dependencies": { "@actions/core": "^1.10.0", "chalk": "^4.0.0", - "jest": "^29.3.1" + "jest": "^29.3.1", + "jest-util": "^29.7.0" }, "devDependencies": { "@babel/preset-env": "^7.19.4", - "typescript": "^5.0.4", "@types/node": "^20.1.3", "@typescript-eslint/parser": "^5.59.5", "eslint": "^8.40.0", @@ -49,6 +49,7 @@ "eslint-plugin-jest": "^27.2.1", "js-yaml": "^4.1.0", "prettier": "^3.0.0", - "ts-jest": "^29.1.0" + "ts-jest": "^29.1.0", + "typescript": "^5.0.4" } } diff --git a/src/gha.reporter.ts b/src/gha.reporter.ts index 1eefc04..04ffbf3 100644 --- a/src/gha.reporter.ts +++ b/src/gha.reporter.ts @@ -3,39 +3,42 @@ import * as reporters from '@jest/reporters'; import { AggregatedResult, AssertionResult, + Status, Test, TestContext, TestResult } from '@jest/test-result'; import chalk from 'chalk'; +import {specialChars} from 'jest-util'; -type ResultTree = { +const ICONS = specialChars.ICONS; + +type PerformanceInfo = { + end: number; + runtime: number; + slow: boolean; + start: number; +}; + +type ResultTreeLeaf = { name: string; - passed: boolean; - performanceInfo: PerformanceInfo; - children: (ResultTreeNode | ResultTreeLeaf)[]; + status: Status; + duration: number; + children: Array; }; type ResultTreeNode = { name: string; passed: boolean; - children: (ResultTreeNode | ResultTreeLeaf)[]; + children: Array; }; -type ResultTreeLeaf = { +type ResultTree = { + children: Array; name: string; passed: boolean; - duration: number; - children: never[]; -}; - -type PerformanceInfo = { - end: number; - runtime: number; - slow: boolean; - start: number; + performanceInfo: PerformanceInfo; }; - export default class GithubActionsReporter extends reporters.BaseReporter { override onTestResult( test: Test, @@ -44,10 +47,7 @@ export default class GithubActionsReporter extends reporters.BaseReporter { ): void { this.printFullResult(test.context, testResult); if (this.isLastTestSuite(results)) { - core.info(''); - if (this.printFailedTestLogs(test, results)) { - core.info(''); - } + this.printFailedTestLogs(test, results); } } @@ -71,9 +71,9 @@ export default class GithubActionsReporter extends reporters.BaseReporter { testContexts: Set, results: AggregatedResult ): void { - core.info(''); - core.info(reporters.utils.getSummary(results)); - core.info('Ran all test suites.'); + this.log(''); + this.log(reporters.utils.getSummary(results)); + this.log('Ran all test suites.'); } private printFullResult(context: TestContext, results: TestResult): void { @@ -89,12 +89,11 @@ export default class GithubActionsReporter extends reporters.BaseReporter { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private arrayEqual(a1: any[], a2: any[]): boolean { + private 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]; + for (const [index, element] of a1.entries()) { if (element !== a2[index]) { return false; } @@ -103,12 +102,11 @@ export default class GithubActionsReporter extends reporters.BaseReporter { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private arrayChild(a1: any[], a2: any[]): boolean { + private 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]; + for (const [index, element] of a2.entries()) { if (element !== a1[index]) { return false; } @@ -117,7 +115,7 @@ export default class GithubActionsReporter extends reporters.BaseReporter { } private getResultTree( - suiteResult: AssertionResult[], + suiteResult: Array, testPath: string, suitePerf: PerformanceInfo ): ResultTree { @@ -127,23 +125,18 @@ export default class GithubActionsReporter extends reporters.BaseReporter { passed: true, performanceInfo: suitePerf }; - const branches: string[][] = []; + const branches: Array> = []; for (const element of suiteResult) { 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}` - ); } + const duration = element.duration || 1; root.children.push({ children: [], - duration: Math.max(element.duration || 0, 1), + duration, name: element.title, - passed + status: element.status }); } else { let alreadyInserted = false; @@ -169,27 +162,29 @@ export default class GithubActionsReporter extends reporters.BaseReporter { } private getResultChildren( - suiteResult: AssertionResult[], - ancestors: string[] - ): ResultTreeNode | ResultTreeLeaf { - const node: ResultTreeNode | ResultTreeLeaf = { + suiteResult: Array, + ancestors: Array + ): ResultTreeNode { + const node: ResultTreeNode = { children: [], - name: ancestors[ancestors.length - 1], + name: ancestors.at(-1) || '', passed: true }; - const branches: string[][] = []; + const branches: Array> = []; for (const element of suiteResult) { - let passed = true; + let duration = element.duration; + if (!duration || Number.isNaN(duration)) { + duration = 1; + } if (this.arrayEqual(element.ancestorTitles, ancestors)) { if (element.status === 'failed') { node.passed = false; - passed = false; } node.children.push({ children: [], - duration: Math.max(element.duration || 0, 1), + duration, name: element.title, - passed + status: element.status }); } else if ( this.arrayChild( @@ -234,15 +229,15 @@ export default class GithubActionsReporter extends reporters.BaseReporter { perfMs = ` (${resultTree.performanceInfo.runtime} ms)`; } if (resultTree.passed) { - core.startGroup( + this.startGroup( `${chalk.bold.green.inverse('PASS')} ${resultTree.name}${perfMs}` ); for (const child of resultTree.children) { this.recursivePrintResultTree(child, true, 1); } - core.endGroup(); + this.endGroup(); } else { - core.info( + this.log( ` ${chalk.bold.red.inverse('FAIL')} ${resultTree.name}${perfMs}` ); for (const child of resultTree.children) { @@ -257,37 +252,53 @@ export default class GithubActionsReporter extends reporters.BaseReporter { depth: number ): void { if (resultTree.children.length === 0) { - const leaf = resultTree as ResultTreeLeaf; + 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 (leaf.passed) { - resultSymbol = chalk.green('\u2713'); - } else { - resultSymbol = chalk.red('\u00D7'); + switch (resultTree.status) { + case 'passed': + resultSymbol = chalk.green(ICONS.success); + break; + case 'failed': + resultSymbol = chalk.red(ICONS.failed); + break; + case 'todo': + resultSymbol = chalk.magenta(ICONS.todo); + break; + case 'pending': + case 'skipped': + resultSymbol = chalk.yellow(ICONS.pending); + break; } - core.info(`${spaces + resultSymbol} ${leaf.name} (${leaf.duration} ms)`); + this.log( + `${spaces}${resultSymbol} ${resultTree.name} (${resultTree.duration} ms)` + ); } else { - const node = resultTree as ResultTreeNode; - if (node.passed) { + if (!('passed' in resultTree)) { + throw new Error('Expected a node. Got a leaf'); + } + if (resultTree.passed) { if (alreadyGrouped) { - core.info(' '.repeat(depth) + node.name); - for (const child of node.children) { + this.log(' '.repeat(depth) + resultTree.name); + for (const child of resultTree.children) { this.recursivePrintResultTree(child, true, depth + 1); } } else { - core.startGroup(' '.repeat(depth) + node.name); - for (const child of node.children) { + this.startGroup(' '.repeat(depth) + resultTree.name); + for (const child of resultTree.children) { this.recursivePrintResultTree(child, true, depth + 1); } - core.endGroup(); + this.endGroup(); } } else { - core.info(' '.repeat(depth + 1) + node.name); - for (const child of node.children) { + this.log(' '.repeat(depth + 1) + resultTree.name); + for (const child of resultTree.children) { this.recursivePrintResultTree(child, false, depth + 1); } } @@ -306,12 +317,27 @@ export default class GithubActionsReporter extends reporters.BaseReporter { testDir = testDir.replace(rootDir, ''); testDir = testDir.slice(1, testDir.length); if (result.failureMessage) { - written = true; - core.startGroup(`Errors thrown in ${testDir}`); - core.info(result.failureMessage); - core.endGroup(); + if (!written) { + this.log(''); + written = true; + } + this.startGroup(`Errors thrown in ${testDir}`); + this.log(result.failureMessage); + this.endGroup(); } } return written; } + + override log(message: string): void { + core.info(message); + } + + private startGroup(title: string): void { + core.startGroup(title); + } + + private endGroup(): void { + core.endGroup(); + } } diff --git a/tests/__snapshots__/gha.reporter.test.ts.snap b/tests/__snapshots__/gha.reporter.test.ts.snap index 333b773..a8ef4a9 100644 --- a/tests/__snapshots__/gha.reporter.test.ts.snap +++ b/tests/__snapshots__/gha.reporter.test.ts.snap @@ -15,7 +15,6 @@ exports[`Reporter interface onTestResult last 1`] = ` ::group::Errors thrown in test1.js Failure message ::endgroup:: - " `; @@ -29,13 +28,13 @@ exports[`Reporter interface onTestResult not last 1`] = ` exports[`Result tree output failed single test inside describe 1`] = ` " FAIL / (20 ms) Test describe - × test (10 ms) + ✕ test (10 ms) " `; exports[`Result tree output failed single test without describe 1`] = ` " FAIL / (20 ms) - × test (10 ms) + ✕ test (10 ms) " `; @@ -53,3 +52,19 @@ exports[`Result tree output passed single test without describe 1`] = ` ::endgroup:: " `; + +exports[`Result tree output skipped single test inside describe 1`] = ` +"::group::PASS / (20 ms) + Test describe + ○ test (10 ms) +::endgroup:: +" +`; + +exports[`Result tree output todo single test inside describe 1`] = ` +"::group::PASS / (20 ms) + Test describe + ✎ test (10 ms) +::endgroup:: +" +`; diff --git a/tests/gha.reporter.test.ts b/tests/gha.reporter.test.ts index adf24b5..e7395db 100644 --- a/tests/gha.reporter.test.ts +++ b/tests/gha.reporter.test.ts @@ -3,6 +3,7 @@ import * as reporters from '@jest/reporters'; import { AggregatedResult, AssertionResult, + Status, Test, TestContext, TestResult @@ -17,6 +18,13 @@ import { } from '@jest/globals'; import GhaReporter from '../src'; +function normalizeIcons(str: string): string { + if (!str) { + return str; + } + return str.replace(/\u00D7/gu, '\u2715').replace(/\u221A/gu, '\u2713'); +} + jest.mock('@actions/core'); const mockedCore = jest.mocked(core); mockedCore.startGroup.mockImplementation(groupName => @@ -41,7 +49,7 @@ describe('Result tree generation', () => { beforeAll(() => { mockedCore.info.mockImplementation(message => { - consoleLog = consoleLog.concat(message, '\n'); + consoleLog = consoleLog.concat(normalizeIcons(message), '\n'); }); }); @@ -57,12 +65,12 @@ describe('Result tree generation', () => { status: 'failed', title: 'test' } - ]; + ] as unknown as Array; const suitePerf = { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 }; const expectedResults = { children: [ @@ -70,25 +78,21 @@ describe('Result tree generation', () => { children: [], duration: 10, name: 'test', - passed: false + status: 'failed' } ], name: '/', passed: false, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); - const generated = gha['getResultTree']( - testResults as unknown as AssertionResult[], - '/', - suitePerf - ); + const generated = gha['getResultTree'](testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -102,12 +106,12 @@ describe('Result tree generation', () => { status: 'passed', title: 'test' } - ]; + ] as unknown as Array; const suitePerf = { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 }; const expectedResults = { children: [ @@ -115,25 +119,21 @@ describe('Result tree generation', () => { children: [], duration: 10, name: 'test', - passed: true + status: 'passed' } ], name: '/', passed: true, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); - const generated = gha['getResultTree']( - testResults as unknown as AssertionResult[], - '/', - suitePerf - ); + const generated = gha['getResultTree'](testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -147,12 +147,12 @@ describe('Result tree generation', () => { status: 'failed', title: 'test' } - ]; + ] as unknown as Array; const suitePerf = { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 }; const expectedResults = { children: [ @@ -162,7 +162,7 @@ describe('Result tree generation', () => { children: [], duration: 10, name: 'test', - passed: false + status: 'failed' } ], name: 'Test describe', @@ -172,19 +172,15 @@ describe('Result tree generation', () => { name: '/', passed: false, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); - const generated = gha['getResultTree']( - testResults as unknown as AssertionResult[], - '/', - suitePerf - ); + const generated = gha['getResultTree'](testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -198,12 +194,12 @@ describe('Result tree generation', () => { status: 'passed', title: 'test' } - ]; + ] as unknown as Array; const suitePerf = { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 }; const expectedResults = { children: [ @@ -213,7 +209,7 @@ describe('Result tree generation', () => { children: [], duration: 10, name: 'test', - passed: true + status: 'passed' } ], name: 'Test describe', @@ -223,19 +219,74 @@ describe('Result tree generation', () => { name: '/', passed: true, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); - const generated = gha['getResultTree']( - testResults as unknown as AssertionResult[], - '/', - suitePerf - ); + const generated = gha['getResultTree'](testResults, '/', suitePerf); + + expect(consoleLog).toBe(''); + expect(generated).toEqual(expectedResults); + }); + + test('skipped single test and todo single test inside describe', () => { + const testResults = [ + { + ancestorTitles: ['Test describe'], + duration: 10, + status: 'skipped', + title: 'test' + }, + { + ancestorTitles: ['Test describe'], + duration: 14, + status: 'todo', + title: 'test2' + } + ] as unknown as Array; + const suitePerf = { + end: 30, + runtime: 20, + slow: false, + start: 10 + }; + const expectedResults = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + status: 'skipped' + }, + { + children: [], + duration: 14, + name: 'test2', + status: 'todo' + } + ], + name: 'Test describe', + passed: true + } + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10 + } + }; + const gha = new GhaReporter(); + + const generated = gha['getResultTree'](testResults, '/', suitePerf); expect(consoleLog).toBe(''); expect(generated).toEqual(expectedResults); @@ -247,7 +298,7 @@ describe('Result tree output', () => { beforeAll(() => { mockedCore.info.mockImplementation(message => { - consoleLog = consoleLog.concat(message, '\n'); + consoleLog = consoleLog.concat(normalizeIcons(message), '\n'); }); }); @@ -262,16 +313,16 @@ describe('Result tree output', () => { children: [], duration: 10, name: 'test', - passed: false + status: 'failed' as Status } ], name: '/', passed: false, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); @@ -288,16 +339,16 @@ describe('Result tree output', () => { children: [], duration: 10, name: 'test', - passed: true + status: 'passed' as Status } ], name: '/', passed: true, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); @@ -316,7 +367,7 @@ describe('Result tree output', () => { children: [], duration: 10, name: 'test', - passed: false + status: 'failed' as Status } ], name: 'Test describe', @@ -326,10 +377,10 @@ describe('Result tree output', () => { name: '/', passed: false, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 } }; const gha = new GhaReporter(); @@ -348,7 +399,7 @@ describe('Result tree output', () => { children: [], duration: 10, name: 'test', - passed: true + status: 'passed' as Status } ], name: 'Test describe', @@ -358,10 +409,74 @@ describe('Result tree output', () => { name: '/', passed: true, performanceInfo: { - end: 1, + end: 30, runtime: 20, slow: false, - start: 0 + start: 10 + } + }; + const gha = new GhaReporter(); + + gha['printResultTree'](generatedTree); + + expect(consoleLog).toMatchSnapshot(); + }); + + test('todo single test inside describe', () => { + const generatedTree = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + status: 'todo' as Status + } + ], + name: 'Test describe', + passed: true + } + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10 + } + }; + const gha = new GhaReporter(); + + gha['printResultTree'](generatedTree); + + expect(consoleLog).toMatchSnapshot(); + }); + + test('skipped single test inside describe', () => { + const generatedTree = { + children: [ + { + children: [ + { + children: [], + duration: 10, + name: 'test', + status: 'skipped' as Status + } + ], + name: 'Test describe', + passed: true + } + ], + name: '/', + passed: true, + performanceInfo: { + end: 30, + runtime: 20, + slow: false, + start: 10 } }; const gha = new GhaReporter(); @@ -377,7 +492,7 @@ describe('Reporter interface', () => { beforeAll(() => { mockedCore.info.mockImplementation(message => { - consoleLog = consoleLog.concat(message, '\n'); + consoleLog = consoleLog.concat(normalizeIcons(message), '\n'); }); }); @@ -408,10 +523,8 @@ describe('Reporter interface', () => { }; const mockTestResult = { perfStats: { - end: 1, runtime: 20, - slow: false, - start: 0 + slow: false }, testFilePath: '/testDir/test1.js', testResults: [ @@ -431,9 +544,9 @@ describe('Reporter interface', () => { const gha = new GhaReporter(); gha.onTestResult( - mockTest as unknown as Test, + mockTest as Test, mockTestResult as unknown as TestResult, - mockResults as unknown as AggregatedResult + mockResults as AggregatedResult ); expect(consoleLog).toMatchSnapshot(); @@ -450,10 +563,8 @@ describe('Reporter interface', () => { const mockTestResult = { failureMessage: 'Failure message', perfStats: { - end: 1, runtime: 20, - slow: false, - start: 0 + slow: false }, testFilePath: '/testDir/test1.js', testResults: [ @@ -474,7 +585,7 @@ describe('Reporter interface', () => { const gha = new GhaReporter(); gha.onTestResult( - mockTest as unknown as Test, + mockTest as Test, mockTestResult as unknown as TestResult, mockResults as unknown as AggregatedResult );