testcomplete-qatouch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # testcomplete-qatouch
2
+
3
+ > QA Touch reporter for **TestComplete** (SmartBear).
4
+ > Reads JUnit XML files exported from TestComplete and automatically creates
5
+ > modules, test cases, and a test run in QA Touch.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install testcomplete-qatouch
11
+ ```
12
+
13
+ No peer dependencies — works with any Node.js >= 16 project.
14
+
15
+ ## Usage
16
+
17
+ Export results from TestComplete via **File > Export > JUnit XML** (or use the
18
+ `tcDumpJUnit` CLI utility), then create an `upload-results.ts` file:
19
+
20
+ ```ts
21
+ import { TestCompleteQATouchReporter } from "testcomplete-qatouch";
22
+
23
+ const reporter = new TestCompleteQATouchReporter({
24
+ domain: "mycompany",
25
+ apiToken: "YOUR_API_TOKEN",
26
+ projectKey: "PROJ",
27
+ assignTo: "user-key",
28
+ resultsFile: "./TestResults/junit.xml",
29
+ milestoneName: "Sprint 12",
30
+ tag: "testcomplete",
31
+ });
32
+
33
+ await reporter.run();
34
+ ```
35
+
36
+ Run it:
37
+
38
+ ```bash
39
+ npx ts-node upload-results.ts
40
+ ```
41
+
42
+ ## Options
43
+
44
+ | Option | Type | Default | Description |
45
+ |---|---|---|---|
46
+ | `domain` | `string` | — | QA Touch subdomain |
47
+ | `apiToken` | `string` | — | QA Touch API token |
48
+ | `projectKey` | `string` | — | QA Touch project key |
49
+ | `assignTo` | `string` | — | User key to assign the test run |
50
+ | `resultsFile` | `string` | — | Path to the JUnit XML file |
51
+ | `testsuiteId` | `string?` | auto | Fixed module key — skips auto-create |
52
+ | `milestoneName` | `string?` | `"TestComplete Automation"` | Milestone name |
53
+ | `milestoneKey` | `string?` | — | Existing milestone key |
54
+ | `createCases` | `boolean?` | `true` | Auto-create missing test cases |
55
+ | `tag` | `string?` | `"testcomplete"` | Tag applied to the test run |
56
+
57
+ ## How to export JUnit XML from TestComplete
58
+
59
+ 1. Run your TestComplete project suite.
60
+ 2. **File > Export > JUnit XML Report** — or use the CLI:
61
+ ```
62
+ tcDumpJUnit /results:"%TESTCOMPLETE_LOG%" /output:"TestResults\junit.xml"
63
+ ```
64
+ 3. Point `resultsFile` at the output file.
65
+
66
+ ## CI example
67
+
68
+ ```yaml
69
+ - name: Run TestComplete tests
70
+ run: |
71
+ "C:\Program Files\SmartBear\TestComplete\bin\TestComplete.exe" MyProject.pjs /run /exit
72
+ tcDumpJUnit /results:"%TC_LOG_FILE%" /output:"TestResults\junit.xml"
73
+
74
+ - name: Upload to QA Touch
75
+ run: npx ts-node upload-results.ts
76
+ ```
@@ -0,0 +1,2 @@
1
+ export { TestCompleteQATouchReporter } from "./reporter";
2
+ export type { TestCompleteQATouchReporterOptions } from "./reporter";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestCompleteQATouchReporter = void 0;
4
+ var reporter_1 = require("./reporter");
5
+ Object.defineProperty(exports, "TestCompleteQATouchReporter", { enumerable: true, get: function () { return reporter_1.TestCompleteQATouchReporter; } });
@@ -0,0 +1,27 @@
1
+ export interface QATouchClientOptions {
2
+ domain: string;
3
+ apiToken: string;
4
+ projectKey: string;
5
+ }
6
+ export declare class QATouchClient {
7
+ private domain;
8
+ private apiToken;
9
+ private projectKey;
10
+ constructor(options: QATouchClientOptions);
11
+ private request;
12
+ private multipartRequest;
13
+ private normalizeList;
14
+ getTestCases(moduleKey?: string): Promise<any[]>;
15
+ createTestCase(moduleKey: string, title: string, description: string, reference: string, precondition: string, stepsTemplate: string): Promise<void>;
16
+ getModules(): Promise<any[]>;
17
+ createModule(moduleName: string, parentKey?: string): Promise<any>;
18
+ getMilestones(): Promise<any[]>;
19
+ createMilestone(name: string): Promise<void>;
20
+ createTestRun(milestoneKey: string, title: string, assignTo: string, caseKeys: string[], tags?: string): Promise<any>;
21
+ createTestRunByModules(milestoneKey: string, title: string, assignTo: string, moduleKeys: string[], tags?: string): Promise<any>;
22
+ updateResults(testRunKey: string, results: Array<{
23
+ case: string;
24
+ status: number;
25
+ }>): Promise<any>;
26
+ addResultWithComment(testRunKey: string, caseKey: string, statusId: number, comments?: string, screenshotPath?: string): Promise<any>;
27
+ }
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.QATouchClient = void 0;
37
+ const https = __importStar(require("https"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ class QATouchClient {
41
+ constructor(options) {
42
+ this.domain = options.domain;
43
+ this.apiToken = options.apiToken;
44
+ this.projectKey = options.projectKey;
45
+ }
46
+ request(method, endpoint) {
47
+ return new Promise((resolve, reject) => {
48
+ const headers = {
49
+ "api-token": this.apiToken,
50
+ domain: this.domain,
51
+ "Content-Type": "application/json",
52
+ };
53
+ if (method !== "GET") {
54
+ headers["Content-Length"] = "0";
55
+ }
56
+ const req = https.request({
57
+ hostname: "api.qatouch.com",
58
+ path: `/api/v1${endpoint}`,
59
+ method,
60
+ headers,
61
+ }, (res) => {
62
+ let body = "";
63
+ res.on("data", (chunk) => {
64
+ body += chunk;
65
+ });
66
+ res.on("end", () => {
67
+ let parsed = {};
68
+ if (body) {
69
+ try {
70
+ parsed = JSON.parse(body);
71
+ }
72
+ catch {
73
+ reject(new Error(`Non-JSON response (${res.statusCode}) for ${endpoint}`));
74
+ return;
75
+ }
76
+ }
77
+ if (res.statusCode >= 200 && res.statusCode < 300) {
78
+ resolve(parsed);
79
+ return;
80
+ }
81
+ reject(new Error(parsed.error_msg ||
82
+ parsed.error ||
83
+ `API Error ${res.statusCode} for ${endpoint}`));
84
+ });
85
+ });
86
+ req.on("error", reject);
87
+ req.end();
88
+ });
89
+ }
90
+ multipartRequest(endpoint, filePath) {
91
+ return new Promise((resolve, reject) => {
92
+ const boundary = `----QATouchBoundary${Date.now()}`;
93
+ const fileName = path.basename(filePath);
94
+ const fileData = fs.readFileSync(filePath);
95
+ const preamble = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file[]"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`);
96
+ const epilogue = Buffer.from(`\r\n--${boundary}--\r\n`);
97
+ const body = Buffer.concat([preamble, fileData, epilogue]);
98
+ const req = https.request({
99
+ hostname: "api.qatouch.com",
100
+ path: `/api/v1${endpoint}`,
101
+ method: "POST",
102
+ headers: {
103
+ "api-token": this.apiToken,
104
+ domain: this.domain,
105
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
106
+ "Content-Length": body.length,
107
+ },
108
+ }, (res) => {
109
+ let resBody = "";
110
+ res.on("data", (chunk) => {
111
+ resBody += chunk;
112
+ });
113
+ res.on("end", () => {
114
+ let parsed = {};
115
+ if (resBody) {
116
+ try {
117
+ parsed = JSON.parse(resBody);
118
+ }
119
+ catch {
120
+ reject(new Error(`Non-JSON response (${res.statusCode}) for ${endpoint}`));
121
+ return;
122
+ }
123
+ }
124
+ if (res.statusCode >= 200 && res.statusCode < 300) {
125
+ resolve(parsed);
126
+ return;
127
+ }
128
+ reject(new Error(parsed.error_msg ||
129
+ parsed.error ||
130
+ `API Error ${res.statusCode} for ${endpoint}`));
131
+ });
132
+ });
133
+ req.on("error", reject);
134
+ req.write(body);
135
+ req.end();
136
+ });
137
+ }
138
+ normalizeList(response) {
139
+ if (Array.isArray(response))
140
+ return response;
141
+ if (Array.isArray(response.data))
142
+ return response.data;
143
+ if (Array.isArray(response.items))
144
+ return response.items;
145
+ return [];
146
+ }
147
+ async getTestCases(moduleKey) {
148
+ const all = [];
149
+ let page = 1;
150
+ for (;;) {
151
+ const query = moduleKey
152
+ ? `moduleKey=${moduleKey}&page=${page}`
153
+ : `mode=automation&page=${page}`;
154
+ const response = await this.request("GET", `/getAllTestCases/${this.projectKey}?${query}`);
155
+ const batch = this.normalizeList(response);
156
+ if (batch.length === 0)
157
+ break;
158
+ all.push(...batch);
159
+ const total = Number(response?.meta?.total) || 0;
160
+ if (total > 0 && all.length >= total)
161
+ break;
162
+ if (batch.length < Number(response?.meta?.per_page || 50))
163
+ break;
164
+ page++;
165
+ if (page > 200)
166
+ break;
167
+ }
168
+ return all;
169
+ }
170
+ async createTestCase(moduleKey, title, description, reference, precondition, stepsTemplate) {
171
+ const endpoint = `/testCase/steps?projectKey=${this.projectKey}&sectionKey=${moduleKey}&caseTitle=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&reference=${encodeURIComponent(reference)}&precondition=${encodeURIComponent(precondition)}&steps_template=${stepsTemplate}&mode=automation`;
172
+ await this.request("POST", endpoint);
173
+ }
174
+ async getModules() {
175
+ const all = [];
176
+ let page = 1;
177
+ for (;;) {
178
+ const response = await this.request("GET", `/getAllModules/${this.projectKey}?page=${page}`);
179
+ const batch = this.normalizeList(response);
180
+ if (batch.length === 0)
181
+ break;
182
+ all.push(...batch);
183
+ const total = Number(response?.meta?.total) || 0;
184
+ if (total > 0 && all.length >= total)
185
+ break;
186
+ if (batch.length < Number(response?.meta?.per_page || 20))
187
+ break;
188
+ page++;
189
+ if (page > 50)
190
+ break;
191
+ }
192
+ return all;
193
+ }
194
+ async createModule(moduleName, parentKey) {
195
+ let endpoint = `/module?projectKey=${this.projectKey}&moduleName=${encodeURIComponent(moduleName)}`;
196
+ if (parentKey) {
197
+ endpoint += `&parentKey=${encodeURIComponent(parentKey)}`;
198
+ }
199
+ const response = await this.request("POST", endpoint);
200
+ if (response?.data && typeof response.data === "object" && !Array.isArray(response.data)) {
201
+ return response.data;
202
+ }
203
+ const list = this.normalizeList(response);
204
+ return list.length > 0 ? list[0] : response;
205
+ }
206
+ async getMilestones() {
207
+ const response = await this.request("GET", `/getAllMilestones/${this.projectKey}`);
208
+ return this.normalizeList(response);
209
+ }
210
+ async createMilestone(name) {
211
+ await this.request("POST", `/milestone?projectKey=${this.projectKey}&milestone=${encodeURIComponent(name)}`);
212
+ }
213
+ async createTestRun(milestoneKey, title, assignTo, caseKeys, tags) {
214
+ const params = [
215
+ `projectKey=${this.projectKey}`,
216
+ `milestoneKey=${encodeURIComponent(milestoneKey)}`,
217
+ `testRun=${encodeURIComponent(title)}`,
218
+ `assignTo=${encodeURIComponent(assignTo)}`,
219
+ ];
220
+ if (tags)
221
+ params.push(`tags=${encodeURIComponent(tags)}`);
222
+ for (const key of caseKeys) {
223
+ params.push(`caseId[]=${encodeURIComponent(key)}`);
224
+ }
225
+ const response = await this.request("POST", `/testRun/specific?${params.join("&")}`);
226
+ const payload = this.normalizeList(response)[0] ||
227
+ response.data?.[0] ||
228
+ response.data ||
229
+ {};
230
+ return payload;
231
+ }
232
+ async createTestRunByModules(milestoneKey, title, assignTo, moduleKeys, tags) {
233
+ const params = [
234
+ `projectKey=${this.projectKey}`,
235
+ `milestoneKey=${encodeURIComponent(milestoneKey)}`,
236
+ `testRun=${encodeURIComponent(title)}`,
237
+ `assignTo=${encodeURIComponent(assignTo)}`,
238
+ `mode=automation`,
239
+ ];
240
+ if (tags)
241
+ params.push(`tags=${encodeURIComponent(tags)}`);
242
+ for (const key of moduleKeys) {
243
+ params.push(`moduleKey[]=${encodeURIComponent(key)}`);
244
+ }
245
+ const response = await this.request("POST", `/testRun/specific/module/mode?${params.join("&")}`);
246
+ const payload = this.normalizeList(response)[0] ||
247
+ response.data?.[0] ||
248
+ response.data ||
249
+ {};
250
+ return payload;
251
+ }
252
+ async updateResults(testRunKey, results) {
253
+ const casesObj = {};
254
+ results.forEach((r, i) => {
255
+ casesObj[String(i)] = r;
256
+ });
257
+ const endpoint = `/runresult/updatestatus/multiple?project=${this.projectKey}&test_run=${testRunKey}&cases=${encodeURIComponent(JSON.stringify(casesObj))}`;
258
+ return this.request("POST", endpoint);
259
+ }
260
+ async addResultWithComment(testRunKey, caseKey, statusId, comments, screenshotPath) {
261
+ const statusName = statusIdToName(statusId);
262
+ let endpoint = `/testRunResults/add/results?status=${encodeURIComponent(statusName)}&project=${encodeURIComponent(this.projectKey)}&test_run=${encodeURIComponent(testRunKey)}&run_result[]=CASE${encodeURIComponent(caseKey)}`;
263
+ if (comments) {
264
+ endpoint += `&comments=${encodeURIComponent(comments)}`;
265
+ }
266
+ if (screenshotPath && fs.existsSync(screenshotPath)) {
267
+ const stat = fs.statSync(screenshotPath);
268
+ if (stat.size <= 2 * 1024 * 1024) {
269
+ return this.multipartRequest(endpoint, screenshotPath);
270
+ }
271
+ }
272
+ return this.request("POST", endpoint);
273
+ }
274
+ }
275
+ exports.QATouchClient = QATouchClient;
276
+ function statusIdToName(id) {
277
+ switch (id) {
278
+ case 1: return "passed";
279
+ case 3: return "blocked";
280
+ case 4: return "retest";
281
+ case 5: return "failed";
282
+ case 6: return "not-applicable";
283
+ case 7: return "in-progress";
284
+ default: return "untested";
285
+ }
286
+ }
@@ -0,0 +1,20 @@
1
+ export interface TestCompleteQATouchReporterOptions {
2
+ domain: string;
3
+ apiToken: string;
4
+ projectKey: string;
5
+ assignTo: string;
6
+ resultsFile: string;
7
+ testsuiteId?: string;
8
+ milestoneName?: string;
9
+ milestoneKey?: string;
10
+ createCases?: boolean;
11
+ tag?: string;
12
+ }
13
+ export declare class TestCompleteQATouchReporter {
14
+ private opts;
15
+ private client;
16
+ constructor(options: TestCompleteQATouchReporterOptions);
17
+ run(): Promise<void>;
18
+ private resolveMilestone;
19
+ private resolveModules;
20
+ }
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TestCompleteQATouchReporter = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const qatouch_client_1 = require("./qatouch-client");
39
+ function normalizeTitle(t) { return t.replace(/[^a-z0-9\s]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase(); }
40
+ function extractCaseKey(i) { return i.case_key || i.caseKey || i.key || i.api_key || i.apiKey || i.id || null; }
41
+ function extractRunKey(i) { return i.testrun_id || i.key || i.testRunKey || i.testrunkey || i.test_run || null; }
42
+ function extractMilestoneKey(i) { return i.milestone_key || i.key || i.milestoneKey || i.id || null; }
43
+ function extractModuleKey(i) { return i.section_key || i.module_key || i.key || i.sectionKey || i.api_key || i.apiKey || i.id || null; }
44
+ function extractModuleName(i) { return i.section_name || i.module_name || i.name || i.title || i.sectionName || null; }
45
+ function statusToId(s) { switch (s) {
46
+ case "passed": return 1;
47
+ case "failed": return 5;
48
+ case "blocked": return 3;
49
+ default: return 2;
50
+ } }
51
+ function buildStepsTemplate(title) {
52
+ const steps = {
53
+ "0": { steps: `Open the application for: ${title}.`, expected_result: "Application is in the expected initial state." },
54
+ "1": { steps: `Execute the TestComplete automated scenario: ${title}.`, expected_result: "All keyword/scripted interactions complete without errors." },
55
+ "2": { steps: `Validate checkpoints for: ${title}.`, expected_result: "All TestComplete checkpoints pass." },
56
+ };
57
+ return encodeURIComponent(JSON.stringify(steps));
58
+ }
59
+ function parseJUnitXml(xml) {
60
+ const tests = [];
61
+ const suiteRe = /<testsuite\s([^>]*)>([\s\S]*?)<\/testsuite>/gi;
62
+ for (const sm of xml.matchAll(suiteRe)) {
63
+ const sa = sm[1] || "";
64
+ const si = sm[2] || "";
65
+ const snm = sa.match(/name="([^"]*)"/i);
66
+ const suiteName = snm ? snm[1] : "Default";
67
+ const caseRe = /<testcase\s([^>]*)(?:\/>|>([\s\S]*?)<\/testcase>)/gi;
68
+ for (const cm of si.matchAll(caseRe)) {
69
+ const ca = cm[1] || "";
70
+ const ci = cm[2] || "";
71
+ const get = (a) => { const m = ca.match(new RegExp(`${a}="([^"]*)"`, "i")); return m ? m[1] : ""; };
72
+ const title = get("name");
73
+ if (!title)
74
+ continue;
75
+ let status = "passed";
76
+ let errorMessage;
77
+ if (/<failure/i.test(ci) || /<error/i.test(ci)) {
78
+ status = "failed";
79
+ const mm = ci.match(/<(?:failure|error)[^>]*message="([^"]*)"/i);
80
+ if (mm)
81
+ errorMessage = mm[1].slice(0, 2000);
82
+ else {
83
+ const tm = ci.match(/<(?:failure|error)[^>]*>([\s\S]*?)<\/(?:failure|error)>/i);
84
+ if (tm)
85
+ errorMessage = tm[1].replace(/<[^>]+>/g, "").trim().slice(0, 2000);
86
+ }
87
+ }
88
+ else if (/<skipped/i.test(ci))
89
+ status = "blocked";
90
+ tests.push({ fullName: `${suiteName}.${title}`, title, normalizedTitle: normalizeTitle(title), suiteName, status, errorMessage });
91
+ }
92
+ }
93
+ return tests;
94
+ }
95
+ class TestCompleteQATouchReporter {
96
+ constructor(options) {
97
+ this.opts = {
98
+ domain: options.domain, apiToken: options.apiToken, projectKey: options.projectKey,
99
+ assignTo: options.assignTo, resultsFile: options.resultsFile,
100
+ testsuiteId: options.testsuiteId, milestoneName: options.milestoneName || "TestComplete Automation",
101
+ milestoneKey: options.milestoneKey, createCases: options.createCases !== false,
102
+ tag: options.tag || "testcomplete",
103
+ };
104
+ this.client = new qatouch_client_1.QATouchClient({ domain: this.opts.domain, apiToken: this.opts.apiToken, projectKey: this.opts.projectKey });
105
+ }
106
+ async run() {
107
+ const tests = parseJUnitXml(fs.readFileSync(this.opts.resultsFile, "utf-8"));
108
+ if (tests.length === 0) {
109
+ console.warn("[testcomplete-qatouch] No test cases found.");
110
+ return;
111
+ }
112
+ const suiteMap = new Map();
113
+ for (const t of tests) {
114
+ const l = suiteMap.get(t.suiteName) || [];
115
+ l.push(t);
116
+ suiteMap.set(t.suiteName, l);
117
+ }
118
+ const mkByS = await this.resolveModules([...suiteMap.keys()]);
119
+ const caseMap = new Map();
120
+ for (const mk of new Set(mkByS.values())) {
121
+ for (const item of await this.client.getTestCases(mk)) {
122
+ const t = (item.title || item.case_title || "").trim();
123
+ const k = extractCaseKey(item);
124
+ if (t && k)
125
+ caseMap.set(normalizeTitle(t), k);
126
+ }
127
+ }
128
+ if (this.opts.createCases) {
129
+ for (const [sn, st] of suiteMap) {
130
+ const mk = mkByS.get(sn);
131
+ if (!mk)
132
+ continue;
133
+ for (const test of st) {
134
+ if (caseMap.has(test.normalizedTitle))
135
+ continue;
136
+ await this.client.createTestCase(mk, test.title, `Automated TestComplete scenario: ${test.title}`, test.fullName, "The TestComplete project is configured and the AUT is running.", buildStepsTemplate(test.title));
137
+ for (const item of await this.client.getTestCases(mk)) {
138
+ const t = (item.title || item.case_title || "").trim();
139
+ const k = extractCaseKey(item);
140
+ if (t && k)
141
+ caseMap.set(normalizeTitle(t), k);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ const milestoneKey = await this.resolveMilestone();
147
+ if (!milestoneKey)
148
+ throw new Error("[testcomplete-qatouch] Unable to resolve milestone key.");
149
+ const bulk = [];
150
+ const detailed = [];
151
+ const caseKeys = [];
152
+ for (const test of tests) {
153
+ const ck = caseMap.get(test.normalizedTitle);
154
+ if (!ck) {
155
+ console.warn(`[testcomplete-qatouch] Unmapped: ${test.title}`);
156
+ continue;
157
+ }
158
+ caseKeys.push(ck);
159
+ if (test.status === "failed" && test.errorMessage)
160
+ detailed.push({ caseKey: ck, test });
161
+ else
162
+ bulk.push({ case: ck, status: statusToId(test.status) });
163
+ }
164
+ if (caseKeys.length === 0) {
165
+ console.warn("[testcomplete-qatouch] No mapped cases.");
166
+ return;
167
+ }
168
+ const rp = await this.client.createTestRun(milestoneKey, `TestComplete Run ${new Date().toISOString()}`, this.opts.assignTo, [...new Set(caseKeys)], this.opts.tag);
169
+ const trk = extractRunKey(rp);
170
+ if (!trk)
171
+ throw new Error("[testcomplete-qatouch] Test run returned no key.");
172
+ for (const { caseKey, test } of detailed) {
173
+ try {
174
+ await this.client.addResultWithComment(trk, caseKey, statusToId(test.status), test.errorMessage);
175
+ }
176
+ catch (e) {
177
+ console.error(`[testcomplete-qatouch] ${e.message}`);
178
+ bulk.push({ case: caseKey, status: statusToId(test.status) });
179
+ }
180
+ }
181
+ if (bulk.length > 0) {
182
+ try {
183
+ const r = await this.client.updateResults(trk, bulk);
184
+ if (r?.error)
185
+ throw new Error(r.error);
186
+ }
187
+ catch {
188
+ for (const item of bulk) {
189
+ try {
190
+ await this.client.addResultWithComment(trk, item.case, item.status);
191
+ }
192
+ catch (e) {
193
+ console.error(`[testcomplete-qatouch] ${e.message}`);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ console.log(`[testcomplete-qatouch] Uploaded ${caseKeys.length} result(s) to run ${trk}.`);
199
+ }
200
+ async resolveMilestone() {
201
+ if (this.opts.milestoneKey)
202
+ return this.opts.milestoneKey;
203
+ const name = this.opts.milestoneName;
204
+ const find = (list) => list.find((m) => (m.milestone_name || m.name || m.title || m.milestone || "") === name);
205
+ let ms = await this.client.getMilestones();
206
+ if (find(ms))
207
+ return extractMilestoneKey(find(ms));
208
+ await this.client.createMilestone(name);
209
+ ms = await this.client.getMilestones();
210
+ return find(ms) ? extractMilestoneKey(find(ms)) : null;
211
+ }
212
+ async resolveModules(suiteNames) {
213
+ const mm = new Map();
214
+ if (this.opts.testsuiteId) {
215
+ for (const n of suiteNames)
216
+ mm.set(n, this.opts.testsuiteId);
217
+ return mm;
218
+ }
219
+ let existing = await this.client.getModules();
220
+ const byName = new Map();
221
+ for (const m of existing) {
222
+ const n = extractModuleName(m);
223
+ const k = extractModuleKey(m);
224
+ if (n && k)
225
+ byName.set(n.toLowerCase().trim(), k);
226
+ }
227
+ for (const sn of suiteNames) {
228
+ const norm = sn.toLowerCase().trim();
229
+ if (byName.has(norm)) {
230
+ mm.set(sn, byName.get(norm));
231
+ continue;
232
+ }
233
+ let ck = null;
234
+ try {
235
+ ck = extractModuleKey(await this.client.createModule(sn));
236
+ }
237
+ catch (e) {
238
+ console.warn(`[testcomplete-qatouch] createModule: ${e.message}`);
239
+ }
240
+ if (!ck) {
241
+ existing = await this.client.getModules();
242
+ for (const m of existing) {
243
+ const n = extractModuleName(m);
244
+ const k = extractModuleKey(m);
245
+ if (n && k)
246
+ byName.set(n.toLowerCase().trim(), k);
247
+ }
248
+ ck = byName.get(norm) || null;
249
+ }
250
+ if (ck) {
251
+ mm.set(sn, ck);
252
+ byName.set(norm, ck);
253
+ }
254
+ else
255
+ console.error(`[testcomplete-qatouch] Failed to resolve module: ${sn}`);
256
+ }
257
+ return mm;
258
+ }
259
+ }
260
+ exports.TestCompleteQATouchReporter = TestCompleteQATouchReporter;
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "testcomplete-qatouch",
3
+ "version": "1.0.0",
4
+ "description": "QA Touch reporter for TestComplete (SmartBear) — auto-creates modules, test cases and test runs from JUnit XML exports.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist"],
8
+ "scripts": { "build": "tsc", "prepare": "npm run build", "prepublishOnly": "npm run build" },
9
+ "keywords": ["testcomplete","smartbear","junit","qatouch","reporter","test-management","qa"],
10
+ "author": "UjjwalaMeena",
11
+ "license": "ISC",
12
+ "repository": { "type": "git", "url": "git+https://github.com/ujjwala91/testcomplete-qatouch.git" },
13
+ "homepage": "https://github.com/ujjwala91/testcomplete-qatouch#readme",
14
+ "bugs": { "url": "https://github.com/ujjwala91/testcomplete-qatouch/issues" },
15
+ "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.7.0" }
16
+ }