├── .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 | [](https://sonarcloud.io/summary/new_code?id=CasualBot_jest-sonar-reporter)
4 | [](https://sonarcloud.io/summary/new_code?id=CasualBot_jest-sonar-reporter)
5 |
6 | [](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 |
--------------------------------------------------------------------------------