@msalaam/xray-qe-toolkit 1.4.1 → 1.6.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 +157 -71
- package/bin/cli.js +38 -25
- package/commands/createExecution.js +1 -0
- package/commands/createPlan.js +23 -34
- package/commands/importResults.js +2 -0
- package/commands/init.js +2 -0
- package/commands/pushTests.js +65 -7
- package/lib/config.js +7 -0
- package/lib/jsonFile.js +12 -0
- package/lib/playwrightConverter.js +68 -45
- package/lib/testCaseBuilder.js +374 -90
- package/lib/xrayClient.js +168 -11
- package/package.json +2 -1
- package/schema/tests.schema.json +126 -7
- package/templates/README.template.md +80 -23
- package/templates/tests.json +5295 -47
- package/commands/genTests.js +0 -138
- package/templates/SPEC-DRIVEN-APPROACH.md +0 -372
package/commands/createPlan.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import fs from "node:fs";
|
|
13
13
|
import logger, { setVerbose } from "../lib/logger.js";
|
|
14
14
|
import { loadConfig, validateConfig } from "../lib/config.js";
|
|
15
|
-
import { authenticate, createTestPlan } from "../lib/xrayClient.js";
|
|
15
|
+
import { authenticate, createIssue, createTestPlan, getIssue } from "../lib/xrayClient.js";
|
|
16
16
|
|
|
17
17
|
export default async function createPlan(opts = {}) {
|
|
18
18
|
if (opts.verbose) setVerbose(true);
|
|
@@ -42,47 +42,36 @@ export default async function createPlan(opts = {}) {
|
|
|
42
42
|
|
|
43
43
|
let planKey;
|
|
44
44
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
fixVersions: [{ name: opts.version }],
|
|
52
|
-
}),
|
|
53
|
-
...(opts.label && {
|
|
54
|
-
labels: opts.label.split(",").map((l) => l.trim()),
|
|
55
|
-
}),
|
|
56
|
-
},
|
|
45
|
+
const fields = {
|
|
46
|
+
summary,
|
|
47
|
+
description: summary,
|
|
48
|
+
...(opts.label && {
|
|
49
|
+
labels: opts.label.split(",").map((l) => l.trim()),
|
|
50
|
+
}),
|
|
57
51
|
};
|
|
58
52
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
headers: {
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
Authorization: `Basic ${Buffer.from(`${cfg.jiraEmail}:${cfg.jiraApiToken}`).toString("base64")}`,
|
|
64
|
-
},
|
|
65
|
-
body: JSON.stringify(jiraPayload),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
const body = await response.text();
|
|
70
|
-
throw new Error(`JIRA returned ${response.status}: ${body}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const data = await response.json();
|
|
74
|
-
planKey = data.key;
|
|
53
|
+
const issue = await createIssue(cfg, "Test Plan", fields);
|
|
54
|
+
planKey = issue.key;
|
|
75
55
|
} catch (err) {
|
|
76
56
|
// Fallback: try via Xray GraphQL createTestPlan mutation
|
|
77
57
|
logger.debug(`JIRA REST failed, trying Xray GraphQL: ${err.message}`);
|
|
78
58
|
try {
|
|
79
59
|
const result = await createTestPlan(cfg, xrayToken, {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
60
|
+
projectId: cfg.jiraProjectKey,
|
|
61
|
+
summary,
|
|
62
|
+
description: summary,
|
|
84
63
|
});
|
|
85
|
-
|
|
64
|
+
|
|
65
|
+
// GraphQL returns { testPlan: { issueId, projectId } } — resolve key via JIRA
|
|
66
|
+
const issueId = result?.testPlan?.issueId;
|
|
67
|
+
if (issueId) {
|
|
68
|
+
try {
|
|
69
|
+
const issue = await getIssue(cfg, issueId);
|
|
70
|
+
planKey = issue?.key;
|
|
71
|
+
} catch {
|
|
72
|
+
logger.debug(`Could not resolve JIRA key for issueId ${issueId}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
86
75
|
} catch (gqlErr) {
|
|
87
76
|
logger.error(`Could not create Test Plan: ${gqlErr.message}`);
|
|
88
77
|
process.exit(1);
|
|
@@ -88,6 +88,7 @@ export default async function importResults(opts = {}) {
|
|
|
88
88
|
...(!execKey && planKey && { testPlanKey: planKey }),
|
|
89
89
|
...(opts.version && { version: opts.version }),
|
|
90
90
|
...(opts.revision && { revision: opts.revision }),
|
|
91
|
+
...(opts.fixVersion && { fixVersion: opts.fixVersion }),
|
|
91
92
|
};
|
|
92
93
|
|
|
93
94
|
let result;
|
|
@@ -101,6 +102,7 @@ export default async function importResults(opts = {}) {
|
|
|
101
102
|
|
|
102
103
|
xrayJson = convertPlaywrightToXray(playwrightJson, {
|
|
103
104
|
projectKey: cfg.jiraProjectKey,
|
|
105
|
+
statusMapping: cfg.statusMapping || undefined,
|
|
104
106
|
infoOverrides: infoBase,
|
|
105
107
|
});
|
|
106
108
|
|
package/commands/init.js
CHANGED
|
@@ -200,6 +200,8 @@ export default async function init(opts = {}) {
|
|
|
200
200
|
environments: ["IOP-DEV", "IOP-QA", "IOP-PROD"],
|
|
201
201
|
folderRoot: `/${serviceName}`,
|
|
202
202
|
xrayRegion: "us",
|
|
203
|
+
bulkImportThreshold: 50,
|
|
204
|
+
// statusMapping: { interrupted: "ABORTED", skipped: "TODO" }
|
|
203
205
|
};
|
|
204
206
|
fs.writeFileSync(xrayrcPath, JSON.stringify(xrayrc, null, 2));
|
|
205
207
|
logger.success(".xrayrc created — Project config");
|
package/commands/pushTests.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* 1. Load & validate config + tests.json
|
|
9
9
|
* 2. Authenticate with Xray (once)
|
|
10
10
|
* 3. Build & push tests (create/update via GraphQL)
|
|
11
|
-
* 4. Sync Test
|
|
12
|
-
* 5. Sync
|
|
11
|
+
* 4. Sync Test Sets — create sets by testSet name, add tests (mapping._testSets)
|
|
12
|
+
* 5. Sync Test Plan membership (add new keys)
|
|
13
|
+
* 6. Sync Xray folder structure from test `folder` fields
|
|
14
|
+
* 7. Optionally create a Test Execution linked to the plan
|
|
13
15
|
* 6. Save mapping
|
|
14
16
|
*/
|
|
15
17
|
|
|
@@ -17,11 +19,13 @@ import fs from "node:fs";
|
|
|
17
19
|
import { createRequire } from "node:module";
|
|
18
20
|
import logger, { setVerbose } from "../lib/logger.js";
|
|
19
21
|
import { loadConfig, validateConfig } from "../lib/config.js";
|
|
20
|
-
import {
|
|
22
|
+
import { readJsonFile } from "../lib/jsonFile.js";
|
|
23
|
+
import { authenticate, getIssue, createTestExecution } from "../lib/xrayClient.js";
|
|
21
24
|
import {
|
|
22
25
|
buildAndPush,
|
|
23
26
|
loadMapping,
|
|
24
27
|
saveMapping,
|
|
28
|
+
syncTestSets,
|
|
25
29
|
syncTestPlan,
|
|
26
30
|
syncFolders,
|
|
27
31
|
} from "../lib/testCaseBuilder.js";
|
|
@@ -41,7 +45,7 @@ export default async function pushTests(opts = {}) {
|
|
|
41
45
|
process.exit(1);
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
const testsConfig =
|
|
48
|
+
const testsConfig = readJsonFile(cfg.testsPath);
|
|
45
49
|
const allTests = testsConfig.tests || [];
|
|
46
50
|
const tests = allTests.filter((t) => !t.skip);
|
|
47
51
|
const skippedCount = allTests.length - tests.length;
|
|
@@ -58,9 +62,7 @@ export default async function pushTests(opts = {}) {
|
|
|
58
62
|
try {
|
|
59
63
|
const require = createRequire(import.meta.url);
|
|
60
64
|
const Ajv = require("ajv");
|
|
61
|
-
const schema =
|
|
62
|
-
fs.readFileSync(new URL("../schema/tests.schema.json", import.meta.url), "utf8")
|
|
63
|
-
);
|
|
65
|
+
const schema = readJsonFile(new URL("../schema/tests.schema.json", import.meta.url));
|
|
64
66
|
const ajv = new Ajv({ allErrors: true });
|
|
65
67
|
const validate = ajv.compile(schema);
|
|
66
68
|
if (!validate(testsConfig)) {
|
|
@@ -99,6 +101,31 @@ export default async function pushTests(opts = {}) {
|
|
|
99
101
|
saveMapping(cfg.mappingPath, mapping);
|
|
100
102
|
logger.save(`Mapping saved to ${cfg.mappingPath}`);
|
|
101
103
|
|
|
104
|
+
// ── Sync Test Sets ──────────────────────────────────────────────────
|
|
105
|
+
const testsWithSets = tests.filter((t) => t.testSet && (Array.isArray(t.testSet) ? t.testSet.length > 0 : true));
|
|
106
|
+
|
|
107
|
+
if (testsWithSets.length > 0) {
|
|
108
|
+
logger.blank();
|
|
109
|
+
const uniqueSets = [...new Set(testsWithSets.flatMap((t) => Array.isArray(t.testSet) ? t.testSet : [t.testSet]))];
|
|
110
|
+
logger.send(`Syncing ${uniqueSets.length} Test Set(s) (${testsWithSets.length} test(s))...`);
|
|
111
|
+
try {
|
|
112
|
+
const setResult = await syncTestSets(cfg, xrayToken, tests, mapping, cfg.mappingPath);
|
|
113
|
+
logger.success(
|
|
114
|
+
`Test Sets: ${setResult.created} created, ${setResult.synced} test(s) assigned` +
|
|
115
|
+
(setResult.failed.length > 0 ? `, ${setResult.failed.length} failed` : "")
|
|
116
|
+
);
|
|
117
|
+
if (setResult.failed.length > 0) {
|
|
118
|
+
for (const name of setResult.failed) {
|
|
119
|
+
logger.warn(` FAILED: Test Set "${name}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.warn(`Test Set sync failed: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
logger.debug("No testSet fields — skipping Test Set sync");
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
// ── Sync Test Plan ─────────────────────────────────────────────────────────
|
|
103
130
|
const planKey = opts.plan || cfg.testPlanKey || testsConfig.testPlan?.key;
|
|
104
131
|
|
|
@@ -151,6 +178,37 @@ export default async function pushTests(opts = {}) {
|
|
|
151
178
|
logger.debug("No folder fields — skipping folder sync");
|
|
152
179
|
}
|
|
153
180
|
|
|
181
|
+
// ── Create Test Execution (optional) ───────────────────────────────────────
|
|
182
|
+
if (opts.createExecution && planKey) {
|
|
183
|
+
logger.blank();
|
|
184
|
+
logger.send("Creating Test Execution linked to Test Plan...");
|
|
185
|
+
try {
|
|
186
|
+
const environment = opts.executionEnv || cfg.defaultEnvironment || "IOP-DEV";
|
|
187
|
+
const testIssueIds = Object.values(mapping)
|
|
188
|
+
.filter((v) => v.id && !String(v.id).startsWith("_"))
|
|
189
|
+
.map((v) => String(v.id));
|
|
190
|
+
|
|
191
|
+
const execSummary =
|
|
192
|
+
opts.executionSummary ||
|
|
193
|
+
`${environment} Test Run — ${new Date().toLocaleString()}`;
|
|
194
|
+
|
|
195
|
+
const { key: execKey } = await createTestExecution(cfg, xrayToken, {
|
|
196
|
+
summary: execSummary,
|
|
197
|
+
environments: [environment],
|
|
198
|
+
testIssueIds,
|
|
199
|
+
testPlanKey: planKey,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
logger.success(`Test Execution created: ${execKey}`);
|
|
203
|
+
logger.link(`${cfg.jiraUrl}/browse/${execKey}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
logger.warn(`Test Execution creation failed: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
} else if (opts.createExecution && !planKey) {
|
|
208
|
+
logger.blank();
|
|
209
|
+
logger.warn("--create-execution requires a Test Plan key (--plan or .xrayrc testPlanKey)");
|
|
210
|
+
}
|
|
211
|
+
|
|
154
212
|
logger.blank();
|
|
155
213
|
logger.success("push-tests complete");
|
|
156
214
|
logger.blank();
|
package/lib/config.js
CHANGED
|
@@ -97,6 +97,13 @@ export function loadConfig(opts = {}) {
|
|
|
97
97
|
environments: rcConfig.environments || defaultEnvironments,
|
|
98
98
|
folderRoot: rcConfig.folderRoot || null,
|
|
99
99
|
|
|
100
|
+
// Playwright status mapping (overrides defaults per-project)
|
|
101
|
+
// e.g. { "interrupted": "ABORTED", "skipped": "TODO" }
|
|
102
|
+
statusMapping: rcConfig.statusMapping || null,
|
|
103
|
+
|
|
104
|
+
// Bulk import threshold — use Xray REST bulk API when creating this many new tests
|
|
105
|
+
bulkImportThreshold: Number(process.env.XQT_BULK_THRESHOLD ?? rcConfig.bulkImportThreshold ?? 50),
|
|
106
|
+
|
|
100
107
|
// Paths (resolved relative to consuming project)
|
|
101
108
|
testsPath: path.resolve(cwd, rcConfig.testsPath || "tests.json"),
|
|
102
109
|
mappingPath: path.resolve(cwd, rcConfig.mappingPath || "xray-mapping.json"),
|
package/lib/jsonFile.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a JSON file and tolerate UTF-8 BOM at file start.
|
|
5
|
+
* @param {string | URL} filePath
|
|
6
|
+
* @returns {any}
|
|
7
|
+
*/
|
|
8
|
+
export function readJsonFile(filePath) {
|
|
9
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
10
|
+
const content = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
}
|
|
@@ -20,8 +20,10 @@ import path from "node:path";
|
|
|
20
20
|
* @param {object} playwrightJson - Playwright JSON reporter output
|
|
21
21
|
* @param {object} options - Conversion options
|
|
22
22
|
* @param {string} [options.projectKey] - JIRA project key (for new test creation)
|
|
23
|
+
* @param {object} [options.statusMapping] - Override default Playwright→Xray status map
|
|
24
|
+
* e.g. { interrupted: "ABORTED", skipped: "TODO" }
|
|
23
25
|
* @param {object} [options.infoOverrides] - Merged into the Xray `info` object:
|
|
24
|
-
* testPlanKey, testEnvironments, version, revision, summary, description
|
|
26
|
+
* testPlanKey, testEnvironments, version, revision, fixVersion, summary, description
|
|
25
27
|
* @returns {object} Xray JSON format ready for import
|
|
26
28
|
*/
|
|
27
29
|
export function convertPlaywrightToXray(playwrightJson, options = {}) {
|
|
@@ -110,7 +112,7 @@ function convertTest(test, spec, suite, options) {
|
|
|
110
112
|
const result = test.results[test.results.length - 1];
|
|
111
113
|
|
|
112
114
|
const testKey = extractTestKey(spec.title, test.annotations);
|
|
113
|
-
const status = mapStatus(result.status);
|
|
115
|
+
const status = mapStatus(result.status, options.statusMapping);
|
|
114
116
|
|
|
115
117
|
const xrayTest = {
|
|
116
118
|
status,
|
|
@@ -145,14 +147,16 @@ function convertTest(test, spec, suite, options) {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
// ── Map Playwright steps to Xray step results ─────────────────────────────
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
const relevantSteps = result.steps
|
|
151
|
+
? result.steps.filter((s) => s.category === "test.step" || s.category === "hook")
|
|
152
|
+
: [];
|
|
153
|
+
|
|
154
|
+
if (relevantSteps.length > 0) {
|
|
155
|
+
xrayTest.steps = relevantSteps.map((s) => ({
|
|
156
|
+
status: s.error ? "FAILED" : "PASSED",
|
|
157
|
+
comment: s.title || s.category,
|
|
158
|
+
...(s.duration && { duration: Math.round(s.duration) }),
|
|
159
|
+
}));
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
// ── Error details ──────────────────────────────────────────────────────────
|
|
@@ -162,35 +166,34 @@ function convertTest(test, spec, suite, options) {
|
|
|
162
166
|
xrayTest.comment += `\n\nError: ${msg}${stack}`;
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
// ── Attachments / Evidence
|
|
169
|
+
// ── Attachments / Evidence (with per-step attachment for screenshots) ──────
|
|
166
170
|
if (result.attachments && result.attachments.length > 0) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
? attachment.body.toString("base64")
|
|
175
|
-
: Buffer.from(attachment.body).toString("base64");
|
|
176
|
-
} else if (attachment.path && fs.existsSync(attachment.path)) {
|
|
177
|
-
// File on disk
|
|
178
|
-
data = fs.readFileSync(attachment.path).toString("base64");
|
|
179
|
-
}
|
|
171
|
+
const screenshots = result.attachments.filter(
|
|
172
|
+
(a) => a.contentType?.startsWith("image/") || a.name?.match(/screenshot/i)
|
|
173
|
+
);
|
|
174
|
+
const traces = result.attachments.filter((a) => a.name?.match(/trace/i));
|
|
175
|
+
const other = result.attachments.filter(
|
|
176
|
+
(a) => !screenshots.includes(a) && !traces.includes(a)
|
|
177
|
+
);
|
|
180
178
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
// Find the last failed step index for per-step evidence attachment
|
|
180
|
+
const lastFailedStepIdx =
|
|
181
|
+
relevantSteps.reduce((acc, s, i) => (s.error ? i : acc), -1);
|
|
182
|
+
|
|
183
|
+
// Attach screenshots to the last failed step (per-step evidence)
|
|
184
|
+
if (xrayTest.steps && lastFailedStepIdx >= 0 && screenshots.length > 0) {
|
|
185
|
+
xrayTest.steps[lastFailedStepIdx].evidences = screenshots
|
|
186
|
+
.map((a) => buildEvidenceItem(a))
|
|
187
|
+
.filter(Boolean);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
// Everything else (traces, other) stays at test level
|
|
191
|
+
const testLevelAttachments = [...(lastFailedStepIdx >= 0 ? [] : screenshots), ...traces, ...other];
|
|
192
|
+
const testLevelEvidence = testLevelAttachments.map((a) => buildEvidenceItem(a)).filter(Boolean);
|
|
193
|
+
|
|
194
|
+
if (testLevelEvidence.length > 0) {
|
|
195
|
+
xrayTest.evidence = testLevelEvidence;
|
|
196
|
+
xrayTest.comment += `\n\nAttachments: ${testLevelAttachments.map((a) => a.name).join(", ")}`;
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
@@ -209,16 +212,36 @@ function extractTestKey(title, annotations) {
|
|
|
209
212
|
return match ? match[1] : null;
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
function mapStatus(playwrightStatus) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
function mapStatus(playwrightStatus, customMapping) {
|
|
216
|
+
const defaults = {
|
|
217
|
+
passed: "PASSED",
|
|
218
|
+
failed: "FAILED",
|
|
219
|
+
timedOut: "FAILED",
|
|
220
|
+
interrupted: "ABORTED",
|
|
221
|
+
skipped: "TODO",
|
|
222
|
+
};
|
|
223
|
+
const map = customMapping ? { ...defaults, ...customMapping } : defaults;
|
|
224
|
+
return map[playwrightStatus] || "TODO";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildEvidenceItem(attachment) {
|
|
228
|
+
let data = null;
|
|
229
|
+
|
|
230
|
+
if (attachment.body) {
|
|
231
|
+
data = Buffer.isBuffer(attachment.body)
|
|
232
|
+
? attachment.body.toString("base64")
|
|
233
|
+
: Buffer.from(attachment.body).toString("base64");
|
|
234
|
+
} else if (attachment.path && fs.existsSync(attachment.path)) {
|
|
235
|
+
data = fs.readFileSync(attachment.path).toString("base64");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!data) return null;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
filename: attachment.name || path.basename(attachment.path || "attachment"),
|
|
242
|
+
contentType: attachment.contentType || "application/octet-stream",
|
|
243
|
+
data,
|
|
244
|
+
};
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
function buildComment(result, spec, suite) {
|