├── .github ├── CODEOWNERS ├── pull_request_template.md ├── issue_template.md └── workflows │ ├── test.yml │ ├── lint.yml │ └── sonarqube.yml ├── .eslintignore ├── index.d.ts ├── src ├── utils │ ├── replaceRootDirInPath.spec.ts │ ├── xml │ │ ├── __snapshots__ │ │ │ ├── failure.spec.ts.snap │ │ │ ├── file.spec.ts.snap │ │ │ └── testCase.spec.ts.snap │ │ ├── failure.spec.ts │ │ ├── failure.ts │ │ ├── file.ts │ │ ├── testCase.ts │ │ ├── testCase.spec.ts │ │ └── file.spec.ts │ ├── buildXmlReport.ts │ ├── replaceRootDirInPath.ts │ ├── getOutputPath.ts │ ├── __snapshots__ │ │ └── buildXmlReport.spec.ts.snap │ ├── getOptions.ts │ ├── buildXmlReport.spec.ts │ └── buildJsonResults.ts └── constants │ └── index.ts ├── .gitignore ├── babel.config.js ├── .npmignore ├── sonar-project.properties ├── .eslintrc.js ├── tsconfig.json ├── jest.config.js ├── LICENSE ├── index.ts ├── package.json ├── CHANGELOG.md └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @CasualBot 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .eslintrc.js 4 | *.config.js 5 | *.config.json 6 | *.d.ts 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | Changes proposed in this pull request: 4 | * 5 | * 6 | * 7 | 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function JestSonarReporter(globalConfig: any, options: Object): any; 2 | export default JestSonarReporter; 3 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Expected behaviour 2 | 3 | 4 | ### Actual behaviour 5 | 6 | 7 | ### Steps to reproduce the behaviour 8 | -------------------------------------------------------------------------------- /src/utils/replaceRootDirInPath.spec.ts: -------------------------------------------------------------------------------- 1 | describe('replaceRootDirInPath', () => { 2 | it('should run', () => { 3 | expect(true).toBe(true); 4 | }) 5 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sonar/ 3 | node_modules 4 | coverage/ 5 | reports/ 6 | test/ 7 | debug.json 8 | *.log 9 | test-report.xml 10 | .vscode 11 | lib/ 12 | jest-sonar.xml 13 | .scannerwork -------------------------------------------------------------------------------- /src/utils/xml/__snapshots__/failure.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`failure 1`] = `""`; 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | "@babel/plugin-transform-modules-commonjs", 8 | ] 9 | }; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .idea/ 3 | .sonar/ 4 | node_modules/.yarn-integrity 5 | coverage/ 6 | reports/ 7 | __tests__ 8 | test/ 9 | .scannerwork/ 10 | .travis.yml 11 | sonar-project.properties 12 | version.sh 13 | wallaby.js 14 | yarn.lock 15 | *.log 16 | test-report.xml 17 | src/ 18 | !lib/ 19 | *.config.* 20 | .* 21 | CHANGELOG* -------------------------------------------------------------------------------- /src/utils/xml/failure.spec.ts: -------------------------------------------------------------------------------- 1 | import xml from 'xml'; 2 | import { failure } from './failure'; 3 | 4 | describe('failure', () => { 5 | test('', () => { 6 | //Arrange 7 | const actualReport = xml(failure('Lorem ispum')) 8 | // Act 9 | // Assert 10 | expect(actualReport).toMatchSnapshot() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/utils/xml/failure.ts: -------------------------------------------------------------------------------- 1 | export const failure = (message: string): any => { 2 | // eslint-disable-next-line no-control-regex 3 | const filteredMessage = message.replace(/([\u001b]\[.{1,2}m)/g, ''); 4 | const shortMessage = filteredMessage.replace(/[\n].*/g, ''); 5 | return { 6 | failure: { 7 | _attr: { 8 | message: shortMessage 9 | }, 10 | _cdata: filteredMessage 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/utils/buildXmlReport.ts: -------------------------------------------------------------------------------- 1 | import file from './xml/file'; 2 | 3 | export default (data: any, options: any = {}): any => { 4 | const aTestExecution = [{_attr: {version: '1'}}] 5 | const testResults = data.testResults.map((result: any) => { return file(result, options.relativePaths, options.projectRoot) }) 6 | 7 | return options?.formatForSonar56 8 | ? { unitTest: aTestExecution.concat(testResults) } 9 | : { testExecutions: aTestExecution.concat(testResults) }; 10 | } 11 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=CasualBot_jest-sonar-reporter 2 | sonar.organization=casualbot 3 | 4 | # Source Coverage 5 | sonar.sources=src/ 6 | sonar.source.inclusions=**/*.ts 7 | 8 | # test coverage 9 | sonar.tests=src/ 10 | sonar.test.inclusions=src/**/*.spec.ts 11 | sonar.coverage.exclusions=src/**/__snapshots__/** 12 | 13 | # reports 14 | sonar.testExecutionReportPaths=coverage/jest-sonar.xml 15 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 16 | sonar.eslint.reportPaths=coverage/eslint.json -------------------------------------------------------------------------------- /src/utils/xml/__snapshots__/file.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`file 1`] = `""`; 4 | 5 | exports[`file testCase projectRoot 1`] = ` 6 | " 7 | " 8 | `; 9 | 10 | exports[`file testCase tag 1`] = ` 11 | " 12 | 13 | 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /src/utils/replaceRootDirInPath.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/facebook/jest/blob/master/packages/jest-config/src/utils.js 2 | // in order to reduce incompatible jest dependencies 3 | import * as path from 'path'; 4 | 5 | 6 | export const replaceRootDirInPath = (rootDir: string, filePath: string): any => { 7 | if (!/^/.test(filePath)) { 8 | return filePath; 9 | } 10 | 11 | return path.resolve( 12 | rootDir, 13 | path.normalize('./' + filePath.substr(''.length)) 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | 'jest' 7 | ], 8 | rules: { 9 | "jest/no-disabled-tests": "warn", 10 | "jest/no-focused-tests": "error", 11 | "jest/no-identical-title": "error", 12 | "jest/prefer-to-have-length": "warn", 13 | "jest/valid-expect": "error" 14 | }, 15 | extends: [ 16 | 'eslint:recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | ] 19 | }; -------------------------------------------------------------------------------- /src/utils/xml/__snapshots__/testCase.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`testCase 1`] = `""`; 4 | 5 | exports[`testCase failing test case 1`] = ` 6 | " 7 | 8 | " 9 | `; 10 | 11 | exports[`testCase skipped test case 1`] = ` 12 | " 13 | 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "target": "ES2020", 5 | "declaration": true, 6 | "emitDeclarationOnly": false, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "outDir": "lib", 11 | "paths": { 12 | "*": ["./node_modules/*"], 13 | }, 14 | "moduleResolution": "node", 15 | "allowJs": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | 19 | 20 | }, 21 | "include": ["src/**/*", "index.ts"], 22 | "exclude": ["node_modules", "**/*.spec.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/xml/file.ts: -------------------------------------------------------------------------------- 1 | import { testCase } from './testCase'; 2 | import * as path from 'path'; 3 | 4 | export default (testResult: any, relativePaths = false, projectRoot: string | null): any => { 5 | let aFile: any; 6 | 7 | if (relativePaths) { 8 | const relativeRoot = projectRoot == null ? process.cwd() : path.resolve(projectRoot); 9 | aFile = [{_attr: { path: path.relative(relativeRoot, testResult.testFilePath) } }]; 10 | } else { 11 | aFile = [{_attr: { path: testResult.testFilePath }}]; 12 | } 13 | 14 | const testCases = testResult.testResults.map(testCase) 15 | 16 | return {file: aFile.concat(testCases)} 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | name: Test 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Run tests 31 | run: npm test 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | name: Lint 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Run linting 31 | run: npm run lint 32 | -------------------------------------------------------------------------------- /src/utils/xml/testCase.ts: -------------------------------------------------------------------------------- 1 | import { failure } from "./failure"; 2 | 3 | export const testCase = (testResult: any): any => { 4 | let failures; 5 | const aTestCase = { 6 | _attr: { 7 | name: testResult.fullName || testResult.title, 8 | duration: testResult.duration || 0 9 | } 10 | } 11 | 12 | if (testResult.status === 'failed') { 13 | failures = testResult.failureMessages.map(failure) 14 | return {testCase: [aTestCase].concat(failures)} 15 | } else if (testResult.status === 'pending') { 16 | return { 17 | testCase: [aTestCase].concat({ 18 | skipped: { 19 | _attr: { 20 | message: "Test skipped" 21 | }, 22 | }, 23 | } as any), 24 | }; 25 | } 26 | return {testCase: aTestCase} 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/getOutputPath.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import getOptions from './getOptions'; 3 | 4 | export default (options: any, jestRootDir: any) => { 5 | // Override outputName and outputDirectory with outputFile if outputFile is defined 6 | let output = options.outputFile; 7 | if (!output) { 8 | // Set output to use new outputDirectory and fallback on original output 9 | const outputName = (options.uniqueOutputName === 'true') ? getOptions.getUniqueOutputName() : options.outputName 10 | output = getOptions.replaceRootDirInOutput(jestRootDir, options.outputDirectory); 11 | const finalOutput = path.join(output, outputName); 12 | return finalOutput; 13 | } 14 | 15 | const finalOutput = getOptions.replaceRootDirInOutput(jestRootDir, output); 16 | return finalOutput; 17 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | rootDir: ".", 4 | preset: 'ts-jest', 5 | moduleNameMapper: { 6 | '^(\\.{1,2}/.*)\\.js$': '$1', 7 | }, 8 | testEnvironment: 'node', 9 | roots: ['/src'], 10 | "transform": { 11 | "^.+\\.[t|j]sx?$": "babel-jest" 12 | }, 13 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(tsx|js|ts)?$', 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 15 | coverageReporters: [ 16 | "lcov" 17 | ], 18 | moduleFileExtensions: ["js", 'ts', "jsx", "json", "node"], 19 | moduleDirectories: ["node_modules", "bower_components", "src"], 20 | coverageThreshold: { 21 | global: { 22 | statements: 80, 23 | branches: 80, 24 | functions: 80, 25 | lines: 80 26 | } 27 | }, 28 | coverageDirectory: "coverage", 29 | reporters: [ 30 | 'default', 31 | [ 32 | '.', 33 | { 34 | relativePaths: true, 35 | outputName: 'jest-sonar.xml', 36 | outputDirectory: 'coverage/', 37 | } 38 | ] 39 | ] 40 | }; -------------------------------------------------------------------------------- /src/utils/xml/testCase.spec.ts: -------------------------------------------------------------------------------- 1 | import xml from 'xml'; 2 | import { testCase } from './testCase'; 3 | 4 | describe('testCase', () => { 5 | test('', () => { 6 | // Arrange 7 | const mock = { 8 | duration: 5, 9 | fullName: 'lorem ipsum' 10 | } 11 | 12 | // Act 13 | const actualReport = xml(testCase(mock)) 14 | 15 | // Assert 16 | expect(actualReport).toMatchSnapshot() 17 | }) 18 | 19 | test('failing test case', () => { 20 | // Arrange 21 | const mock = { 22 | failureMessages: ['Lorem ipsum'], 23 | status: 'failed', 24 | title: 'lorem ipsum' 25 | } 26 | 27 | // Act 28 | const actualReport = xml(testCase(mock), true) 29 | 30 | // Assert 31 | expect(actualReport).toMatchSnapshot() 32 | }) 33 | 34 | test('skipped test case', () => { 35 | // Arrange 36 | const mock = { 37 | status: 'pending', 38 | title: 'lorem ipsum' 39 | } 40 | 41 | // Act 42 | const actualReport = xml(testCase(mock), true) 43 | 44 | // Assert 45 | expect(actualReport).toMatchSnapshot() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Shawn Bernard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/sonarqube.yml: -------------------------------------------------------------------------------- 1 | name: SonarQube Cloud Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | sonarqube-scan: 12 | runs-on: ubuntu-latest 13 | name: SonarQube Cloud Analysis 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Run tests with coverage 33 | run: npm run test:coverage 34 | 35 | - name: Run linting with coverage 36 | run: npm run lint:coverage 37 | 38 | - name: SonarQube Cloud Scan 39 | uses: SonarSource/sonarcloud-github-action@v2 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 43 | -------------------------------------------------------------------------------- /src/utils/xml/file.spec.ts: -------------------------------------------------------------------------------- 1 | import xml from 'xml'; 2 | import file from './file'; 3 | 4 | describe('file', () => { 5 | test('', () => { 6 | // Arrange 7 | const mock = { 8 | testFilePath: 'test/FooTest.js', 9 | testResults: [] 10 | } 11 | 12 | // Act 13 | const actualReport = xml(file(mock, false, null)) 14 | 15 | // Assert 16 | expect(actualReport).toMatchSnapshot() 17 | }) 18 | 19 | test('testCase tag', () => { 20 | // Arrange 21 | const mock = { 22 | testFilePath: 'test/FooTest.js', 23 | testResults: [ 24 | {title: 'lorem ipsum'}, 25 | {title: 'lorem ipsum'} 26 | ] 27 | } 28 | 29 | // Act 30 | const actualReport = xml(file(mock, false, null), true) 31 | 32 | // Assert 33 | expect(actualReport).toMatchSnapshot() 34 | }) 35 | 36 | test('testCase projectRoot', () => { 37 | // Arrange 38 | const mock = { 39 | testFilePath: 'test/FooTest.js', 40 | testResults: [] 41 | } 42 | 43 | // Act 44 | const actualReport = xml(file(mock, true, 'test'), true) 45 | 46 | // Assert 47 | expect(actualReport).toMatchSnapshot() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/utils/__snapshots__/buildXmlReport.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`buildXmlReport file tag 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | " 10 | `; 11 | 12 | exports[`buildXmlReport full report 1`] = ` 13 | " 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | " 28 | `; 29 | 30 | exports[`buildXmlReport root: when not formatted for sonar 5.6.x 1`] = `""`; 31 | 32 | exports[`buildXmlReport root: when formatted for sonar 5.6.x 1`] = `""`; 33 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import xml from 'xml'; 2 | const mkdirp = require('mkdirp'); // eslint-disable-line 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import buildXmlReport from './src/utils/buildXmlReport'; 6 | import getOptions from './src/utils/getOptions'; 7 | import getOutputPath from './src/utils/getOutputPath'; 8 | 9 | const consoleBuffer: any = {}; 10 | 11 | const processor = (report: any, reporterOptions: any = {}, jestRootDir = null) => { 12 | const options = getOptions.options(reporterOptions); 13 | 14 | report.testResults.forEach((t: any, i: any) => { 15 | t.console = consoleBuffer[t.testFilePath]; 16 | }); 17 | 18 | const outputPath = getOutputPath(options, jestRootDir); 19 | 20 | mkdirp.sync(path.dirname(outputPath)); 21 | 22 | fs.writeFileSync(outputPath, xml(buildXmlReport(report, options), {declaration: false, indent: ' '})); 23 | 24 | return report; 25 | }; 26 | 27 | function JestSonar(this: any, globalConfig: any, options: any): void { 28 | 29 | if (globalConfig.hasOwnProperty('testResults')) { // eslint-disable-line 30 | const newConfig = JSON.stringify({ 31 | reporters: ['@casualbot/jest-sonar-reporter'] 32 | }, null, 2); 33 | 34 | return processor(globalConfig); 35 | } 36 | 37 | this._globalConfig = globalConfig; 38 | this._options = options; 39 | 40 | this.onTestResult = (test: any, testResult: any, aggregatedResult: any) => { 41 | if (testResult.console && testResult.console.length > 0) { 42 | consoleBuffer[testResult.testFilePath] = testResult.console; 43 | } 44 | }; 45 | 46 | this.onRunComplete = (contexts: any, results: any) => { 47 | processor(results, this._options, this._globalConfig.rootDir); 48 | }; 49 | } 50 | 51 | module.exports = JestSonar; -------------------------------------------------------------------------------- /src/utils/getOptions.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://raw.githubusercontent.com/jest-community/jest-junit/master/utils/getOptions.js 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { v1 as uuid } from 'uuid'; 5 | import constants from '../constants'; 6 | import { replaceRootDirInPath } from './replaceRootDirInPath'; 7 | 8 | function getEnvOptions() { 9 | const options: any = {}; 10 | const setupConf: any = constants; 11 | 12 | for (const name in setupConf.ENV_CONFIG_MAP) { 13 | if (process.env[name]) { 14 | options[setupConf.ENV_CONFIG_MAP[name] as any] = process.env[name]; 15 | } 16 | } 17 | 18 | return options; 19 | } 20 | 21 | function getAppOptions(pathToResolve: any) { 22 | let traversing = true; 23 | 24 | // Find nearest package.json by traversing up directories until / 25 | while(traversing) { 26 | traversing = pathToResolve !== path.sep; 27 | 28 | const pkgpath = path.join(pathToResolve, 'package.json'); 29 | 30 | if (fs.existsSync(pkgpath)) { 31 | let options = (require(pkgpath) || {})['@casualbot/jest-sonar-reporter']; 32 | 33 | if (Object.prototype.toString.call(options) !== '[object Object]') { 34 | options = {}; 35 | } 36 | 37 | return options; 38 | } else { 39 | pathToResolve = path.dirname(pathToResolve); 40 | } 41 | } 42 | 43 | return {}; 44 | } 45 | 46 | function replaceRootDirInOutput(rootDir: any, output: any) { 47 | return rootDir !== null ? replaceRootDirInPath(rootDir, output) : output; 48 | } 49 | 50 | function getUniqueOutputName() { 51 | return `jest-sonar-reporter-${uuid()}.xml` 52 | } 53 | 54 | export default { 55 | options: (reporterOptions = {}) => { 56 | return Object.assign({}, constants.DEFAULT_OPTIONS, reporterOptions, getAppOptions(process.cwd()), getEnvOptions()); 57 | }, 58 | getAppOptions: getAppOptions, 59 | getEnvOptions: getEnvOptions, 60 | replaceRootDirInOutput: replaceRootDirInOutput, 61 | getUniqueOutputName: getUniqueOutputName 62 | }; -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | ENV_CONFIG_MAP: { 3 | JEST_SUITE_NAME: 'suiteName', 4 | JEST_SONAR_OUTPUT_DIR: 'outputDirectory', 5 | JEST_SONAR_OUTPUT_NAME: 'outputName', 6 | JEST_SONAR_OUTPUT_FILE: 'outputFile', 7 | JEST_SONAR_UNIQUE_OUTPUT_NAME: 'uniqueOutputName', 8 | JEST_SONAR_CLASSNAME: 'classNameTemplate', 9 | JEST_SONAR_SUITE_NAME: 'suiteNameTemplate', 10 | JEST_SONAR_TITLE: 'titleTemplate', 11 | JEST_SONAR_ANCESTOR_SEPARATOR: 'ancestorSeparator', 12 | JEST_SONAR_ADD_FILE_ATTRIBUTE: 'addFileAttribute', 13 | JEST_SONAR_INCLUDE_CONSOLE_OUTPUT: 'includeConsoleOutput', 14 | JEST_SONAR_INCLUDE_SHORT_CONSOLE_OUTPUT: 'includeShortConsoleOutput', 15 | JEST_SONAR_REPORT_TEST_SUITE_ERRORS: 'reportTestSuiteErrors', 16 | JEST_SONAR_NO_STACK_TRACE: "noStackTrace", 17 | JEST_USE_PATH_FOR_SUITE_NAME: 'usePathForSuiteName', 18 | JEST_SONAR_TEST_SUITE_PROPERTIES_JSON_FILE: 'testSuitePropertiesFile', 19 | JEST_SONAR_RELATIVE_PATHS: 'relativePaths', 20 | JEST_SONAR_PROJECT_ROOT: 'projectRoot', 21 | JEST_SONAR_56_FORMAT: 'formatForSonar56' 22 | }, 23 | DEFAULT_OPTIONS: { 24 | suiteName: 'jest tests', 25 | outputDirectory: process.cwd(), 26 | outputName: 'jest-sonar.xml', 27 | uniqueOutputName: false, 28 | classNameTemplate: '{classname} {title}', 29 | suiteNameTemplate: '{title}', 30 | titleTemplate: '{classname} {title}', 31 | ancestorSeparator: ' ', 32 | usePathForSuiteName: false, 33 | addFileAttribute: false, 34 | includeConsoleOutput: false, 35 | includeShortConsoleOutput: false, 36 | reportTestSuiteErrors: false, 37 | noStackTrace: false, 38 | testSuitePropertiesFile: 'jestSonarProperties.js', 39 | relativePaths: false, 40 | projectRoot: null, 41 | formatForSonar56: false 42 | }, 43 | SUITENAME_VAR: 'suitename', 44 | CLASSNAME_VAR: 'classname', 45 | FILENAME_VAR: 'filename', 46 | FILEPATH_VAR: 'filepath', 47 | TITLE_VAR: 'title', 48 | DISPLAY_NAME_VAR: 'displayName', 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils/buildXmlReport.spec.ts: -------------------------------------------------------------------------------- 1 | import xml from 'xml'; 2 | import buildXmlReport from './buildXmlReport'; 3 | 4 | describe('buildXmlReport', () => { 5 | test('root: when not formatted for sonar 5.6.x', () => { 6 | const mock = {testResults: []} 7 | 8 | const actualReport = xml(buildXmlReport(mock, false)) 9 | 10 | expect(actualReport).toMatchSnapshot() 11 | }) 12 | 13 | test('root: when formatted for sonar 5.6.x', () => { 14 | const mock = {testResults: []} 15 | 16 | const actualReport = xml(buildXmlReport(mock, true)) 17 | 18 | expect(actualReport).toMatchSnapshot() 19 | }) 20 | 21 | test('file tag', () => { 22 | const mock = { 23 | testResults: [ 24 | { 25 | testFilePath: 'test/FooTest.js', 26 | testResults: [] 27 | }, 28 | { 29 | testFilePath: 'test/BarTest.js', 30 | testResults: [] 31 | } 32 | ] 33 | } 34 | 35 | const actualReport = xml(buildXmlReport(mock), true) 36 | 37 | expect(actualReport).toMatchSnapshot() 38 | }) 39 | 40 | test('full report', () => { 41 | const mock = { 42 | testResults: [ 43 | { 44 | testFilePath: 'test/FooTest.js', 45 | testResults: [ 46 | { 47 | duration: 5, 48 | fullName: 'lorem ipsum' 49 | } 50 | ] 51 | }, 52 | { 53 | testFilePath: 'test/BarTest.js', 54 | testResults: [ 55 | { 56 | duration: 5, 57 | failureMessages: ['Lorem ipsum'], 58 | fullName: 'lorem ipsum', 59 | status: 'failed', 60 | } 61 | ] 62 | }, 63 | { 64 | testFilePath: 'test/BazTest.js', 65 | testResults: [ 66 | { 67 | duration: 5, 68 | fullName: 'Skipped test', 69 | status: 'pending', 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | 76 | const actualReport = xml(buildXmlReport(mock), true) 77 | 78 | expect(actualReport).toMatchSnapshot() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casualbot/jest-sonar-reporter", 3 | "version": "2.5.0", 4 | "description": "A Sonar test reporter for Jest.", 5 | "keywords": [ 6 | "sonar", 7 | "sonarqube", 8 | "jest", 9 | "reporter", 10 | "processor", 11 | "test", 12 | "jest reporter", 13 | "jest sonar reporter", 14 | "sonarcloud", 15 | "javascript", 16 | "typescript" 17 | ], 18 | "main": "lib/index.js", 19 | "files": [ "lib", "lib/src" ], 20 | "scripts": { 21 | "test": "tsc --module commonjs && jest", 22 | "test:watch": "jest --watch", 23 | "test:coverage": "tsc --module commonjs && jest --coverage", 24 | "build": "tsc --module commonjs", 25 | "lint": "eslint . -c .eslintrc.js --quiet", 26 | "lint:coverage": "eslint . -c .eslintrc.js -f json-relative -o ./coverage/eslint.json" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/CasualBot/jest-sonar-reporter.git" 31 | }, 32 | "author": "Shawn Bernard ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/CasualBot/jest-sonar-reporter/issues" 36 | }, 37 | "homepage": "https://github.com/CasualBot/jest-sonar-reporter#readme", 38 | "engines": { 39 | "node": ">=14.0.0" 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "7.16.0", 43 | "@babel/core": "7.16.0", 44 | "@babel/plugin-transform-modules-commonjs": "7.16.0", 45 | "@babel/plugin-transform-typescript": "7.16.1", 46 | "@babel/preset-env": "7.16.4", 47 | "@babel/preset-typescript": "7.16.0", 48 | "@types/jest": "27.0.3", 49 | "@types/mkdirp": "1.0.2", 50 | "@types/node": "16.11.12", 51 | "@types/uuid": "8.3.3", 52 | "@types/xml": "1.0.6", 53 | "@typescript-eslint/eslint-plugin": "^5.6.0", 54 | "@typescript-eslint/parser": "^5.6.0", 55 | "babel-eslint": "^10.1.0", 56 | "babel-jest": "27.4.2", 57 | "eslint": "^8.4.1", 58 | "eslint-config-prettier": "8.3.0", 59 | "eslint-formatter-json-relative": "^0.1.0", 60 | "eslint-plugin-import": "^2.25.3", 61 | "eslint-plugin-jest": "^25.3.0", 62 | "eslint-plugin-prettier": "4.0.0", 63 | "eslint-plugin-react": "^7.27.1", 64 | "jest": "27.4.3", 65 | "prettier": "2.5.1", 66 | "strip-ansi": "^7.0.1", 67 | "ts-jest": "^27.1.1", 68 | "typescript": "^4.5.3" 69 | }, 70 | "dependencies": { 71 | "mkdirp": "1.0.4", 72 | "uuid": "8.3.2", 73 | "xml": "1.0.1" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.4.0 2 | - Fix package name in README (scanner -> reporter) #23 3 | - Add files to solve NPM 10 lib/src strip problem #22 4 | - Add projectRoot option for relativePaths #20 5 | 6 | # 2.2.5 7 | - fix: update documentation in README 8 | 9 | # 2.2.4 10 | - chore: update CHANGELOG 11 | - fix: add .scarnnerwork to npmignore and add CHANGELOG 12 | 13 | # 2.2.3 14 | 15 | - fix: update readme 16 | - set proper version 17 | - Update README.md 18 | - Update LICENSE 19 | - Update README.md 20 | - update version 21 | - Update README (#4) 22 | - update readme (#3) 23 | - Update to create jest-sonar-reporter (#1) 24 | - Merge branch 'master' into develop 25 | - Merge develop to release version v2.0.0 26 | - Chore: Add templates for GitHub. 27 | - Chore: Update keywords for NPM. 28 | - Documentation: Update LICENSE 29 | - Merge branch 'jest-sonar-reporter-11' into develop 30 | - Documentation: Support different configuration environments. 31 | - Refactoring: Reduce cyclomatic complexity. 32 | - Feature: Support different configuration environments. 33 | - Refactoring: Export default configuration. 34 | - Refactoring: Reorganise project structure. 35 | - Chore: Ignore test report. 36 | - Refactoring: Reduce cyclomatic complexity. 37 | - Fix: Fetch quality status from SonarCloud. 38 | - Fix: Coverage Broken: metrics.isEmpty is not a function at tableRow 39 | - Chore: Update minimum Node version to 8 LTS. 40 | - Merge branch 'jest-sonar-reporter-14' into develop 41 | - Fix: Fetch quality status from SonarCloud. 42 | - Feature: Drop support for env-cmd. 43 | - Chore: Upgrade dev dependencies. 44 | - Merge branch 'master' into develop 45 | - Merge develop to release version v1.3.0 46 | - Ensure Node4 compatibility. 47 | - Merge pull request #12 from Dubes/support_for_sonar_5_6 48 | - jest-sonar-reporter-13 Sonarqube addon has been renamed to Sonarcloud. 49 | - Undo changes done by prettier 50 | - Add support for Sonarqube 5.6.x 51 | - Merge branch 'master' into develop 52 | - Merge branch 'develop' 53 | - v1.2.0-4 54 | - Travis needs organization key for SonarQube. 55 | - Update token for SonarQube. 56 | - Deprecate old configuration option. 57 | - Fixes typo. 58 | - jest-sonar-reporter-9 Adds documentation for the new configuration options. 59 | - Configure scanned branches. 60 | - Adds script to update Sonar's project version. 61 | - Upgrades version of Jest. 62 | - jest-sonar-reporter-8 Adds support to use package.json for configuration. 63 | - Refactoring 64 | - Cleans up generated reports. 65 | - Merge pull request #7 from qtell/master 66 | - testResultsProcessor function is now required to return the modified results 67 | - v1.1.0 68 | - jest-sonar-reporter-5 Add support for stack traces. 69 | - Enable debug output. 70 | - jest-sonar-reporter-4 Replace TestExecutions class with function. 71 | - jest-sonar-reporter-3 Replace File class with function. 72 | - jest-sonar-reporter-2 Replace TestCase class with function. 73 | - jest-sonar-reporter-1 Replace Failure class with function. 74 | - Run tests with coverage. 75 | - Provide proper documentation. 76 | - Provide information for publishing. 77 | - Travis CI integration. 78 | - Import test results and coverage information into Sonar. 79 | - Write test results into XML file. 80 | - Use 'fullName' and 'duration' for test case. 81 | - Test full report. 82 | - Adds failure tag with message information. 83 | - Adds test case tag with name and duration information. 84 | - Adds file tag with path information. 85 | - Adds root tag with version information. 86 | - Project setup. 87 | - Initial commit 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jest Sonar Reporter 2 | 3 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=CasualBot_jest-sonar-reporter&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=CasualBot_jest-sonar-reporter) 4 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=CasualBot_jest-sonar-reporter&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=CasualBot_jest-sonar-reporter) 5 | 6 | [![SonarQube Cloud](https://sonarcloud.io/images/project_badges/sonarcloud-dark.svg)](https://sonarcloud.io/summary/new_code?id=CasualBot_jest-sonar-reporter) 7 | 8 | 9 | `@casualbot/jest-sonar-reporter` is a custom results processor for Jest derived from Christian W. original work [here](https://github.com/3dmind/jest-sonar-reporter). 10 | 11 | It has been updated to be usable as a reporter in the `jest.config`, as well as, provide the ability to output relative paths for the generated XML file. 12 | 13 | ## Installation 14 | 15 | Using npm: 16 | 17 | ```bash 18 | npm install --save-dev @casualbot/jest-sonar-reporter 19 | ``` 20 | 21 | Using yarn: 22 | 23 | ```bash 24 | yarn add --dev @casualbot/jest-sonar-reporter 25 | ``` 26 | 27 | ## Usage 28 | In your jest config add the following entry: 29 | ```JSON 30 | { 31 | "reporters": [ "default", "@casualbot/jest-sonar-reporter" ] 32 | } 33 | ``` 34 | 35 | Then simply run: 36 | 37 | ```shell 38 | jest 39 | ``` 40 | 41 | For your Continuous Integration you can simply do: 42 | ```shell 43 | jest --ci --reporters=default --reporters=@casualbot/jest-sonar-reporter 44 | ``` 45 | 46 | ## Usage as testResultsProcessor (deprecated) 47 | The support for `testResultsProcessor` is only kept for [legacy reasons][test-results-processor] and might be removed in the future. 48 | You should therefore prefer to configure `@casualbot/jest-sonar-reporter` as a _reporter_. 49 | 50 | Should you still want to, add the following entry to your jest config: 51 | ```JSON 52 | { 53 | "testResultsProcessor": "@casualbot/jest-sonar-reporter" 54 | } 55 | ``` 56 | 57 | Then simply run: 58 | 59 | ```shell 60 | jest 61 | ``` 62 | 63 | For your Continuous Integration you can simply do: 64 | ```shell 65 | jest --ci --testResultsProcessor="@casualbot/jest-sonar-reporter" 66 | ``` 67 | 68 | ## Configuration 69 | 70 | `@casualbot/jest-sonar-reporter` offers several configurations based on environment variables or a `@casualbot/jest-sonar-reporter` key defined in `package.json` or a reporter option. 71 | Environment variable and package.json configuration should be **strings**. 72 | Reporter options should also be strings exception for suiteNameTemplate, classNameTemplate, titleNameTemplate that can also accept a function returning a string. 73 | 74 | | Environment Variable Name | Reporter Config Name| Description | Default | Possible Injection Values 75 | |---|---|---|---|---| 76 | | `JEST_SUITE_NAME` | `suiteName` | `name` attribute of `` | `"jest tests"` | N/A 77 | | `JEST_SONAR_OUTPUT_DIR` | `outputDirectory` | Directory to save the output. | `process.cwd()` | N/A 78 | | `JEST_SONAR_OUTPUT_NAME` | `outputName` | File name for the output. | `"jest-report.xml"` | N/A 79 | | `JEST_SONAR_OUTPUT_FILE` | `outputFile` | Fullpath for the output. If defined, `outputDirectory` and `outputName` will be overridden | `undefined` | N/A 80 | | `JEST_SONAR_56_FORMAT` | `formatForSonar56` | Will generate the xml report for Sonar 5.6 | `false` | N/A 81 | | `JEST_SONAR_RELATIVE_PATHS` | `relativePaths` | Will use relative paths when generating the xml report | `false` | N/A 82 | | `JEST_SONAR_UNIQUE_OUTPUT_NAME` | `uniqueOutputName` | Create unique file name for the output `jest-sonar-report-${uuid}.xml`, overrides `outputName` | `false` | N/A 83 | | `JEST_SONAR_SUITE_NAME` | `suiteNameTemplate` | Template string for `name` attribute of the ``. | `"{title}"` | `{title}`, `{filepath}`, `{filename}`, `{displayName}` 84 | | `JEST_SONAR_CLASSNAME` | `classNameTemplate` | Template string for the `classname` attribute of ``. | `"{classname} {title}"` | `{classname}`, `{title}`, `{suitename}`, `{filepath}`, `{filename}`, `{displayName}` 85 | | `JEST_SONAR_TITLE` | `titleTemplate` | Template string for the `name` attribute of ``. | `"{classname} {title}"` | `{classname}`, `{title}`, `{filepath}`, `{filename}`, `{displayName}` 86 | | `JEST_SONAR_ANCESTOR_SEPARATOR` | `ancestorSeparator` | Character(s) used to join the `describe` blocks. | `" "` | N/A 87 | | `JEST_SONAR_ADD_FILE_ATTRIBUTE` | `addFileAttribute` | Add file attribute to the output. This config is primarily for Circle CI. This setting provides richer details but may break on other CI platforms. Must be a string. | `"false"` | N/A 88 | | `JEST_SONAR_INCLUDE_CONSOLE_OUTPUT` | `includeConsoleOutput` | Adds console output to any testSuite that generates stdout during a test run. | `false` | N/A 89 | | `JEST_SONAR_INCLUDE_SHORT_CONSOLE_OUTPUT` | `includeShortConsoleOutput` | Adds short console output (only message value) to any testSuite that generates stdout during a test run. | `false` | N/A 90 | | `JEST_SONAR_REPORT_TEST_SUITE_ERRORS` | `reportTestSuiteErrors` | Reports test suites that failed to execute altogether as `error`. _Note:_ since the suite name cannot be determined from files that fail to load, it will default to file path.| `false` | N/A 91 | | `JEST_SONAR_NO_STACK_TRACE` | `noStackTrace` | Omit stack traces from test failure reports, similar to `jest --noStackTrace` | `false` | N/A 92 | | `JEST_USE_PATH_FOR_SUITE_NAME` | `usePathForSuiteName` | **DEPRECATED. Use `suiteNameTemplate` instead.** Use file path as the `name` attribute of `` | `"false"` | N/A 93 | 94 | 95 | You can configure these options via the command line as seen below: 96 | 97 | ```shell 98 | JEST_SUITE_NAME="Jest JUnit Unit Tests" JEST_SONAR_OUTPUT_DIR="./artifacts" jest 99 | ``` 100 | 101 | Or you can also define a `@casualbot/jest-sonar-reporter` key in your `package.json`. All are **string** values. 102 | 103 | ```json 104 | { 105 | ..., 106 | "jest": { 107 | "rootDir": ".", 108 | "testResultsProcessor": "@casualbot/jest-sonar-reporter" 109 | }, 110 | "@casualbot/jest-sonar-reporter": { 111 | "suiteName": "jest tests", 112 | "outputDirectory": "coverage", 113 | "outputName": "jest-report.xml", 114 | "uniqueOutputName": "false", 115 | "classNameTemplate": "{classname}-{title}", 116 | "titleTemplate": "{classname}-{title}", 117 | "ancestorSeparator": " › ", 118 | "usePathForSuiteName": "true", 119 | "relativePaths": "true" 120 | } 121 | } 122 | ``` 123 | 124 | Or you can define your options in your reporter configuration. 125 | 126 | ```js 127 | // jest.config.js 128 | { 129 | reporters: [ 130 | 'default', 131 | [ 132 | '@casualbot/jest-sonar-reporter', 133 | { 134 | relativePaths: true, 135 | outputName: 'sonar-report.xml', 136 | outputDirectory: 'coverage' 137 | } 138 | ] 139 | ], 140 | } 141 | ``` -------------------------------------------------------------------------------- /src/utils/buildJsonResults.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://raw.githubusercontent.com/jest-community/jest-junit/master/utils/buildJsonResults.js 2 | import stripAnsi from 'strip-ansi'; 3 | import constants from '../constants'; 4 | import * as path from 'path'; 5 | import * as fs from 'fs'; 6 | 7 | const toTemplateTag = function (varName: string) { 8 | return "{" + varName + "}"; 9 | } 10 | 11 | const replaceVars = function (strOrFunc: any, variables: any) { 12 | if (typeof strOrFunc === 'string') { 13 | let str = strOrFunc; 14 | Object.keys(variables).forEach((varName) => { 15 | str = str.replace(toTemplateTag(varName), variables[varName]); 16 | }); 17 | return str; 18 | } else { 19 | const func = strOrFunc; 20 | const resolvedStr = func(variables); 21 | if (typeof resolvedStr !== 'string') { 22 | throw new Error('Template function should return a string'); 23 | } 24 | return resolvedStr; 25 | } 26 | }; 27 | 28 | const executionTime = function (startTime: any, endTime: any) { 29 | return (endTime - startTime) / 1000; 30 | } 31 | 32 | const addErrorTestResult = function (suite: any) { 33 | suite.testResults.push({ 34 | "ancestorTitles": [], 35 | "duration": 0, 36 | "failureMessages": [ 37 | suite.failureMessage 38 | ], 39 | "numPassingAsserts": 0, 40 | "status": "error" 41 | }) 42 | } 43 | 44 | export default (report: any, appDirectory: any, options: any): any => { 45 | const junitSuitePropertiesFilePath = path.join(process.cwd(), options.testSuitePropertiesFile); 46 | const ignoreSuitePropertiesCheck = !fs.existsSync(junitSuitePropertiesFilePath); 47 | 48 | // If the usePathForSuiteName option is true and the 49 | // suiteNameTemplate value is set to the default, overrides 50 | // the suiteNameTemplate. 51 | if (options.usePathForSuiteName === 'true' && 52 | options.suiteNameTemplate === toTemplateTag(constants.TITLE_VAR)) { 53 | 54 | options.suiteNameTemplate = toTemplateTag(constants.FILEPATH_VAR); 55 | } 56 | 57 | // Generate a single XML file for all jest tests 58 | const jsonResults = { 59 | 'testsuites': [{ 60 | '_attr': { 61 | 'name': options.suiteName, 62 | 'tests': 0, 63 | 'failures': 0, 64 | 'errors': 0, 65 | 'skipped': 0, 66 | // Overall execution time: 67 | // Since tests are typically executed in parallel this time can be significantly smaller 68 | // than the sum of the individual test suites 69 | 'time': executionTime(report.startTime, Date.now()) 70 | } 71 | }] 72 | }; 73 | 74 | // Iterate through outer testResults (test suites) 75 | report.testResults.forEach((suite: any) => { 76 | const noResults = suite.testResults.length === 0; 77 | if (noResults && options.reportTestSuiteErrors === 'false') { 78 | return; 79 | } 80 | 81 | const noResultOptions = noResults ? { 82 | suiteNameTemplate: toTemplateTag(constants.FILEPATH_VAR), 83 | titleTemplate: toTemplateTag(constants.FILEPATH_VAR), 84 | classNameTemplate: `Test suite failed to run` 85 | } : {}; 86 | 87 | const suiteOptions = Object.assign({}, options, noResultOptions); 88 | if (noResults) { 89 | addErrorTestResult(suite); 90 | } 91 | 92 | // Build variables for suite name 93 | const filepath = path.relative(appDirectory, suite.testFilePath); 94 | const filename = path.basename(filepath); 95 | const suiteTitle = suite.testResults[0].ancestorTitles[0]; 96 | const displayName = typeof suite.displayName === 'object' 97 | ? suite.displayName.name 98 | : suite.displayName; 99 | 100 | // Build replacement map 101 | const suiteNameVariables: string | any = {}; 102 | suiteNameVariables[constants.FILEPATH_VAR] = filepath; 103 | suiteNameVariables[constants.FILENAME_VAR] = filename; 104 | suiteNameVariables[constants.TITLE_VAR] = suiteTitle; 105 | suiteNameVariables[constants.DISPLAY_NAME_VAR] = displayName; 106 | 107 | // Add properties 108 | const suiteNumTests = suite.numFailingTests + suite.numPassingTests + suite.numPendingTests; 109 | const suiteExecutionTime = executionTime(suite.perfStats.start, suite.perfStats.end); 110 | 111 | const suiteErrors = noResults ? 1 : 0; 112 | const testSuite = { 113 | testsuite: [{ 114 | _attr: { 115 | name: replaceVars(suiteOptions.suiteNameTemplate, suiteNameVariables), 116 | errors: suiteErrors, 117 | failures: suite.numFailingTests, 118 | skipped: suite.numPendingTests, 119 | timestamp: (new Date(suite.perfStats.start)).toISOString().slice(0, -5), 120 | time: suiteExecutionTime, 121 | tests: suiteNumTests 122 | } 123 | }] 124 | }; 125 | 126 | // Update top level testsuites properties 127 | jsonResults.testsuites[0]._attr.failures += suite.numFailingTests; 128 | jsonResults.testsuites[0]._attr.skipped += suite.numPendingTests; 129 | jsonResults.testsuites[0]._attr.errors += suiteErrors; 130 | jsonResults.testsuites[0]._attr.tests += suiteNumTests; 131 | 132 | if (!ignoreSuitePropertiesCheck) { 133 | const junitSuiteProperties = require(junitSuitePropertiesFilePath)(suite); // eslint-disable-line 134 | 135 | // Add any test suite properties 136 | const testSuitePropertyMain: any = { 137 | properties: [] 138 | }; 139 | 140 | Object.keys(junitSuiteProperties).forEach((p) => { 141 | const testSuiteProperty: any = { 142 | property: { 143 | _attr: { 144 | name: p, 145 | value: replaceVars(junitSuiteProperties[p], suiteNameVariables) 146 | } 147 | } 148 | }; 149 | 150 | testSuitePropertyMain.properties.push(testSuiteProperty); 151 | }); 152 | 153 | testSuite.testsuite.push(testSuitePropertyMain); 154 | } 155 | 156 | // Iterate through test cases 157 | suite.testResults.forEach((tc: any) => { 158 | const classname = tc.ancestorTitles.join(suiteOptions.ancestorSeparator); 159 | const testTitle = tc.title; 160 | 161 | // Build replacement map 162 | const testVariables: string | any = {}; 163 | testVariables[constants.FILEPATH_VAR] = filepath; 164 | testVariables[constants.FILENAME_VAR] = filename; 165 | testVariables[constants.SUITENAME_VAR] = suiteTitle; 166 | testVariables[constants.CLASSNAME_VAR] = classname; 167 | testVariables[constants.TITLE_VAR] = testTitle; 168 | testVariables[constants.DISPLAY_NAME_VAR] = displayName; 169 | 170 | const testCase: any = { 171 | 'testcase': [{ 172 | _attr: { 173 | classname: replaceVars(suiteOptions.classNameTemplate, testVariables), 174 | name: replaceVars(suiteOptions.titleTemplate, testVariables), 175 | time: tc.duration / 1000, 176 | file: '', 177 | 178 | }, 179 | }] 180 | }; 181 | 182 | if (suiteOptions.addFileAttribute === 'true') { 183 | testCase.testcase[0]._attr.file = filepath; 184 | } 185 | 186 | if (tc.status === 'failed'|| tc.status === 'error') { 187 | const failureMessages = options.noStackTrace === 'true' && tc.failureDetails ? 188 | tc.failureDetails.map((detail: any) => detail.message) : tc.failureMessages; 189 | 190 | failureMessages.forEach((failure: any) => { 191 | const tagName = tc.status === 'failed' ? 'failure': 'error' 192 | testCase.testcase.push({ 193 | [tagName]: stripAnsi(failure) 194 | }); 195 | }) 196 | } 197 | 198 | if (tc.status === 'pending') { 199 | testCase.testcase.push({ 200 | skipped: {} 201 | }); 202 | } 203 | 204 | testSuite.testsuite.push(testCase); 205 | }); 206 | 207 | // Write stdout console output if available 208 | if (suiteOptions.includeConsoleOutput === 'true' && suite.console && suite.console.length) { 209 | // Stringify the entire console object 210 | // Easier this way because formatting in a readable way is tough with XML 211 | // And this can be parsed more easily 212 | const testSuiteConsole: any = { 213 | 'system-out': { 214 | _cdata: JSON.stringify(suite.console, null, 2) 215 | } 216 | }; 217 | 218 | testSuite.testsuite.push(testSuiteConsole); 219 | } 220 | 221 | // Write short stdout console output if available 222 | if (suiteOptions.includeShortConsoleOutput === 'true' && suite.console && suite.console.length) { 223 | // Extract and then Stringify the console message value 224 | // Easier this way because formatting in a readable way is tough with XML 225 | // And this can be parsed more easily 226 | const testSuiteConsole: any = { 227 | 'system-out': { 228 | _cdata: JSON.stringify(suite.console.map((item: any) => item.message), null, 2) 229 | } 230 | }; 231 | 232 | testSuite.testsuite.push(testSuiteConsole); 233 | } 234 | 235 | jsonResults.testsuites.push(testSuite as any); 236 | }); 237 | 238 | return jsonResults; 239 | }; 240 | --------------------------------------------------------------------------------