diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 3436e5f..d7ec292 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -4,23 +4,22 @@ on: [push] jobs: build: - runs-on: ubuntu-latest strategy: matrix: - node-version: [12, 14, 16, 18] + node-version: [18, 20] steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - name: npm install, build, and test - run: | - npm ci - npm run build --if-present - npm test - env: - CI: true + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: npm install, build, and test + run: | + npm ci + npm run build --if-present + npm test + env: + CI: true diff --git a/__tests__/data.json b/__tests__/data.json index 4ac0b45..35b46f9 100644 --- a/__tests__/data.json +++ b/__tests__/data.json @@ -11,8 +11,14 @@ "testResults": [ { "ancestorTitles": ["path2", "to", "test3"], "title": "title3", "status": "passed", "duration": 123 }, { "ancestorTitles": ["path2", "to", "test4"], "title": "title4", "status": "failed", "duration": 123 }, - { "ancestorTitles": ["path2", "to", "test5"], "title": "title5", "status": "failed", "failureMessages": ["Unexpected exception\n at path/to/file1.js:1\n at path/to/file2.js:2"], "duration": 123 }, + { + "ancestorTitles": ["path2", "to", "test5"], + "title": "title5", + "status": "failed", + "failureMessages": ["Unexpected exception\n at path/to/file1.js:1\n at path/to/file2.js:2"], + "duration": 123 + }, { "ancestorTitles": ["path2", "to", "constructor"], "title": "title6", "status": "passed", "duration": 123 } ] } -] \ No newline at end of file +] diff --git a/__tests__/formatter.js b/__tests__/formatter.js index 7d58639..e2fc9db 100644 --- a/__tests__/formatter.js +++ b/__tests__/formatter.js @@ -34,7 +34,9 @@ const consoleOutput = [ ["##teamcity[testSuiteFinished name='test4' flowId='12345']"], ["##teamcity[testSuiteStarted name='test5' flowId='12345']"], ["##teamcity[testStarted name='title5' flowId='12345']"], - ["##teamcity[testFailed name='title5' message='Unexpected exception' details='at path/to/file1.js:1|n at path/to/file2.js:2' flowId='12345']"], + [ + "##teamcity[testFailed name='title5' message='Unexpected exception' details='at path/to/file1.js:1|n at path/to/file2.js:2' flowId='12345']", + ], ["##teamcity[testFinished name='title5' duration='123' flowId='12345']"], ["##teamcity[testSuiteFinished name='test5' flowId='12345']"], ["##teamcity[testSuiteStarted name='constructor' flowId='12345']"], @@ -43,7 +45,7 @@ const consoleOutput = [ ["##teamcity[testSuiteFinished name='constructor' flowId='12345']"], ["##teamcity[testSuiteFinished name='to' flowId='12345']"], ["##teamcity[testSuiteFinished name='path2' flowId='12345']"], - ["##teamcity[testSuiteFinished name='foo/__tests__/file2.js' flowId='12345']"] + ["##teamcity[testSuiteFinished name='foo/__tests__/file2.js' flowId='12345']"], ]; describe("jest-teamcity", () => { @@ -52,8 +54,8 @@ describe("jest-teamcity", () => { let formatterFn = formatter.log; beforeAll(() => { - console.log = jest.fn().mockImplementation(s => s); - const formatterMock = path.sep == "/" ? s => formatterFn(s) : s => formatterFn(s.replace(/\\/g, "/")); + console.log = jest.fn().mockImplementation((s) => s); + const formatterMock = path.sep == "/" ? (s) => formatterFn(s) : (s) => formatterFn(s.replace(/\\/g, "/")); formatter.log = jest.fn().mockImplementation(formatterMock); }); @@ -77,14 +79,14 @@ describe("jest-teamcity", () => { test("escape", () => { expect( formatter.escape(`|test[test2]| -test3`) +test3`), ).toEqual("||test|[test2|]|||ntest3"); }); }); describe("printTestLog", () => { test("empty tests", () => { - ["", null, undefined, {}, [], 0].forEach(data => { + ["", null, undefined, {}, [], 0].forEach((data) => { formatter.printTestLog(data); expect(console.log.mock.calls).toHaveLength(0); }); @@ -118,18 +120,18 @@ test3`) path: expect.objectContaining({ to: expect.objectContaining({ test1: expect.any(Object), - test2: expect.any(Object) - }) - }) + test2: expect.any(Object), + }), + }), }, [file2Key]: { path2: expect.objectContaining({ to: expect.objectContaining({ test3: expect.any(Object), - test4: expect.any(Object) - }) - }) - } + test4: expect.any(Object), + }), + }), + }, }); }); }); @@ -140,19 +142,28 @@ test3`) }); test("textExecError", () => { - formatter.formatReport([{ - "testFilePath": "/Users/spec-with-error/failing.spec.ts", - "testResults": [], - "testExecError": { - "message": "Error:\nSomething bad is happened!", - "stack": "Error: Timeout of 181000 waiting for jest process 168 reached!\nThat means that your test suite, the spec file, took too much time to execute. Try spliting the spec to multiple specs.\n at Timeout._onTimeout (evalmachine.:1901:31)\n at listOnTimeout (internal/timers.js:549:17)\n at processTimers (internal/timers.js:492:7)", - "type": "Error" - } - }], "/Users/spec-with-error", "12345"); + formatter.formatReport( + [ + { + testFilePath: "/Users/spec-with-error/failing.spec.ts", + testResults: [], + testExecError: { + message: "Error:\nSomething bad is happened!", + stack: + "Error: Timeout of 181000 waiting for jest process 168 reached!\nThat means that your test suite, the spec file, took too much time to execute. Try spliting the spec to multiple specs.\n at Timeout._onTimeout (evalmachine.:1901:31)\n at listOnTimeout (internal/timers.js:549:17)\n at processTimers (internal/timers.js:492:7)", + type: "Error", + }, + }, + ], + "/Users/spec-with-error", + "12345", + ); expect(console.log.mock.calls).toEqual([ ["##teamcity[testSuiteStarted name='failing.spec.ts' flowId='12345']"], ["##teamcity[testStarted name='Jest failed to execute suite' flowId='12345']"], - ["##teamcity[testFailed name='Jest failed to execute suite' message='Error:|nSomething bad is happened!' details='Error: Timeout of 181000 waiting for jest process 168 reached!|nThat means that your test suite, the spec file, took too much time to execute. Try spliting the spec to multiple specs.|n at Timeout._onTimeout (evalmachine.:1901:31)|n at listOnTimeout (internal/timers.js:549:17)|n at processTimers (internal/timers.js:492:7)' flowId='12345']"], + [ + "##teamcity[testFailed name='Jest failed to execute suite' message='Error:|nSomething bad is happened!' details='Error: Timeout of 181000 waiting for jest process 168 reached!|nThat means that your test suite, the spec file, took too much time to execute. Try spliting the spec to multiple specs.|n at Timeout._onTimeout (evalmachine.:1901:31)|n at listOnTimeout (internal/timers.js:549:17)|n at processTimers (internal/timers.js:492:7)' flowId='12345']", + ], ["##teamcity[testFinished name='Jest failed to execute suite' duration='0' flowId='12345']"], ["##teamcity[testSuiteFinished name='failing.spec.ts' flowId='12345']"], ]); diff --git a/__tests__/index.js b/__tests__/index.js index 192a593..5882073 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -29,7 +29,9 @@ const consoleOutput = [ ["##teamcity[testSuiteFinished name='test4' flowId='12345']"], ["##teamcity[testSuiteStarted name='test5' flowId='12345']"], ["##teamcity[testStarted name='title5' flowId='12345']"], - ["##teamcity[testFailed name='title5' message='Unexpected exception' details='at path/to/file1.js:1|n at path/to/file2.js:2' flowId='12345']"], + [ + "##teamcity[testFailed name='title5' message='Unexpected exception' details='at path/to/file1.js:1|n at path/to/file2.js:2' flowId='12345']", + ], ["##teamcity[testFinished name='title5' duration='123' flowId='12345']"], ["##teamcity[testSuiteFinished name='test5' flowId='12345']"], ["##teamcity[testSuiteStarted name='constructor' flowId='12345']"], @@ -38,7 +40,7 @@ const consoleOutput = [ ["##teamcity[testSuiteFinished name='constructor' flowId='12345']"], ["##teamcity[testSuiteFinished name='to' flowId='12345']"], ["##teamcity[testSuiteFinished name='path2' flowId='12345']"], - ["##teamcity[testSuiteFinished name='foo/__tests__/file2.js' flowId='12345']"] + ["##teamcity[testSuiteFinished name='foo/__tests__/file2.js' flowId='12345']"], ]; const testData = require("./data"); const reporter = require("../lib/index"); @@ -46,7 +48,7 @@ const reporter = require("../lib/index"); describe("jest-teamcity", () => { beforeAll(() => { process.env.TEAMCITY_FLOWID = 12345; - console.log = jest.fn().mockImplementation(s => s); + console.log = jest.fn().mockImplementation((s) => s); }); beforeEach(() => { @@ -58,7 +60,7 @@ describe("jest-teamcity", () => { process.env.TEAMCITY_VERSION = "0.0.0"; const originalCwd = process.cwd(); - process.cwd = function() { + process.cwd = function () { return "/Users/test"; }; reporter({ testResults: testData }); diff --git a/lib/formatter.js b/lib/formatter.js index d457fc5..3c67af5 100644 --- a/lib/formatter.js +++ b/lib/formatter.js @@ -34,20 +34,20 @@ module.exports = { */ printTestLog(tests, flowId) { if (tests) { - Object.keys(tests).forEach(suiteName => { + Object.keys(tests).forEach((suiteName) => { if (suiteName === "_tests_") { // print test details - tests[suiteName].forEach(test => { + tests[suiteName].forEach((test) => { this.log(`##teamcity[testStarted name='${this.escape(test.title)}' flowId='${flowId}']`); switch (test.status) { case "failed": if (test.failureMessages) { - test.failureMessages.forEach(error => { + test.failureMessages.forEach((error) => { const [message, ...stack] = error.split(errorMessageStackSeparator); this.log( `##teamcity[testFailed name='${this.escape(test.title)}' message='${this.escape( - message - )}' details='${this.escape(stack.join(errorMessageStackSeparator))}' flowId='${flowId}']` + message, + )}' details='${this.escape(stack.join(errorMessageStackSeparator))}' flowId='${flowId}']`, ); }); } else { @@ -57,7 +57,7 @@ module.exports = { break; case "pending": this.log( - `##teamcity[testIgnored name='${this.escape(test.title)}' message='pending' flowId='${flowId}']` + `##teamcity[testIgnored name='${this.escape(test.title)}' message='pending' flowId='${flowId}']`, ); break; case "passed": @@ -66,14 +66,12 @@ module.exports = { this.log( `##teamcity[testFinished name='${this.escape(test.title)}' duration='${ test.duration - }' flowId='${flowId}']` + }' flowId='${flowId}']`, ); }); } else { // print suite names - this.log(`##teamcity[testSuiteStarted name='${this.escape(suiteName)}' flowId='${flowId}']`); - this.printTestLog(tests[suiteName], flowId); - this.log(`##teamcity[testSuiteFinished name='${this.escape(suiteName)}' flowId='${flowId}']`); + this.printSuiteBlock(suiteName, flowId, () => this.printTestLog(tests[suiteName], flowId)); } }); } @@ -101,9 +99,9 @@ module.exports = { } const suites = {}; - testResults.forEach(testFile => { + testResults.forEach((testFile) => { const filename = path.relative(cwd, testFile.testFilePath); - testFile.testResults.forEach(test => { + testFile.testResults.forEach((test) => { const path = [filename].concat(test.ancestorTitles); // find current suite, creating each level if necessary @@ -126,24 +124,47 @@ module.exports = { if (testFile.testExecError) { suites[filename] = suites[filename] || {}; - suites[filename]['_tests_'] = [{ - status: 'failed', - title: 'Jest failed to execute suite', - duration: 0, - failureMessages: [`${testFile.testExecError.message}${errorMessageStackSeparator}${testFile.testExecError.stack}`], - }]; + suites[filename]["_tests_"] = [ + { + status: "failed", + title: "Jest failed to execute suite", + duration: 0, + failureMessages: [ + `${testFile.testExecError.message}${errorMessageStackSeparator}${testFile.testExecError.stack}`, + ], + }, + ]; } }); return suites; }, + /** + * @param {string} suiteName + * @param {string} flowId + * @param {function} printLogFn + */ + printSuiteBlock(suiteName, flowId, printLogFn) { + this.log(`##teamcity[testSuiteStarted name='${this.escape(suiteName)}' flowId='${flowId}']`); + printLogFn(); + this.log(`##teamcity[testSuiteFinished name='${this.escape(suiteName)}' flowId='${flowId}']`); + }, + /** * Formats and outputs tests results * @param {array} testResults + * @param {string} cwd + * @param {string} flowId + * @param {string} [projectSuiteName] */ - formatReport(testResults, cwd, flowId) { + formatReport(testResults, cwd, flowId, projectSuiteName) { const suites = this.collectSuites(testResults, cwd); - this.printTestLog(suites, flowId); - } + + if (projectSuiteName) { + this.printSuiteBlock(projectSuiteName, flowId, () => this.printTestLog(suites, flowId)); + } else { + this.printTestLog(suites, flowId); + } + }, }; diff --git a/lib/index.js b/lib/index.js index 30fe81f..78f5565 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,24 +4,25 @@ const formatter = require("./formatter"); -module.exports = function(result) { +module.exports = function (result) { const flowId = process.env.TEAMCITY_FLOWID || process.pid.toString(); const teamCityVersion = process.env.TEAMCITY_VERSION; - + const projectSuiteName = process.env.PROJECT_NAME; + // Constructor call means usage as a Jest reporter // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target if (new.target) { if (teamCityVersion) { this.onTestResult = (_, result) => { - formatter.formatReport([result], process.cwd(), flowId) - } + formatter.formatReport([result], process.cwd(), flowId, projectSuiteName); + }; } - return + return; } if (teamCityVersion) { - formatter.formatReport(result.testResults, process.cwd(), flowId); + formatter.formatReport(result.testResults, process.cwd(), flowId, projectSuiteName); } return result;