squish-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,73 @@
1
+ # squish-qatouch
2
+
3
+ > QA Touch reporter for **Squish** (Froglogic / Qt) — reads your Squish result XML and automatically creates modules, test cases, and test runs in QA Touch.
4
+
5
+ ## Supported result formats
6
+
7
+ | Format | How to generate |
8
+ |--------|----------------|
9
+ | **Squish native XML** | `squishrunner --reportgen xml,results.xml` |
10
+ | **JUnit XML** | `squish2junit results/ junit.xml` or `--reportgen xml.junit,...` |
11
+
12
+ Auto-detected at runtime by the root element.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install squish-qatouch
18
+ ```
19
+
20
+ No peer dependencies — works standalone in any Node.js project.
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import { SquishQATouchReporter } from "squish-qatouch";
26
+
27
+ const reporter = new SquishQATouchReporter({
28
+ domain: "mycompany", // QA Touch domain
29
+ apiToken: "YOUR_API_TOKEN",
30
+ projectKey: "PROJ",
31
+ assignTo: "user-key",
32
+ resultsFile: "./results/squish.xml", // native XML or JUnit XML
33
+ milestoneName: "Sprint 12", // created if it does not exist
34
+ tag: "squish",
35
+ });
36
+
37
+ await reporter.run();
38
+ ```
39
+
40
+ Run it after `squishrunner` finishes:
41
+
42
+ ```bash
43
+ npx ts-node upload-results.ts
44
+ ```
45
+
46
+ ## Options
47
+
48
+ | Option | Type | Default | Description |
49
+ |--------|------|---------|-------------|
50
+ | `domain` | `string` | — | Your QA Touch domain |
51
+ | `apiToken` | `string` | — | QA Touch API token |
52
+ | `projectKey` | `string` | — | QA Touch project key |
53
+ | `assignTo` | `string` | — | User key to assign the run to |
54
+ | `resultsFile` | `string` | — | Path to the Squish XML result file |
55
+ | `testsuiteId` | `string?` | — | Pin all cases to one module key (skips auto-create) |
56
+ | `milestoneName` | `string?` | `"Squish Automation"` | Milestone name — reused or created |
57
+ | `milestoneKey` | `string?` | — | Use an existing milestone key directly |
58
+ | `createCases` | `boolean?` | `true` | Auto-create missing test cases |
59
+ | `tag` | `string?` | `"squish"` | Tag applied to the test run |
60
+
61
+ ## CI example (GitHub Actions)
62
+
63
+ ```yaml
64
+ - name: Run Squish tests
65
+ run: squishrunner --testsuite suite_MyApp --reportgen xml,results/squish.xml
66
+
67
+ - name: Upload to QA Touch
68
+ run: npx ts-node upload-results.ts
69
+ ```
70
+
71
+ ## License
72
+
73
+ ISC
@@ -0,0 +1,2 @@
1
+ export { SquishQATouchReporter } from "./reporter";
2
+ export type { SquishQATouchReporterOptions } from "./reporter";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SquishQATouchReporter = void 0;
4
+ var reporter_1 = require("./reporter");
5
+ Object.defineProperty(exports, "SquishQATouchReporter", { enumerable: true, get: function () { return reporter_1.SquishQATouchReporter; } });
@@ -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 SquishQATouchReporterOptions {
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 SquishQATouchReporter {
14
+ private opts;
15
+ private client;
16
+ constructor(options: SquishQATouchReporterOptions);
17
+ run(): Promise<void>;
18
+ private resolveMilestone;
19
+ private resolveModules;
20
+ }
@@ -0,0 +1,309 @@
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.SquishQATouchReporter = 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: `Launch the AUT for: ${title}.`, expected_result: "The AUT starts in the expected initial state." },
54
+ "1": { steps: `Execute the Squish test case: ${title}.`, expected_result: "All script actions and verifications run without errors." },
55
+ "2": { steps: `Verify all verification points for: ${title}.`, expected_result: "All verification points 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 sn = 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: `${sn}.${title}`, title, normalizedTitle: normalizeTitle(title), suiteName: sn, status, errorMessage });
91
+ }
92
+ }
93
+ return tests;
94
+ }
95
+ function extractSquishFailMsg(xml) {
96
+ const m = xml.match(/<message[^>]*type="FAIL"[^>]*text="([^"]*)"/i);
97
+ if (m)
98
+ return m[1].slice(0, 2000);
99
+ const m2 = xml.match(/<message[^>]*type="FAIL"[^>]*>([\s\S]*?)<\/message>/i);
100
+ if (m2)
101
+ return m2[1].replace(/<[^>]+>/g, "").trim().slice(0, 2000);
102
+ return undefined;
103
+ }
104
+ function parseSquishNativeXml(xml) {
105
+ const tests = [];
106
+ const testRe = /<test\s([^>]*)>([\s\S]*?)<\/test>/gi;
107
+ for (const tm of xml.matchAll(testRe)) {
108
+ const ta = tm[1] || "";
109
+ const ti = tm[2] || "";
110
+ const nm = ta.match(/name="([^"]*)"/i);
111
+ const sn = nm ? nm[1] : "Default";
112
+ const fnRe = /<function\s([^>]*)>([\s\S]*?)<\/function>/gi;
113
+ const fns = [...ti.matchAll(fnRe)];
114
+ if (fns.length === 0) {
115
+ const fail = /<message[^>]*type="FAIL"/i.test(ti);
116
+ tests.push({ fullName: sn, title: sn, normalizedTitle: normalizeTitle(sn), suiteName: sn, status: fail ? "failed" : "passed", errorMessage: extractSquishFailMsg(ti) });
117
+ continue;
118
+ }
119
+ for (const fm of fns) {
120
+ const fa = fm[1] || "";
121
+ const fi = fm[2] || "";
122
+ const fnm = fa.match(/name="([^"]*)"/i);
123
+ const title = fnm ? fnm[1] : sn;
124
+ const fail = /<message[^>]*type="FAIL"/i.test(fi);
125
+ const skip = /<message[^>]*type="SKIP"/i.test(fi);
126
+ let status = "passed";
127
+ if (fail)
128
+ status = "failed";
129
+ else if (skip)
130
+ status = "blocked";
131
+ tests.push({ fullName: `${sn}.${title}`, title, normalizedTitle: normalizeTitle(title), suiteName: sn, status, errorMessage: fail ? extractSquishFailMsg(fi) : undefined });
132
+ }
133
+ }
134
+ return tests;
135
+ }
136
+ function parseResultsFile(filePath) {
137
+ const content = fs.readFileSync(filePath, "utf-8");
138
+ if (/<SquishReport/i.test(content))
139
+ return parseSquishNativeXml(content);
140
+ if (/<testsuites/i.test(content) || /<testsuite/i.test(content))
141
+ return parseJUnitXml(content);
142
+ throw new Error(`[squish-qatouch] Unrecognised format (expected Squish XML or JUnit XML): ${filePath}`);
143
+ }
144
+ class SquishQATouchReporter {
145
+ constructor(options) {
146
+ this.opts = {
147
+ domain: options.domain, apiToken: options.apiToken, projectKey: options.projectKey,
148
+ assignTo: options.assignTo, resultsFile: options.resultsFile,
149
+ testsuiteId: options.testsuiteId, milestoneName: options.milestoneName || "Squish Automation",
150
+ milestoneKey: options.milestoneKey, createCases: options.createCases !== false,
151
+ tag: options.tag || "squish",
152
+ };
153
+ this.client = new qatouch_client_1.QATouchClient({ domain: this.opts.domain, apiToken: this.opts.apiToken, projectKey: this.opts.projectKey });
154
+ }
155
+ async run() {
156
+ const tests = parseResultsFile(this.opts.resultsFile);
157
+ if (tests.length === 0) {
158
+ console.warn("[squish-qatouch] No test cases found.");
159
+ return;
160
+ }
161
+ const suiteMap = new Map();
162
+ for (const t of tests) {
163
+ const l = suiteMap.get(t.suiteName) || [];
164
+ l.push(t);
165
+ suiteMap.set(t.suiteName, l);
166
+ }
167
+ const mkByS = await this.resolveModules([...suiteMap.keys()]);
168
+ const caseMap = new Map();
169
+ for (const mk of new Set(mkByS.values())) {
170
+ for (const item of await this.client.getTestCases(mk)) {
171
+ const t = (item.title || item.case_title || "").trim();
172
+ const k = extractCaseKey(item);
173
+ if (t && k)
174
+ caseMap.set(normalizeTitle(t), k);
175
+ }
176
+ }
177
+ if (this.opts.createCases) {
178
+ for (const [sn, st] of suiteMap) {
179
+ const mk = mkByS.get(sn);
180
+ if (!mk)
181
+ continue;
182
+ for (const test of st) {
183
+ if (caseMap.has(test.normalizedTitle))
184
+ continue;
185
+ await this.client.createTestCase(mk, test.title, `Automated Squish scenario: ${test.title}`, test.fullName, "The Squish AUT is running and accessible.", buildStepsTemplate(test.title));
186
+ for (const item of await this.client.getTestCases(mk)) {
187
+ const t = (item.title || item.case_title || "").trim();
188
+ const k = extractCaseKey(item);
189
+ if (t && k)
190
+ caseMap.set(normalizeTitle(t), k);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ const milestoneKey = await this.resolveMilestone();
196
+ if (!milestoneKey)
197
+ throw new Error("[squish-qatouch] Unable to resolve milestone key.");
198
+ const bulk = [];
199
+ const detailed = [];
200
+ const caseKeys = [];
201
+ for (const test of tests) {
202
+ const ck = caseMap.get(test.normalizedTitle);
203
+ if (!ck) {
204
+ console.warn(`[squish-qatouch] Unmapped: ${test.title}`);
205
+ continue;
206
+ }
207
+ caseKeys.push(ck);
208
+ if (test.status === "failed" && test.errorMessage)
209
+ detailed.push({ caseKey: ck, test });
210
+ else
211
+ bulk.push({ case: ck, status: statusToId(test.status) });
212
+ }
213
+ if (caseKeys.length === 0) {
214
+ console.warn("[squish-qatouch] No mapped cases.");
215
+ return;
216
+ }
217
+ const rp = await this.client.createTestRun(milestoneKey, `Squish Run ${new Date().toISOString()}`, this.opts.assignTo, [...new Set(caseKeys)], this.opts.tag);
218
+ const trk = extractRunKey(rp);
219
+ if (!trk)
220
+ throw new Error("[squish-qatouch] Test run returned no key.");
221
+ for (const { caseKey, test } of detailed) {
222
+ try {
223
+ await this.client.addResultWithComment(trk, caseKey, statusToId(test.status), test.errorMessage);
224
+ }
225
+ catch (e) {
226
+ console.error(`[squish-qatouch] ${e.message}`);
227
+ bulk.push({ case: caseKey, status: statusToId(test.status) });
228
+ }
229
+ }
230
+ if (bulk.length > 0) {
231
+ try {
232
+ const r = await this.client.updateResults(trk, bulk);
233
+ if (r?.error)
234
+ throw new Error(r.error);
235
+ }
236
+ catch {
237
+ for (const item of bulk) {
238
+ try {
239
+ await this.client.addResultWithComment(trk, item.case, item.status);
240
+ }
241
+ catch (e) {
242
+ console.error(`[squish-qatouch] ${e.message}`);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ console.log(`[squish-qatouch] Uploaded ${caseKeys.length} result(s) to run ${trk}.`);
248
+ }
249
+ async resolveMilestone() {
250
+ if (this.opts.milestoneKey)
251
+ return this.opts.milestoneKey;
252
+ const name = this.opts.milestoneName;
253
+ const find = (list) => list.find((m) => (m.milestone_name || m.name || m.title || m.milestone || "") === name);
254
+ let ms = await this.client.getMilestones();
255
+ if (find(ms))
256
+ return extractMilestoneKey(find(ms));
257
+ await this.client.createMilestone(name);
258
+ ms = await this.client.getMilestones();
259
+ return find(ms) ? extractMilestoneKey(find(ms)) : null;
260
+ }
261
+ async resolveModules(suiteNames) {
262
+ const mm = new Map();
263
+ if (this.opts.testsuiteId) {
264
+ for (const n of suiteNames)
265
+ mm.set(n, this.opts.testsuiteId);
266
+ return mm;
267
+ }
268
+ let existing = await this.client.getModules();
269
+ const byName = new Map();
270
+ for (const m of existing) {
271
+ const n = extractModuleName(m);
272
+ const k = extractModuleKey(m);
273
+ if (n && k)
274
+ byName.set(n.toLowerCase().trim(), k);
275
+ }
276
+ for (const sn of suiteNames) {
277
+ const norm = sn.toLowerCase().trim();
278
+ if (byName.has(norm)) {
279
+ mm.set(sn, byName.get(norm));
280
+ continue;
281
+ }
282
+ let ck = null;
283
+ try {
284
+ ck = extractModuleKey(await this.client.createModule(sn));
285
+ }
286
+ catch (e) {
287
+ console.warn(`[squish-qatouch] createModule: ${e.message}`);
288
+ }
289
+ if (!ck) {
290
+ existing = await this.client.getModules();
291
+ for (const m of existing) {
292
+ const n = extractModuleName(m);
293
+ const k = extractModuleKey(m);
294
+ if (n && k)
295
+ byName.set(n.toLowerCase().trim(), k);
296
+ }
297
+ ck = byName.get(norm) || null;
298
+ }
299
+ if (ck) {
300
+ mm.set(sn, ck);
301
+ byName.set(norm, ck);
302
+ }
303
+ else
304
+ console.error(`[squish-qatouch] Failed to resolve module: ${sn}`);
305
+ }
306
+ return mm;
307
+ }
308
+ }
309
+ exports.SquishQATouchReporter = SquishQATouchReporter;
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "squish-qatouch",
3
+ "version": "1.0.0",
4
+ "description": "QA Touch reporter for Squish (Froglogic/Qt) — auto-creates modules, test cases and test runs from Squish native XML or JUnit XML result files.",
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": ["squish","froglogic","qt","qatouch","reporter","test-management","qa"],
10
+ "author": "UjjwalaMeena",
11
+ "license": "ISC",
12
+ "repository": { "type": "git", "url": "git+https://github.com/ujjwala91/squish-qatouch.git" },
13
+ "homepage": "https://github.com/ujjwala91/squish-qatouch#readme",
14
+ "bugs": { "url": "https://github.com/ujjwala91/squish-qatouch/issues" },
15
+ "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.7.0" }
16
+ }