@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.
@@ -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
+ }
@@ -216,8 +216,7 @@ function inferEndpoint(step) {
216
216
 
217
217
  /**
218
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
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 endpoint ──────────────────────────────────────────────────────
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.1.0",
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": {