@msalaam/xray-qe-toolkit 1.1.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/.env.example +16 -0
- package/README.md +893 -0
- package/bin/cli.js +137 -0
- package/commands/createExecution.js +46 -0
- package/commands/editJson.js +165 -0
- package/commands/genPipeline.js +42 -0
- package/commands/genPostman.js +70 -0
- package/commands/genTests.js +138 -0
- package/commands/importResults.js +50 -0
- package/commands/init.js +141 -0
- package/commands/pushTests.js +114 -0
- package/lib/config.js +108 -0
- package/lib/index.js +31 -0
- package/lib/logger.js +43 -0
- package/lib/postmanGenerator.js +305 -0
- package/lib/testCaseBuilder.js +202 -0
- package/lib/xrayClient.js +416 -0
- package/package.json +61 -0
- package/schema/tests.schema.json +112 -0
- package/templates/README.template.md +161 -0
- package/templates/azure-pipelines.yml +65 -0
- package/templates/knowledge-README.md +121 -0
- package/templates/tests.json +72 -0
- package/templates/xray-mapping.json +1 -0
- package/ui/editor/editor.js +484 -0
- package/ui/editor/index.html +150 -0
- package/ui/editor/styles.css +550 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @msalaam/xray-qe-toolkit — Postman Collection Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates a Postman Collection v2.1 JSON from tests.json.
|
|
5
|
+
* Each test becomes a request folder; steps map to request items with
|
|
6
|
+
* pre-request scripts and test assertions.
|
|
7
|
+
*
|
|
8
|
+
* The output is a working starting point — QEs or agents can further
|
|
9
|
+
* refine the generated collection.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import logger from "./logger.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a Postman v2.1 collection from tests.json config.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} testsConfig Parsed tests.json content
|
|
20
|
+
* @param {string} outputPath Where to write collection.postman.json
|
|
21
|
+
* @param {object} [opts] Options
|
|
22
|
+
* @param {string} [opts.baseUrl] Base URL placeholder (default: "{{baseUrl}}")
|
|
23
|
+
* @param {object} [opts.mapping] xray-mapping.json content (for embedding JIRA keys)
|
|
24
|
+
* @returns {object} The generated collection object
|
|
25
|
+
*/
|
|
26
|
+
export function generate(testsConfig, outputPath, opts = {}) {
|
|
27
|
+
const baseUrl = opts.baseUrl || "{{baseUrl}}";
|
|
28
|
+
const mapping = opts.mapping || {};
|
|
29
|
+
const tests = testsConfig.tests || [];
|
|
30
|
+
|
|
31
|
+
// Filter for API tests only (type: "api" or unset for backward compatibility)
|
|
32
|
+
const apiTests = tests.filter((t) => !t.type || t.type === "api");
|
|
33
|
+
|
|
34
|
+
const collection = {
|
|
35
|
+
info: {
|
|
36
|
+
name: testsConfig.testExecution?.summary || "Generated Test Collection",
|
|
37
|
+
description:
|
|
38
|
+
(testsConfig.testExecution?.description || "Auto-generated by @msalaam/xray-qe-toolkit") +
|
|
39
|
+
"\n\n⚠️ SCAFFOLD: This is a generated starting point. Review and enhance each request with proper assertions, environment variables, and edge cases before use.",
|
|
40
|
+
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
41
|
+
_postman_id: generateUUID(),
|
|
42
|
+
},
|
|
43
|
+
variable: [
|
|
44
|
+
{
|
|
45
|
+
key: "baseUrl",
|
|
46
|
+
value: "https://api.example.com",
|
|
47
|
+
type: "string",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "authToken",
|
|
51
|
+
value: "",
|
|
52
|
+
type: "string",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
auth: {
|
|
56
|
+
type: "bearer",
|
|
57
|
+
bearer: [
|
|
58
|
+
{
|
|
59
|
+
key: "token",
|
|
60
|
+
value: "{{authToken}}",
|
|
61
|
+
type: "string",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
item: apiTests.map((test) => buildTestFolder(test, baseUrl, mapping)),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
fs.writeFileSync(outputPath, JSON.stringify(collection, null, 2));
|
|
69
|
+
logger.save(`Collection written to ${path.basename(outputPath)}`);
|
|
70
|
+
logger.info(`${apiTests.length} API test(s) → ${countRequests(collection)} request(s)`);
|
|
71
|
+
if (tests.length > apiTests.length) {
|
|
72
|
+
logger.info(`${tests.length - apiTests.length} non-API tests skipped (use type: "api" to include)`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return collection;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Internal builders ────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert a single test definition into a Postman folder with request items.
|
|
82
|
+
*/
|
|
83
|
+
function buildTestFolder(test, baseUrl, mapping = {}) {
|
|
84
|
+
// Use JIRA key if available in mapping, otherwise use test_id
|
|
85
|
+
const testKey = mapping[test.test_id]?.key || test.test_id;
|
|
86
|
+
|
|
87
|
+
const folder = {
|
|
88
|
+
name: `[${testKey}] ${test.xray.summary}`,
|
|
89
|
+
description: test.xray.description || "",
|
|
90
|
+
item: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!test.xray.steps || test.xray.steps.length === 0) {
|
|
94
|
+
// No steps — create a single placeholder request
|
|
95
|
+
folder.item.push(buildPlaceholderRequest(test, baseUrl));
|
|
96
|
+
return folder;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test.xray.steps.forEach((step, idx) => {
|
|
100
|
+
folder.item.push(buildStepRequest(test, step, idx + 1, baseUrl));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return folder;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a request item from a test step.
|
|
108
|
+
*/
|
|
109
|
+
function buildStepRequest(test, step, stepNum, baseUrl) {
|
|
110
|
+
// Try to infer HTTP method and path from the step action/data
|
|
111
|
+
const { method, endpoint } = inferEndpoint(step);
|
|
112
|
+
|
|
113
|
+
const item = {
|
|
114
|
+
name: `Step ${stepNum}: ${truncate(step.action, 80)}`,
|
|
115
|
+
request: {
|
|
116
|
+
method,
|
|
117
|
+
header: [
|
|
118
|
+
{
|
|
119
|
+
key: "Content-Type",
|
|
120
|
+
value: "application/json",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
url: {
|
|
124
|
+
raw: `${baseUrl}${endpoint}`,
|
|
125
|
+
host: [baseUrl],
|
|
126
|
+
path: endpoint.split("/").filter(Boolean),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
event: [],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Add pre-request script with step data context
|
|
133
|
+
if (step.data) {
|
|
134
|
+
item.event.push({
|
|
135
|
+
listen: "prerequest",
|
|
136
|
+
script: {
|
|
137
|
+
type: "text/javascript",
|
|
138
|
+
exec: [
|
|
139
|
+
`// Step Data: ${step.data}`,
|
|
140
|
+
`// Action: ${step.action}`,
|
|
141
|
+
`console.log("Executing: ${escapeJs(step.action)}");`,
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Try to extract request body from step data
|
|
147
|
+
const body = extractBody(step.data);
|
|
148
|
+
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
149
|
+
item.request.body = {
|
|
150
|
+
mode: "raw",
|
|
151
|
+
raw: body,
|
|
152
|
+
options: { raw: { language: "json" } },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Add test script with expected result assertion
|
|
158
|
+
if (step.expected_result) {
|
|
159
|
+
item.event.push({
|
|
160
|
+
listen: "test",
|
|
161
|
+
script: {
|
|
162
|
+
type: "text/javascript",
|
|
163
|
+
exec: buildTestScript(step),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return item;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build a placeholder request when a test has no steps.
|
|
173
|
+
*/
|
|
174
|
+
function buildPlaceholderRequest(test, baseUrl) {
|
|
175
|
+
return {
|
|
176
|
+
name: test.xray.summary,
|
|
177
|
+
request: {
|
|
178
|
+
method: "GET",
|
|
179
|
+
header: [],
|
|
180
|
+
url: {
|
|
181
|
+
raw: `${baseUrl}/TODO`,
|
|
182
|
+
host: [baseUrl],
|
|
183
|
+
path: ["TODO"],
|
|
184
|
+
},
|
|
185
|
+
description: `TODO: Configure this request for test ${test.test_id}`,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Infer HTTP method and endpoint from step action/data text.
|
|
192
|
+
*/
|
|
193
|
+
function inferEndpoint(step) {
|
|
194
|
+
const combined = `${step.action} ${step.data || ""}`;
|
|
195
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
196
|
+
|
|
197
|
+
let method = "GET";
|
|
198
|
+
let endpoint = "/api/TODO";
|
|
199
|
+
|
|
200
|
+
// Look for explicit HTTP method
|
|
201
|
+
for (const m of methods) {
|
|
202
|
+
if (combined.toUpperCase().includes(m)) {
|
|
203
|
+
method = m;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Look for a path pattern like /api/something or /endpoint
|
|
209
|
+
const pathMatch = combined.match(/(?:\/[\w{}\-]+){1,10}/);
|
|
210
|
+
if (pathMatch) {
|
|
211
|
+
endpoint = pathMatch[0];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { method, endpoint };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build Postman test script lines from an expected result.
|
|
219
|
+
*/SCAFFOLD: Expected - ${step.expected_result}`,
|
|
220
|
+
`// SCAFFOLD: Enhance this assertion based on actual API behavior
|
|
221
|
+
function buildTestScript(step) {
|
|
222
|
+
const lines = [
|
|
223
|
+
`// Expected: ${step.expected_result}`,
|
|
224
|
+
"",
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const expected = step.expected_result.toLowerCase();
|
|
228
|
+
|
|
229
|
+
// Infer status code assertions
|
|
230
|
+
const statusMatch = expected.match(/(\d{3})\s/);
|
|
231
|
+
if (statusMatch) {
|
|
232
|
+
lines.push(`pm.test("Status code is ${statusMatch[1]}", function () {`);
|
|
233
|
+
lines.push(` pm.response.to.have.status(${statusMatch[1]});`);
|
|
234
|
+
lines.push(`});`);
|
|
235
|
+
lines.push("");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for schema vSCAFFOLDation mentions
|
|
239
|
+
if (expected.includes("schema") || expected.includes("required fields")) {
|
|
240
|
+
lines.push(`pm.test("Response has expected schema", function () {`);
|
|
241
|
+
lines.push(` const json = pm.response.json();`);
|
|
242
|
+
lines.push(` // TODO: Add schema assertions based on expected fields`);
|
|
243
|
+
lines.push(` pm.expect(json).to.be.an("object");`);
|
|
244
|
+
lines.push(`});`);
|
|
245
|
+
lines.push("");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for error message mentions
|
|
249
|
+
if (expected.includes("error") || expected.includes("400") || expected.includes("401")) {
|
|
250
|
+
lines.push(`pm.test("Error response format", function () {`);
|
|
251
|
+
lines.push(` const json = pm.response.json();`);
|
|
252
|
+
lines.push(` pm.expect(json).to.have.property("error");`);
|
|
253
|
+
lines.push(`});`);
|
|
254
|
+
lines.push("");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Always add a general assertion
|
|
258
|
+
lines.push(`pm.test("${escapeJs(truncate(step.expected_result, 100))}", function () {`);
|
|
259
|
+
lines.push(` pm.response.to.be.ok;`);
|
|
260
|
+
lines.push(`});`);
|
|
261
|
+
|
|
262
|
+
return lines;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Try to extract a JSON body from step data text.
|
|
267
|
+
*/
|
|
268
|
+
function extractBody(data) {
|
|
269
|
+
// Look for JSON object in the data field
|
|
270
|
+
const jsonMatch = data.match(/\{[\s\S]*\}/);
|
|
271
|
+
if (jsonMatch) {
|
|
272
|
+
try {
|
|
273
|
+
JSON.parse(jsonMatch[0]);
|
|
274
|
+
return jsonMatch[0];
|
|
275
|
+
} catch {
|
|
276
|
+
return jsonMatch[0]; // Return raw even if not valid JSON
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
function truncate(str, max) {
|
|
285
|
+
return str.length > max ? str.slice(0, max - 3) + "..." : str;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function escapeJs(str) {
|
|
289
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function generateUUID() {
|
|
293
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
294
|
+
const r = (Math.random() * 16) | 0;
|
|
295
|
+
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function countRequests(collection) {
|
|
300
|
+
let count = 0;
|
|
301
|
+
for (const folder of collection.item) {
|
|
302
|
+
count += folder.item ? folder.item.length : 1;
|
|
303
|
+
}
|
|
304
|
+
return count;
|
|
305
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @msalaam/xray-qe-toolkit — Test Case Builder
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the creation / update of Xray test cases and their linkage
|
|
5
|
+
* to Test Executions. Supports idempotent push: if a test_id already exists
|
|
6
|
+
* in xray-mapping.json the issue is updated rather than duplicated.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import logger from "./logger.js";
|
|
11
|
+
import {
|
|
12
|
+
authenticate,
|
|
13
|
+
createIssue,
|
|
14
|
+
updateIssue,
|
|
15
|
+
setTestTypeAutomated,
|
|
16
|
+
addTestStep,
|
|
17
|
+
removeAllTestSteps,
|
|
18
|
+
linkMultiple,
|
|
19
|
+
withRetry,
|
|
20
|
+
} from "./xrayClient.js";
|
|
21
|
+
|
|
22
|
+
// ─── Mapping helpers ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load xray-mapping.json from disk (or return empty object).
|
|
26
|
+
* @param {string} mappingPath
|
|
27
|
+
* @returns {object}
|
|
28
|
+
*/
|
|
29
|
+
export function loadMapping(mappingPath) {
|
|
30
|
+
if (fs.existsSync(mappingPath)) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(mappingPath, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
logger.warn("Could not load existing mapping file, creating new one");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Persist mapping object to disk (JSON, 2-space indent).
|
|
42
|
+
* @param {string} mappingPath
|
|
43
|
+
* @param {object} mapping
|
|
44
|
+
*/
|
|
45
|
+
export function saveMapping(mappingPath, mapping) {
|
|
46
|
+
fs.writeFileSync(mappingPath, JSON.stringify(mapping, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Core build & push ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create or update Xray test cases from a tests.json configuration.
|
|
53
|
+
*
|
|
54
|
+
* Flow per test:
|
|
55
|
+
* 1. If test_id exists in mapping → update JIRA issue + replace steps
|
|
56
|
+
* 2. If test_id is new → create JIRA Test issue, set Automated type, add steps
|
|
57
|
+
* 3. Save mapping after each test (crash-safe)
|
|
58
|
+
* 4. 300ms rate-limit delay between tests
|
|
59
|
+
*
|
|
60
|
+
* @param {object} cfg Config from loadConfig()
|
|
61
|
+
* @param {object[]} tests Array of test definitions from tests.json
|
|
62
|
+
* @param {object} mapping Current xray-mapping.json contents (mutated in place)
|
|
63
|
+
* @returns {Promise<{created: string[], updated: string[], failed: string[]}>}
|
|
64
|
+
*/
|
|
65
|
+
export async function buildAndPush(cfg, tests, mapping) {
|
|
66
|
+
const xrayToken = await authenticate(cfg);
|
|
67
|
+
|
|
68
|
+
const created = [];
|
|
69
|
+
const updated = [];
|
|
70
|
+
const failed = [];
|
|
71
|
+
|
|
72
|
+
for (const test of tests) {
|
|
73
|
+
try {
|
|
74
|
+
const existing = mapping[test.test_id];
|
|
75
|
+
|
|
76
|
+
if (existing) {
|
|
77
|
+
// ── Update existing test ──────────────────────────────
|
|
78
|
+
logger.send(`${test.test_id} (update → ${existing.key})`);
|
|
79
|
+
|
|
80
|
+
await updateIssue(cfg, existing.key, {
|
|
81
|
+
summary: test.xray.summary,
|
|
82
|
+
description: test.xray.description,
|
|
83
|
+
priority: test.xray.priority,
|
|
84
|
+
labels: test.xray.labels,
|
|
85
|
+
});
|
|
86
|
+
logger.step("Fields updated");
|
|
87
|
+
|
|
88
|
+
// Replace steps: remove all existing, re-add from tests.json
|
|
89
|
+
if (test.xray.steps && test.xray.steps.length > 0) {
|
|
90
|
+
await withRetry(
|
|
91
|
+
async () => {
|
|
92
|
+
const removed = await removeAllTestSteps(cfg, xrayToken, existing.id);
|
|
93
|
+
logger.debug(`Removed ${removed} existing step(s)`);
|
|
94
|
+
|
|
95
|
+
for (const step of test.xray.steps) {
|
|
96
|
+
await addTestStep(cfg, xrayToken, existing.id, step);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{ retryOn: "issueId provided is not valid" }
|
|
100
|
+
);
|
|
101
|
+
logger.step(`${test.xray.steps.length} step(s) replaced`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updated.push(existing.key);
|
|
105
|
+
} else {
|
|
106
|
+
// ── Create new test ───────────────────────────────────
|
|
107
|
+
logger.send(`${test.test_id}`);
|
|
108
|
+
|
|
109
|
+
const issue = await createIssue(cfg, "Test", {
|
|
110
|
+
summary: test.xray.summary,
|
|
111
|
+
description: test.xray.description,
|
|
112
|
+
priority: test.xray.priority,
|
|
113
|
+
labels: test.xray.labels,
|
|
114
|
+
});
|
|
115
|
+
logger.step(`${issue.key} (ID: ${issue.id})`);
|
|
116
|
+
|
|
117
|
+
// Set type + add steps with retry (Xray indexing delay)
|
|
118
|
+
if (test.xray.steps && test.xray.steps.length > 0) {
|
|
119
|
+
await withRetry(
|
|
120
|
+
async () => {
|
|
121
|
+
await setTestTypeAutomated(cfg, xrayToken, issue.id);
|
|
122
|
+
|
|
123
|
+
for (const step of test.xray.steps) {
|
|
124
|
+
await addTestStep(cfg, xrayToken, issue.id, step);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{ retryOn: "issueId provided is not valid" }
|
|
128
|
+
);
|
|
129
|
+
logger.step(`${test.xray.steps.length} step(s) added`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
mapping[test.test_id] = { key: issue.key, id: issue.id };
|
|
133
|
+
created.push(issue.key);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Save mapping after each test (crash-safe)
|
|
137
|
+
saveMapping(cfg.mappingPath, mapping);
|
|
138
|
+
logger.blank();
|
|
139
|
+
|
|
140
|
+
// Rate limiting
|
|
141
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
144
|
+
logger.stepFail(`Failed: ${detail}`);
|
|
145
|
+
logger.blank();
|
|
146
|
+
failed.push(test.test_id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { created, updated, failed };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a Test Execution and link all tests to it.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} cfg Config
|
|
157
|
+
* @param {object} execConfig { summary, description }
|
|
158
|
+
* @param {string[]} testKeys JIRA keys to link
|
|
159
|
+
* @param {object} mapping Mapping object (mutated — adds _testexecution)
|
|
160
|
+
* @returns {Promise<{key: string, id: string, linked: string[]}>}
|
|
161
|
+
*/
|
|
162
|
+
export async function createExecutionAndLink(cfg, execConfig, testKeys, mapping) {
|
|
163
|
+
logger.pin(`Creating Test Execution: "${execConfig.summary}"...`);
|
|
164
|
+
|
|
165
|
+
const execution = await createIssue(cfg, "Test Execution", {
|
|
166
|
+
summary: execConfig.summary,
|
|
167
|
+
description: execConfig.description || execConfig.summary,
|
|
168
|
+
});
|
|
169
|
+
logger.step(execution.key);
|
|
170
|
+
|
|
171
|
+
mapping._testexecution = { key: execution.key, id: execution.id };
|
|
172
|
+
saveMapping(cfg.mappingPath, mapping);
|
|
173
|
+
|
|
174
|
+
// Wait for Xray indexing
|
|
175
|
+
logger.wait("Waiting for Xray to index tests...");
|
|
176
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
177
|
+
|
|
178
|
+
// Link tests
|
|
179
|
+
logger.link(`Linking ${testKeys.length} test(s) to Test Execution...`);
|
|
180
|
+
const { linked } = await linkMultiple(cfg, testKeys, execution.key);
|
|
181
|
+
logger.step(`${linked.length} test(s) linked`);
|
|
182
|
+
|
|
183
|
+
return { key: execution.key, id: execution.id, linked };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Link tests to an existing Test Execution key.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} cfg
|
|
190
|
+
* @param {string} testExecKey e.g. "QE-123"
|
|
191
|
+
* @param {string[]} testKeys
|
|
192
|
+
* @returns {Promise<{linked: string[], failed: string[]}>}
|
|
193
|
+
*/
|
|
194
|
+
export async function linkToExistingExecution(cfg, testExecKey, testKeys) {
|
|
195
|
+
logger.pin(`Using existing Test Execution: ${testExecKey}`);
|
|
196
|
+
logger.link(`Linking ${testKeys.length} test(s)...`);
|
|
197
|
+
|
|
198
|
+
const result = await linkMultiple(cfg, testKeys, testExecKey);
|
|
199
|
+
logger.step(`${result.linked.length} test(s) linked`);
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
}
|