/** * @exports TestRail * @requires sync-request */ const request = require('sync-request'); /** * TestRail basic API wrapper */ class TestRail { /** * @param {Object} options - wdio TestRail specifc configurations * @param {string} options.domain - Domain for TestRail * @param {number} options.projectId - Project identifier * @param {Array.<number>} options.suiteId - List of suites identifiers * @param {number} [options.assignedToId] - User identifier * @param {string} options.username - User email * @param {string} options.password - User API key * @param {Boolean} options.includeAll - Flag to inlcude all tests from a suite in a run * @param {number} [options.updateRun] - Test run identifier for test run to update * @param {number} [options.updatePlan] - Test plan identifier for a test plan to update */ constructor(options) { this._validate(options, 'domain'); this._validate(options, 'username'); this._validate(options, 'password'); this._validate(options, 'projectId'); this._validate(options, 'suiteId'); this._validate(options, 'includeAll'); // compute base url this.options = options; this.base = `https://${options.domain}/index.php`; } /** * Verifies if required options exist in webdriverio config file * * @param {Object} options - wdio TestRail specifc configurations * @param {string} options.domain - Domain for TestRail * @param {number} options.projectId - Project identifier * @param {Array.<number>} options.suiteId - List of suites identifiers * @param {number} [options.assignedToId] - User identifier * @param {string} options.username - User email * @param {string} options.password - User API key * @param {Boolean} options.includeAll - Flag to inlcude all tests from a suite in a run * @param {number} [options.updateRun] - Test run identifier for test run to update * @param {number} [options.updatePlan] - Test plan identifier for a test plan to update * @param {string} name - Name of the property * @private */ _validate(options, name) { if (options == null) { throw new Error("Missing testRailsOptions in wdio.conf"); } if (options[name] == null) { throw new Error(`Missing ${name} value. Please update testRailsOptions in wdio.conf`); } } /** * Construct and returns an API path * * @param {string} path - The path for the API * @return {string} Constructed URL path to TestRail API * @private */ _url(path) { return `${this.base}?${path}`; } /** * Makes a POST request on a TestRail API * * @param {string} api - API path * @param {*} body - Body of request * @param {callback} error - Callback to handle errors * @return {*} Response object * @private */ _post(api, body, error = undefined) { return this._request("POST", api, body, error); } /** * Makes a GET request on a TestRail API * * @param {string} api - API path * @param {callback} error - Callback to handle errors * @return {*} Response object * @private */ _get(api, error = undefined) { return this._request("GET", api, null, error); } /** * Makes a request to the TestRail API * * @param {string} method - Type of request to make * @param {string} api - API path * @param {*} body Body of request * @param {callback} error * @return {*} API response * @private */ _request(method, api, body, error = undefined) { let options = { headers: { "Authorization": "Basic " + new Buffer(this.options.username + ":" + this.options.password).toString("base64"), "Content-Type": "application/json" }, }; if (body) { options['json'] = body; } let result = request(method, this._url(`/api/v2/${api}`), options); result = JSON.parse(result.getBody('utf8')); if (result.error) { console.log("Error: %s", JSON.stringify(result.body)); if (error) { error(result.error); } else { throw new Error(result.error); } } return result; } /** * Creates a new array of unique data from the data * within 2 existing arrays * * @param {Array.<*>} currArr * @param {Array.<*>} newArr * @returns {array} * @private */ _createUniqueArray(currArr, newArr) { return [...new Set([...newArr, ...currArr])] } /** * Creates a new test plan * * @param {string} name - Plan name * @param {string} desc - Plane description * @param {Array.<Object>} testRuns - Test runs * @returns {*} API response */ addPlan(name, description, testRuns) { return this._post(`add_plan/${this.options.projectId}`, { "name": name, "description": description, "entries": testRuns }); } /** * Retrieves a test plan * * @param {number} planId - Plan identifier * @returns {*} API response */ getPlan(planId) { return this._get(`get_plan/${this.options.updatePlan}`); } /** * Adds a test plan entry to the current project * * @param {number} planId - Plan identifier * @param {number} suiteId - Suite identifier * @param {string} name - Plan name * @param {string} desc - Plan name * @param {Array.<Object>} runs - Test runs * @param {Array.<number>} caseIds - Test case identifiers * @return {*} API response */ addTestPlanEntry(planId, suiteId, name, desc, runs, caseIds) { return this._post( `add_plan_entry/${planId}`, { 'include_all': this.options.includeAll, 'suite_id': suiteId, 'name': name, 'description': desc, 'runs': runs, 'case_ids': caseIds }); } /** * Adds missing case ids to a test plan entry * * @param {number} planId - Plan identifier * @param {number} entryId - Entry identifier * @param {Array.<number>} caseIds - Test case identifiers * @return {*} API response */ updateTestPlanEntry(planId, entryId, caseIds) { return this._post(`update_plan_entry/${planId}/${entryId}`, { case_ids: caseIds }); } /** * Gets a suite * * @param {number} suiteId - Suite identifier * @return {*} API response */ getSuite(suiteId) { return this._get(`get_suite/${suiteId}`); } /** * Gets all the tests in a run * * @param {number} runId - Run identifier * @return {*} API response */ getTestsForRun(runId) { return this._get(`get_tests/${runId}`) } /** * Adds a test run * * @param {string} name - Test run name * @param {string} description - Test run description * @param {number} suiteId - Suite id for test cases in this run * @return {*} API response */ addRun(name, description, suiteId, caseIds) { return this._post(`add_run/${this.options.projectId}`, { "suite_id": suiteId, "name": name, "description": description, "assignedto_id": this.options.assignedToId, "include_all": this.options.includeAll, "case_ids": caseIds }); } /** * Adds test cases to a test run * * @param {number} runId - Run identifier * @param {Array.<Object>} cases - Test case data * @return {*} API response */ addCasesToRun(runId, cases) { const currentCases = this.getTestsForRun(runId).map(c => c.case_id); //console.log([...currentCases, ...cases]) this._post(`update_run/${runId}`, { 'case_ids': this._createUniqueArray(currentCases, cases) }); } /** * Get test cases that belong to a suite * * @param {*} projectId - Project identifier * @param {*} suiteId - Suite identifier * @return {*} API response */ getTestsForSuite(projectId, suiteId) { return this._get(`get_cases/${projectId}&suite_id=${suiteId}`); } /** * Adds test results for a test cases * * @param {number} runId - Run identifier * @param {Array.<Object>} results - Test case results * * @return {*} API response */ addResultsForCases(runId, results) { if (results.length > 0) { return this._post(`add_results_for_cases/${runId}`, { results: results }); } } /** * Publishes results of execution of an automated test run * * @param {string} name - Test run/plan name * @param {string} description * @param {Array.<Object>} results * @param {callback} callback */ publish(name, description, results, runners, callback = undefined) { let runs = []; let body = null; let plan = null if (typeof this.options.suiteId !== 'number') { if (this.options.updatePlan) { //1. find our existing plan plan = this.getPlan(this.options.updatePlan); //console.log(plan); plan.entries.forEach(entry => { //console.log(entry.runs) //console.log(entry.runs[0].id) const suiteInfo = this.getSuite(entry.runs[0].suite_id); const currentCases = this.getTestsForRun(entry.runs[0].id).map(c => c.case_id); suiteInfo.cases = this.getTestsForSuite(this.options.projectId, suiteInfo.id).map(c => c.id); suiteInfo.newCases = results.filter(r => suiteInfo.cases.includes(r.case_id)); //reset case id listing this.updateTestPlanEntry( plan.id, entry.id, this._createUniqueArray( currentCases, suiteInfo.newCases.map(r => r.case_id) ) ) body = []; //add new results body.push(this.addResultsForCases(entry.runs[0].id, suiteInfo.newCases)); }) } else { let testPlanEntries = [] //1. create the test plan plan = this.addPlan(name, description, []); runners.forEach((runner, runnerIdx) => { this.options.suiteId.forEach((suiteId, suiteIdx) => { const suiteInfo = this.getSuite(suiteId); suiteInfo.cases = this.getTestsForSuite(this.options.projectId, suiteId).map(c => c.id); //runs = testPlanEntries.push( this.addTestPlanEntry( plan.id, suiteId, `${suiteInfo.name} - ${runner.deviceName||runner.platform} - ${runner.browserName}`, description, [], results.filter(result => { return JSON.stringify(result.runner) === JSON.stringify(runner); }).map(result => result.case_id)).runs[0]); // console.log('\n\n'); // console.log(testPlanEntries) // console.log('-------------'); body = []; body.push( this.addResultsForCases( testPlanEntries.slice(-1)[0].id, results.filter(r => suiteInfo.cases.includes(r.case_id) && JSON.stringify(r.runner) === JSON.stringify(runner)) ) ); }) }) } console.log(`Results published to ${this.base}?/plans/view/${plan.id}`); console.log(`Add updatePlan: ${plan.id} to your config to update this plan.`); } else { if (this.options.updateRun) { //update run here runs[0] = { id: this.options.updateRun }; //add any missing test case ids to a run this.addCasesToRun(runs[0].id, results.map(r => r.case_id)); } else { // console.log('runners') // console.log(runners) runners.forEach((runner, idx) => { const runName = `${name} - (${runner.deviceName||runner.platform} - ${runner.browserName})` runs.push(this.addRun(runName, description, this.options.suiteId, results.filter(result => { return JSON.stringify(result.runner) === JSON.stringify(runner); }).map(result => result.case_id))); this.addResultsForCases(runs[idx].id, results); }) } //body = this.addResultsForCases(runs[0].id, results); console.log(`Results published to ${this.base}?/runs/view/${runs[0].id}`); console.log(`Add updateRun: ${runs[0].id} to your config to update this run.`); } // execute callback if specified if (callback) { callback(body); } } } module.exports = TestRail;