@msalaam/xray-qe-toolkit 1.1.0 → 1.2.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 +249 -16
- package/bin/cli.js +6 -4
- package/commands/importResults.js +44 -10
- package/lib/playwrightConverter.js +297 -0
- package/lib/postmanGenerator.js +1 -2
- package/lib/xrayClient.js +24 -1
- package/package.json +1 -1
- package/templates/tests.json +564 -33
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @msalaam/xray-qe-toolkit — Playwright to Xray JSON Converter
|
|
3
|
+
*
|
|
4
|
+
* Converts Playwright test results JSON format to Xray Cloud JSON import format.
|
|
5
|
+
* Supports mapping Playwright test annotations to Xray test keys.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert Playwright JSON results to Xray JSON import format.
|
|
10
|
+
*
|
|
11
|
+
* Playwright test mapping:
|
|
12
|
+
* - Test annotations like `test.info().annotations.push({type: 'xray', description: 'PROJ-123'})`
|
|
13
|
+
* - Or test titles that include [PROJ-123] format
|
|
14
|
+
* - Falls back to creating test summary from test title
|
|
15
|
+
*
|
|
16
|
+
* @param {object} playwrightJson - Playwright JSON reporter output
|
|
17
|
+
* @param {object} options - Conversion options
|
|
18
|
+
* @param {string} options.testExecutionKey - Xray Test Execution key to link results
|
|
19
|
+
* @param {string} options.projectKey - JIRA project key for test creation
|
|
20
|
+
* @param {string} options.summary - Test Execution summary (optional)
|
|
21
|
+
* @param {string} options.description - Test Execution description (optional)
|
|
22
|
+
* @returns {object} Xray JSON format
|
|
23
|
+
*/
|
|
24
|
+
export function convertPlaywrightToXray(playwrightJson, options = {}) {
|
|
25
|
+
const startTime = new Date(playwrightJson.config?.metadata?.actualWorkers ?
|
|
26
|
+
Date.now() - getTotalDuration(playwrightJson) : Date.now()).toISOString();
|
|
27
|
+
const finishTime = new Date().toISOString();
|
|
28
|
+
|
|
29
|
+
const xrayJson = {
|
|
30
|
+
info: {
|
|
31
|
+
summary: options.summary || `Playwright Test Execution - ${new Date().toISOString()}`,
|
|
32
|
+
description: options.description || "Automated Playwright test execution results",
|
|
33
|
+
startDate: startTime,
|
|
34
|
+
finishDate: finishTime,
|
|
35
|
+
},
|
|
36
|
+
tests: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Add testExecutionKey if provided
|
|
40
|
+
if (options.testExecutionKey) {
|
|
41
|
+
xrayJson.info.testExecutionKey = options.testExecutionKey;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add project if provided and no testExecutionKey
|
|
45
|
+
if (options.projectKey && !options.testExecutionKey) {
|
|
46
|
+
xrayJson.info.project = options.projectKey;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Process all test suites
|
|
50
|
+
if (playwrightJson.suites) {
|
|
51
|
+
for (const suite of playwrightJson.suites) {
|
|
52
|
+
processTestSuite(suite, xrayJson.tests, options);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return xrayJson;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get total duration from Playwright results
|
|
61
|
+
* @param {object} playwrightJson
|
|
62
|
+
* @returns {number} Duration in milliseconds
|
|
63
|
+
*/
|
|
64
|
+
function getTotalDuration(playwrightJson) {
|
|
65
|
+
let total = 0;
|
|
66
|
+
if (playwrightJson.suites) {
|
|
67
|
+
for (const suite of playwrightJson.suites) {
|
|
68
|
+
total += getSuiteDuration(suite);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return total;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get duration of a test suite
|
|
76
|
+
* @param {object} suite
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
function getSuiteDuration(suite) {
|
|
80
|
+
let duration = 0;
|
|
81
|
+
if (suite.specs) {
|
|
82
|
+
for (const spec of suite.specs) {
|
|
83
|
+
if (spec.tests) {
|
|
84
|
+
for (const test of spec.tests) {
|
|
85
|
+
if (test.results) {
|
|
86
|
+
for (const result of test.results) {
|
|
87
|
+
duration += result.duration || 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (suite.suites) {
|
|
95
|
+
for (const subSuite of suite.suites) {
|
|
96
|
+
duration += getSuiteDuration(subSuite);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return duration;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Process a test suite recursively
|
|
104
|
+
* @param {object} suite
|
|
105
|
+
* @param {array} xrayTests
|
|
106
|
+
* @param {object} options
|
|
107
|
+
*/
|
|
108
|
+
function processTestSuite(suite, xrayTests, options) {
|
|
109
|
+
// Process specs in this suite
|
|
110
|
+
if (suite.specs) {
|
|
111
|
+
for (const spec of suite.specs) {
|
|
112
|
+
if (spec.tests) {
|
|
113
|
+
for (const test of spec.tests) {
|
|
114
|
+
const xrayTest = convertTest(test, spec, suite, options);
|
|
115
|
+
if (xrayTest) {
|
|
116
|
+
xrayTests.push(xrayTest);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Process nested suites
|
|
124
|
+
if (suite.suites) {
|
|
125
|
+
for (const subSuite of suite.suites) {
|
|
126
|
+
processTestSuite(subSuite, xrayTests, options);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Convert a single Playwright test to Xray test format
|
|
133
|
+
* @param {object} test - Playwright test
|
|
134
|
+
* @param {object} spec - Playwright spec
|
|
135
|
+
* @param {object} suite - Playwright suite
|
|
136
|
+
* @param {object} options
|
|
137
|
+
* @returns {object|null}
|
|
138
|
+
*/
|
|
139
|
+
function convertTest(test, spec, suite, options) {
|
|
140
|
+
if (!test.results || test.results.length === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get the last result (retries)
|
|
145
|
+
const result = test.results[test.results.length - 1];
|
|
146
|
+
|
|
147
|
+
// Extract Xray test key from annotations or title
|
|
148
|
+
const testKey = extractTestKey(spec.title, test.annotations);
|
|
149
|
+
|
|
150
|
+
// Map Playwright status to Xray status
|
|
151
|
+
const status = mapStatus(result.status);
|
|
152
|
+
|
|
153
|
+
const xrayTest = {
|
|
154
|
+
status,
|
|
155
|
+
comment: buildComment(result, spec, suite),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Add testKey if found, otherwise use testInfo for test creation
|
|
159
|
+
if (testKey) {
|
|
160
|
+
xrayTest.testKey = testKey;
|
|
161
|
+
} else {
|
|
162
|
+
// Create test info for new test creation
|
|
163
|
+
xrayTest.testInfo = {
|
|
164
|
+
summary: spec.title,
|
|
165
|
+
type: "Generic",
|
|
166
|
+
projectKey: options.projectKey,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Add file path as label if available
|
|
170
|
+
if (spec.file) {
|
|
171
|
+
xrayTest.testInfo.labels = [extractFileName(spec.file)];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Add duration if available (convert to seconds for Xray)
|
|
176
|
+
if (result.duration) {
|
|
177
|
+
xrayTest.duration = Math.round(result.duration);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Add start/finish times if available
|
|
181
|
+
if (result.startTime) {
|
|
182
|
+
xrayTest.start = new Date(result.startTime).toISOString();
|
|
183
|
+
}
|
|
184
|
+
if (result.startTime && result.duration) {
|
|
185
|
+
xrayTest.finish = new Date(result.startTime + result.duration).toISOString();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add error details if test failed
|
|
189
|
+
if (result.error) {
|
|
190
|
+
xrayTest.comment += `\n\nError: ${result.error.message || ''}`;
|
|
191
|
+
if (result.error.stack) {
|
|
192
|
+
xrayTest.comment += `\n\nStack trace:\n${result.error.stack}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add evidence/attachments if available
|
|
197
|
+
if (result.attachments && result.attachments.length > 0) {
|
|
198
|
+
xrayTest.evidence = result.attachments.map(attachment => ({
|
|
199
|
+
filename: attachment.name || 'attachment',
|
|
200
|
+
contentType: attachment.contentType || 'application/octet-stream',
|
|
201
|
+
// Note: Xray expects base64 data or URL - Playwright attachments need to be read separately
|
|
202
|
+
data: attachment.body ? attachment.body.toString('base64') : null
|
|
203
|
+
})).filter(e => e.data); // Only include attachments with data
|
|
204
|
+
|
|
205
|
+
// Add attachment info to comment
|
|
206
|
+
if (xrayTest.evidence.length > 0) {
|
|
207
|
+
xrayTest.comment += `\n\nAttachments: ${result.attachments.map(a => a.name).join(', ')}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return xrayTest;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extract Xray test key from title or annotations
|
|
216
|
+
* @param {string} title
|
|
217
|
+
* @param {array} annotations
|
|
218
|
+
* @returns {string|null}
|
|
219
|
+
*/
|
|
220
|
+
function extractTestKey(title, annotations) {
|
|
221
|
+
// Check annotations first
|
|
222
|
+
if (annotations) {
|
|
223
|
+
for (const annotation of annotations) {
|
|
224
|
+
if (annotation.type === 'xray' && annotation.description) {
|
|
225
|
+
return annotation.description;
|
|
226
|
+
}
|
|
227
|
+
if (annotation.type === 'test-id' && annotation.description) {
|
|
228
|
+
return annotation.description;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check title for [PROJ-123] format
|
|
234
|
+
const match = title.match(/\[([\w]+-\d+)\]/);
|
|
235
|
+
if (match) {
|
|
236
|
+
return match[1];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Map Playwright status to Xray status
|
|
244
|
+
* @param {string} playwrightStatus
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
function mapStatus(playwrightStatus) {
|
|
248
|
+
const statusMap = {
|
|
249
|
+
passed: 'PASSED',
|
|
250
|
+
failed: 'FAILED',
|
|
251
|
+
timedOut: 'FAILED',
|
|
252
|
+
interrupted: 'ABORTED',
|
|
253
|
+
skipped: 'TODO',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return statusMap[playwrightStatus] || 'TODO';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Build comment text for test result
|
|
261
|
+
* @param {object} result
|
|
262
|
+
* @param {object} spec
|
|
263
|
+
* @param {object} suite
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
function buildComment(result, spec, suite) {
|
|
267
|
+
const parts = [];
|
|
268
|
+
|
|
269
|
+
if (suite.title) {
|
|
270
|
+
parts.push(`Suite: ${suite.title}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
parts.push(`Test: ${spec.title}`);
|
|
274
|
+
|
|
275
|
+
if (spec.file) {
|
|
276
|
+
parts.push(`File: ${extractFileName(spec.file)}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (result.retry > 0) {
|
|
280
|
+
parts.push(`Retry: ${result.retry}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (result.workerIndex !== undefined) {
|
|
284
|
+
parts.push(`Worker: ${result.workerIndex}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return parts.join('\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Extract filename from full path
|
|
292
|
+
* @param {string} filePath
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
function extractFileName(filePath) {
|
|
296
|
+
return filePath.split(/[/\\]/).pop() || filePath;
|
|
297
|
+
}
|
package/lib/postmanGenerator.js
CHANGED
|
@@ -216,8 +216,7 @@ function inferEndpoint(step) {
|
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
218
|
* Build Postman test script lines from an expected result.
|
|
219
|
-
*/
|
|
220
|
-
`// SCAFFOLD: Enhance this assertion based on actual API behavior
|
|
219
|
+
*/
|
|
221
220
|
function buildTestScript(step) {
|
|
222
221
|
const lines = [
|
|
223
222
|
`// Expected: ${step.expected_result}`,
|
package/lib/xrayClient.js
CHANGED
|
@@ -329,7 +329,7 @@ export async function linkMultiple(cfg, testKeys, containerKey) {
|
|
|
329
329
|
return { linked, failed };
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
// ─── Xray import
|
|
332
|
+
// ─── Xray import endpoints ─────────────────────────────────────────────────────
|
|
333
333
|
|
|
334
334
|
/**
|
|
335
335
|
* Import JUnit/XUnit XML results into Xray Cloud.
|
|
@@ -359,6 +359,29 @@ export async function importResults(cfg, xrayToken, xmlBuffer, testExecKey) {
|
|
|
359
359
|
return response.data;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Import test results using Xray's native JSON format.
|
|
364
|
+
* This format supports detailed test information including custom fields and evidence.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} cfg
|
|
367
|
+
* @param {string} xrayToken JWT
|
|
368
|
+
* @param {object} xrayJson Xray JSON format object
|
|
369
|
+
* @returns {Promise<object>}
|
|
370
|
+
*/
|
|
371
|
+
export async function importResultsXrayJson(cfg, xrayToken, xrayJson) {
|
|
372
|
+
const url = "https://xray.cloud.getxray.app/api/v2/import/execution";
|
|
373
|
+
|
|
374
|
+
const response = await axios.post(url, xrayJson, {
|
|
375
|
+
httpsAgent,
|
|
376
|
+
headers: {
|
|
377
|
+
Authorization: `Bearer ${xrayToken}`,
|
|
378
|
+
"Content-Type": "application/json",
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return response.data;
|
|
383
|
+
}
|
|
384
|
+
|
|
362
385
|
// ─── Retry wrapper ─────────────────────────────────────────────────────────────
|
|
363
386
|
|
|
364
387
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msalaam/xray-qe-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Full QE workflow toolkit for Xray Cloud integration — test management, Postman generation, CI pipeline scaffolding, and browser-based review gates for API regression projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|