Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(reporter): allow setting rootDir config #28

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,62 @@ Package.json example:

With this configuration, you can run the tests with `npm test`. If the `TEAMCITY_VERSION` environment variable is set, it produces the output in TeamCity's format. Otherwise, standard jest output is produced.

## Configuration

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);
}
};
15 changes: 13 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@

const formatter = require("./formatter");

module.exports = function(result) {
module.exports = function(resultOrGlobalConfig, optionsIfReporter) {
const flowId = process.env.TEAMCITY_FLOWID || process.pid.toString();
const teamCityVersion = process.env.TEAMCITY_VERSION;

// Constructor call means usage as a Jest reporter
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target
if (new.target) {
if (teamCityVersion) {
if(!optionsIfReporter) {
optionsIfReporter = {};
}

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

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

return
}

// at this point, we are not a reporter.
const result = resultOrGlobalConfig;

if (teamCityVersion) {
formatter.formatReport(result.testResults, process.cwd(), flowId);
}
Expand Down