Skip to content

Commit

Permalink
feat(reporter): allow customizing test name & suite names
Browse files Browse the repository at this point in the history
  • Loading branch information
russell-dot-js committed Jul 2, 2022
1 parent 1114afd commit eff13fa
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 25 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,57 @@ With this configuration, you can run the tests with `npm test`. If the `TEAMCITY
Jest config currently does not support passing any options to testResultsProcessor config.
However, if used as a reporter, this plugin can be configured.

**rootDir**
To change the rootDir config, pass it as a configuration to your reporter as so:

```javascript
//jest.config.js
"reporters": [["jest-teamcity", {"rootDir": "/path/to/your/dir"}]]
```

**testNameGenerator**
You can change how test names are generated to configure how test names show up in teamcity.
Example:
```javascript
//jest.config.js
reporters: [
[
'jest-teamcity',
{
testNameGenerator: (test) => {
return [...test.ancestorTitles, test.title].join('.');
},
},
],
],
```

**suiteNameGenerator**
You can also now change how suite names are chosen, rather than simply using file path.
Example:
```javascript
//jest.config.js
reporters: [
[
'jest-teamcity',
{
suiteNameGenerator: (test, filename, suites) => {
if (filename.includes('automation/tests')) {
return 'Integration tests';
}

if (filename.indexOf('service/' === 0)) {
const [_, serviceName] = filename.split('/');
return serviceName;
}

return filename;
},
},
],
],
```

### License

MIT © [Ivan Tereshchenkov]
88 changes: 65 additions & 23 deletions lib/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,51 +28,86 @@ module.exports = {
.replace(/'/g, "|'");
},

/**
* Default test name generator
* @param {object} test
* @returns {string}
*/
testName(test) {
if (!test) {
return "";
}
return test.title;
},

/**
* Default suite name generator
* @param {object} test
* @returns {string}
*/
suiteName(test, filename, suites) {
// todo this should probably be easier to read
const path = [filename].concat(test.ancestorTitles);

// find current suite, creating each level if necessary
let currentSuite = suites;
let p;
for (p of path) {
if (!Object.prototype.hasOwnProperty.call(currentSuite, p)) {
currentSuite[p] = {};
}
currentSuite = currentSuite[p];
}
return p;
},


/**
* Prints test message
* @param {object} tests
*/
printTestLog(tests, flowId) {
printTestLog(tests, flowId, nameGenerator) {
if (tests) {
Object.keys(tests).forEach(suiteName => {
if (suiteName === "_tests_") {
// print test details
tests[suiteName].forEach(test => {
this.log(`##teamcity[testStarted name='${this.escape(test.title)}' flowId='${flowId}']`);
const testName = this.escape(nameGenerator(test));
this.log(`##teamcity[testStarted name='${testName}' flowId='${flowId}']`);
switch (test.status) {
case "failed":
if (test.failureMessages) {
test.failureMessages.forEach(error => {
const [message, ...stack] = error.split(errorMessageStackSeparator);
this.log(
`##teamcity[testFailed name='${this.escape(test.title)}' message='${this.escape(
`##teamcity[testFailed name='${testName}' message='${this.escape(
message
)}' details='${this.escape(stack.join(errorMessageStackSeparator))}' flowId='${flowId}']`
);
});
} else {
this.log(`##teamcity[testFailed name='${this.escape(test.title)}' flowId='${flowId}']`);
this.log(`##teamcity[testFailed name='${testName}' flowId='${flowId}']`);
}

break;
case "pending":
this.log(
`##teamcity[testIgnored name='${this.escape(test.title)}' message='pending' flowId='${flowId}']`
`##teamcity[testIgnored name='${testName}' message='pending' flowId='${flowId}']`
);
break;
case "passed":
break;
}
this.log(
`##teamcity[testFinished name='${this.escape(test.title)}' duration='${
`##teamcity[testFinished name='${testName}' duration='${
test.duration
}' flowId='${flowId}']`
);
});
} else {
// print suite names
this.log(`##teamcity[testSuiteStarted name='${this.escape(suiteName)}' flowId='${flowId}']`);
this.printTestLog(tests[suiteName], flowId);
this.printTestLog(tests[suiteName], flowId, nameGenerator);
this.log(`##teamcity[testSuiteFinished name='${this.escape(suiteName)}' flowId='${flowId}']`);
}
});
Expand All @@ -92,7 +127,7 @@ module.exports = {
* @param {array} testResults
* @returns {object}
*/
collectSuites(testResults, cwd) {
collectSuites(testResults, cwd, suiteNameGenerator) {
if (!testResults) {
return {};
}
Expand All @@ -101,32 +136,39 @@ module.exports = {
}

const suites = {};
const suitesByFileName = {};
testResults.forEach(testFile => {
const filename = path.relative(cwd, testFile.testFilePath);
testFile.testResults.forEach(test => {
const path = [filename].concat(test.ancestorTitles);

// find current suite, creating each level if necessary
let currentSuite = suites;
for (const p of path) {
if (!Object.prototype.hasOwnProperty.call(currentSuite, p)) {
currentSuite[p] = {};
}
currentSuite = currentSuite[p];
const suiteName = suiteNameGenerator(test, filename, suites);

if(!suites[suiteName]){
suites[suiteName] = {};
}

const currentSuite = suites[suiteName];
suitesByFileName[filename] = currentSuite;

// last level is array of test results
if (!currentSuite["_tests_"]) {
currentSuite["_tests_"] = [];
}

// add the current test
currentSuite["_tests_"].push(test);
currentSuite["_tests_"].push({
...test,
filename,
});
});

if (testFile.testExecError) {
suites[filename] = suites[filename] || {};
suites[filename]['_tests_'] = [{
if(!suitesByFileName[filename]) {
suites[filename] = {};
}

const suite = suitesByFileName[filename] || suites[filename];

suite['_tests_'] = [{
status: 'failed',
title: 'Jest failed to execute suite',
duration: 0,
Expand All @@ -142,8 +184,8 @@ module.exports = {
* Formats and outputs tests results
* @param {array} testResults
*/
formatReport(testResults, cwd, flowId) {
const suites = this.collectSuites(testResults, cwd);
this.printTestLog(suites, flowId);
formatReport(testResults, cwd, flowId, testNameGenerator, suiteNameGenerator) {
const suites = this.collectSuites(testResults, cwd, suiteNameGenerator || this.suiteName);
this.printTestLog(suites, flowId, testNameGenerator || this.testName);
}
};
10 changes: 8 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ module.exports = function(resultOrGlobalConfig, optionsIfReporter) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target
if (new.target) {
if (teamCityVersion) {
const rootDir = (optionsIfReporter && optionsIfReporter.rootDir) || process.cwd();
if(!optionsIfReporter) {
optionsIfReporter = {};
}

const rootDir = optionsIfReporter.rootDir || process.cwd();
const testNameGenerator = optionsIfReporter.testNameGenerator;
const suiteNameGenerator = optionsIfReporter.suiteNameGenerator;

this.onTestResult = (_, result) => {
formatter.formatReport([result], rootDir, flowId)
formatter.formatReport([result], rootDir, flowId, testNameGenerator, suiteNameGenerator)
}
}

Expand Down

0 comments on commit eff13fa

Please sign in to comment.