diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..59c774b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +node-debug* +.c9/ +*.iml +.idea/ +npm-debug.log +dist diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..a4d1e892 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +*.iml +.idea/ +tsconfig.json +src/ +dist/test +*.map \ No newline at end of file diff --git a/README.md b/README.md index fb691d79..09e761bc 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ -# mocha-testrail-reporter +#Testrails Reporter for Mocha + +Pushes test results into TestRail system. + +## Installation + +```shell +$ npm install mocha-testrail-reporter --save-dev +``` + +## Usage +Run mocha with `mocha-testrail-reporter`: + +```shell +$ mocha test --reporter mocha-testrail-reporter --reporter-options domain=example,username=test@example.com,password=12345678,projectId=1,suiteId=1 +``` +all reporter-options are mandatory except the following: + +- assignedToId + + +Reference: https://github.com/michaelleeallen/mocha-testrail-reporter/blob/master/index.js \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 00000000..1b7332c2 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require("./dist/lib/mocha-testrail-reporter").MochaTestRailReporter \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..ce821b96 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "mocha-testrail-reporter", + "version": "1.0.6", + "description": "A Testrails reporter for mocha.", + "main": "index.js", + "private": false, + "keywords": [ + "mocha", + "testrail", + "reporter" + ], + "author": { + "name": "Pierre Awaragi", + "email": "pierre@awaragi.com" + }, + "license": "MIT", + "readmeFilename": "README.md", + "repository": { + "type": "git", + "url": "https://github.com/awaragi/mocha-testrail-reporter.git" + }, + "bugs": { + "url": "https://github.com/awaragi/mocha-testrail-reporter/issues" + }, + "scripts": { + "tsc": "tsc", + "clean": "rimraf dist", + "test": "mocha dist/test", + "build": "npm run clean && npm run tsc" + }, + "dependencies": { + "btoa": "^1.1.2", + "unirest": "^0.5.1" + }, + "peerDependencies": { + "mocha": "^3 || ^2.2.5" + }, + "devDependencies": { + "@types/chai": "^3.4.34", + "@types/mocha": "^2.2.36", + "@types/node": "^6.0.58", + "chai": "^3.5.0", + "rimraf": "^2.5.4", + "typescript": "^2.1.4" + } +} diff --git a/src/lib/mocha-testrail-reporter.ts b/src/lib/mocha-testrail-reporter.ts new file mode 100644 index 00000000..3319171a --- /dev/null +++ b/src/lib/mocha-testrail-reporter.ts @@ -0,0 +1,97 @@ +import {reporters} from 'mocha'; +import {TestRail} from "./testrail"; +import {titleToCaseIds} from "./shared"; + + +export class MochaTestRailReporter extends reporters.Spec { + private testCases: TestCase[] = []; + private passes: number = 0; + private fails: number = 0; + private pending: number = 0; + private out: string[] = []; + + constructor(runner: any, options: any) { + super(runner); + + let reporterOptions: TestRailOptions = options.reporterOptions; + this.validate(reporterOptions, 'domain'); + this.validate(reporterOptions, 'username'); + this.validate(reporterOptions, 'password'); + this.validate(reporterOptions, 'projectId'); + this.validate(reporterOptions, 'suiteId'); + + runner.on('start', () => { + }); + + runner.on('suite', (suite) => { + }); + + runner.on('suite end', () => { + }); + + runner.on('pending', (test) => { + this.pending++; + this.out.push(test.fullTitle() + ': pending'); + }); + + runner.on('pass', (test) => { + this.passes++; + this.out.push(test.fullTitle() + ': pass'); + let caseIds = titleToCaseIds(test.title); + if (caseIds.length > 0) { + if (test.speed === 'fast') { + let testCases = caseIds.map(caseId => { + return { + caseId: caseId, + pass: true, + comment: test.title + }; + }); + this.testCases.push(...testCases); + } else { + let testCases = caseIds.map(caseId => { + return { + caseId: caseId, + pass: true, + comment: `${test.title} (${test.duration}ms)` + }; + }); + this.testCases.push(...testCases); + } + } + }); + + runner.on('fail', (test) => { + this.fails++; + this.out.push(test.fullTitle() + ': fail'); + let caseIds = titleToCaseIds(test.title); + if (caseIds.length > 0) { + let testCases = caseIds.map(caseId => { + return { + caseId: caseId, + pass: false, + comment: `${test.title} +${test.err}` + }; + }); + this.testCases.push(...testCases); + } + }); + + runner.on('end', () => { + if (this.testCases.length == 0) { + console.warn("No testcases were matched. Ensure that your tests are declared correctly and matches TCxxx"); + } + new TestRail(reporterOptions).publish(this.passes, this.fails, this.pending, this.out, this.testCases); + }); + } + + private validate(options: TestRailOptions, name: string) { + if (options == null) { + throw new Error("Missing --reporter-options in mocha.opts"); + } + if (options[name] == null) { + throw new Error("Missing ${name} value. Please update --reporter-options in mocha.opts"); + } + } +} diff --git a/src/lib/shared.ts b/src/lib/shared.ts new file mode 100644 index 00000000..1d29253d --- /dev/null +++ b/src/lib/shared.ts @@ -0,0 +1,17 @@ +/** + * Search for all applicable test cases + * @param title + * @returns {any} + */ +export function titleToCaseIds(title: string): number[] { + let caseIds: number[] = []; + + let testCaseIdRegExp: RegExp = /\bT?C(\d+)\b/g; + let m; + while((m = testCaseIdRegExp.exec(title)) !== null) { + let caseId = parseInt(m[1]); + caseIds.push(caseId); + } + return caseIds; +} + diff --git a/src/lib/test.interface.ts b/src/lib/test.interface.ts new file mode 100644 index 00000000..236f945c --- /dev/null +++ b/src/lib/test.interface.ts @@ -0,0 +1,5 @@ +interface TestCase { + caseId: number, + pass: boolean, + comment?: String, +} \ No newline at end of file diff --git a/src/lib/testrail-options.interface.ts b/src/lib/testrail-options.interface.ts new file mode 100644 index 00000000..26fdd5ce --- /dev/null +++ b/src/lib/testrail-options.interface.ts @@ -0,0 +1,8 @@ +interface TestRailOptions { + domain: string, + username: string, + password: string, + projectId: number, + suiteId: number, + assignedToId?: number, +} \ No newline at end of file diff --git a/src/lib/testrail.ts b/src/lib/testrail.ts new file mode 100644 index 00000000..c0efcd9d --- /dev/null +++ b/src/lib/testrail.ts @@ -0,0 +1,75 @@ +import btoa = require('btoa'); +import unirest = require("unirest"); + +export class TestRail { + private base: String; + + constructor(private options: TestRailOptions) { + // compute base url + this.base = `https://${options.domain}/index.php`; + } + + private _post(api: String, body: any, callback: Function, error?: Function) { + var req = unirest("POST", this.base) + .query(`/api/v2/${api}`) + .headers({ + "content-type": "application/json" + }) + .type("json") + .send(body) + .auth(this.options.username, this.options.password) + .end((res) => { + if (res.error) { + console.log("Error: %s", JSON.stringify(res.body)); + if (error) { + error(res.error); + } else { + throw new Error(res.error); + } + } + callback(res.body); + }); + } + + publish(passes: number, fails: number, pending: number, out: string[], tests: TestCase[], callback?: Function): void { + let total = passes + fails + pending; + let results: any = []; + for (let test of tests) { + results.push({ + "case_id": test.caseId, + "status_id": test.pass ? 1 : 5, + "comment": test.comment ? test.comment: "", + }); + } + + console.log(`Publishing ${results.length} test result(s) to ${this.base}`) + let executionDateTime = new Date().toISOString(); + this._post(`add_run/${this.options.projectId}`, { + "suite_id": this.options.suiteId, + "name": `Automated test run ${executionDateTime}`, + "description": `Automated test run executed on ${executionDateTime} +Execution summary: +Passes: ${passes} +Fails: ${fails} +Pending: ${pending} +Total: ${total} + +Execution details: +${out.join('\n')} +`, + "assignedto_id": this.options.assignedToId, + "include_all": true + }, (body) => { + const runId = body.id + console.log(`Results published to ${this.base}?/runs/view/${runId}`) + this._post(`add_results_for_cases/${runId}`, { + results: results + }, (body) => { + // execute callback if specified + if(callback) { + callback(); + } + }) + }); + } +} diff --git a/src/test/shared.ts b/src/test/shared.ts new file mode 100644 index 00000000..7416e6a7 --- /dev/null +++ b/src/test/shared.ts @@ -0,0 +1,38 @@ +import * as chai from "chai"; +chai.should(); + +import {titleToCaseIds} from "../lib/shared"; + +describe("Shared functions", () => { + describe("titleToCaseIds", () => { + it("Single test case id present", () => { + let caseIds = titleToCaseIds("TC123 Test title"); + caseIds.length.should.be.equals(1); + caseIds[0].should.be.equals(123); + + caseIds = titleToCaseIds("Execution of TC123 Test title"); + caseIds.length.should.be.equals(1); + caseIds[0].should.be.equals(123); + }); + it("Multiple test case ids present", () => { + let caseIds = titleToCaseIds("Execution TC321 TC123 Test title"); + caseIds.length.should.be.equals(2); + caseIds[0].should.be.equals(321); + caseIds[1].should.be.equals(123); + }); + it("No test case ids present", () => { + let caseIds = titleToCaseIds("Execution Test title"); + caseIds.length.should.be.equals(0); + }); + }); + + describe("Misc tests", () => { + it("String join", () => { + let out: string[] = []; + out.push("Test 1: fail"); + out.push("Test 2: pass"); + out.join('\n').should.be.equals(`Test 1: fail +Test 2: pass`); + }); + }); +}); \ No newline at end of file diff --git a/src/test/testrail.ts b/src/test/testrail.ts new file mode 100644 index 00000000..89eb4fbf --- /dev/null +++ b/src/test/testrail.ts @@ -0,0 +1,23 @@ +import {TestRail} from "../lib/testrail"; + +describe.skip("TestRail API", () => { + it("Publish test run", (done) => { + new TestRail({ + domain: "testingoutone", + username: "testingout.one@mailinator.com", + password: "XyMp8uojG3wkzNNNXiTk-dP4MnBmOiQhVC2xGvmyY", + projectId: 1, + suiteId: 2, + assignedToId: 1, + }).publish(0, 0, 0, ["test 1: pass", "test 2: fail"], [ + { + caseId: 74, + pass: true + }, { + caseId: 75, + pass: false, + comment: "Failure...." + } + ], done); + }) +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..d7806c3b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "removeComments": false, + "noImplicitAny": false, + "pretty": true, + "outDir": "dist", + "typeRoots": ["node_modules/@types"] + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file