@msalaam/xray-qe-toolkit 1.3.0 → 1.3.2

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 CHANGED
@@ -861,6 +861,22 @@ npx playwright show-report
861
861
  npx xqt import-results --file playwright-results.json --testExecKey APIEE-6811
862
862
  ```
863
863
 
864
+ **What happens:**
865
+ - ✅ Tests WITH annotations (`test.info().annotations.push(...)`) → Updates existing Xray tests
866
+ - ⏭️ Tests WITHOUT annotations → **Automatically skipped** (won't create duplicates)
867
+ - 📊 Summary shows: Passed, Failed, Skipped counts
868
+ - 🔗 Direct link to view results in Xray
869
+
870
+ **Verbose mode** (see exactly what's being uploaded):
871
+ ```bash
872
+ npx xqt import-results \
873
+ --file playwright-results.json \
874
+ --testExecKey APIEE-6811 \
875
+ --verbose
876
+ ```
877
+
878
+ This saves `playwright-results-xray-debug.json` for inspection.
879
+
864
880
  #### Step 6: Configure CI/CD
865
881
 
866
882
  **Azure Pipelines:**
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * @msalaam/xray-qe-toolkit — CLI Entry Point
@@ -48,7 +48,7 @@ export default async function importResults(opts = {}) {
48
48
 
49
49
  if (fileExt === '.json') {
50
50
  // Playwright JSON format
51
- logger.send(`Importing Playwright JSON results...`);
51
+ logger.send(`Converting Playwright results to Xray format...`);
52
52
 
53
53
  const playwrightJson = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
54
54
 
@@ -58,11 +58,38 @@ export default async function importResults(opts = {}) {
58
58
  projectKey: cfg.jiraProjectKey,
59
59
  summary: opts.summary || `Playwright Test Execution - ${new Date().toLocaleString()}`,
60
60
  description: opts.description,
61
+ skipWithoutAnnotations: !!opts.testExecKey, // Skip tests without keys when updating existing execution
61
62
  });
62
63
 
63
- logger.step(`Converted ${xrayJson.tests.length} test results to Xray format`);
64
+ // Check if we have tests to upload
65
+ if (xrayJson.tests.length === 0) {
66
+ logger.warn('⚠️ No tests with Xray annotations found.');
67
+ logger.info('\nAdd annotations to your tests:');
68
+ logger.info(` test.info().annotations.push({ type: 'xray', description: 'APIEE-XXXX' });\n`);
69
+ process.exit(0);
70
+ }
71
+
72
+ const testsWithKeys = xrayJson.tests.filter(t => t.testKey).length;
73
+ const testsWithoutKeys = xrayJson.tests.filter(t => !t.testKey).length;
74
+
75
+ logger.step(`Found ${testsWithKeys} test(s) with Xray annotations`);
76
+
77
+ if (testsWithoutKeys > 0 && !opts.testExecKey) {
78
+ logger.info(` ${testsWithoutKeys} test(s) without annotations will create new tests`);
79
+ } else if (testsWithoutKeys > 0 && opts.testExecKey) {
80
+ logger.warn(` Skipped ${testsWithoutKeys} test(s) without annotations`);
81
+ }
82
+
83
+ // Save debug output if verbose
84
+ if (opts.verbose) {
85
+ const debugPath = filePath.replace('.json', '-xray-debug.json');
86
+ fs.writeFileSync(debugPath, JSON.stringify(xrayJson, null, 2));
87
+ logger.info(` Debug: Xray JSON saved to ${path.basename(debugPath)}`);
88
+ }
89
+ logger.blank();
64
90
 
65
91
  // Import to Xray
92
+ logger.send(`Uploading ${xrayJson.tests.length} test result(s) to Xray...`);
66
93
  result = await importResultsXrayJson(cfg, xrayToken, xrayJson);
67
94
  } else {
68
95
  // JUnit XML format (default)
@@ -73,12 +100,46 @@ export default async function importResults(opts = {}) {
73
100
  }
74
101
 
75
102
  logger.success("Results imported successfully");
103
+ logger.blank();
76
104
 
105
+ // Display summary for JSON results
106
+ if (fileExt === '.json' && xrayJson) {
107
+ const summaryStats = calculateTestSummary(xrayJson.tests);
108
+
109
+ logger.info('Summary:');
110
+ logger.success(` ✓ Passed: ${summaryStats.passed}`)
111
+ ;
112
+ if (summaryStats.failed > 0) {
113
+ logger.error(` ✗ Failed: ${summaryStats.failed}`);
114
+ }
115
+ if (summaryStats.skipped > 0) {
116
+ logger.warn(` ⊘ Skipped: ${summaryStats.skipped}`);
117
+ }
118
+ logger.blank();
119
+ }
120
+
121
+ // Show link to Test Execution
77
122
  if (result?.key) {
78
123
  logger.link(`View: ${cfg.jiraUrl}/browse/${result.key}`);
79
124
  } else if (result?.testExecIssue?.key) {
80
125
  logger.link(`View: ${cfg.jiraUrl}/browse/${result.testExecIssue.key}`);
126
+ } else if (opts.testExecKey) {
127
+ logger.link(`View: ${cfg.jiraUrl}/browse/${opts.testExecKey}`);
81
128
  }
82
129
 
83
130
  logger.blank();
84
131
  }
132
+
133
+ /**
134
+ * Calculate test result summary
135
+ * @param {array} tests - Xray test results
136
+ * @returns {object} Summary stats
137
+ */
138
+ function calculateTestSummary(tests) {
139
+ return {
140
+ passed: tests.filter(t => t.status === 'PASSED').length,
141
+ failed: tests.filter(t => t.status === 'FAILED').length,
142
+ skipped: tests.filter(t => t.status === 'SKIPPED').length,
143
+ total: tests.length,
144
+ };
145
+ }
@@ -19,12 +19,16 @@
19
19
  * @param {string} options.projectKey - JIRA project key for test creation
20
20
  * @param {string} options.summary - Test Execution summary (optional)
21
21
  * @param {string} options.description - Test Execution description (optional)
22
+ * @param {boolean} options.skipWithoutAnnotations - Skip tests without Xray annotations (default: false)
22
23
  * @returns {object} Xray JSON format
23
24
  */
24
25
  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();
26
+ // Use current time as fallback for timestamps
27
+ const now = Date.now();
28
+ const totalDuration = getTotalDuration(playwrightJson);
29
+
30
+ const startTime = new Date(now - totalDuration).toISOString();
31
+ const finishTime = new Date(now).toISOString();
28
32
 
29
33
  const xrayJson = {
30
34
  info: {
@@ -147,6 +151,11 @@ function convertTest(test, spec, suite, options) {
147
151
  // Extract Xray test key from annotations or title
148
152
  const testKey = extractTestKey(spec.title, test.annotations);
149
153
 
154
+ // Skip tests without annotations if requested
155
+ if (options.skipWithoutAnnotations && !testKey) {
156
+ return null;
157
+ }
158
+
150
159
  // Map Playwright status to Xray status
151
160
  const status = mapStatus(result.status);
152
161
 
@@ -179,10 +188,22 @@ function convertTest(test, spec, suite, options) {
179
188
 
180
189
  // Add start/finish times if available
181
190
  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();
191
+ try {
192
+ const startDate = new Date(result.startTime);
193
+ if (!isNaN(startDate.getTime())) {
194
+ xrayTest.start = startDate.toISOString();
195
+
196
+ // Calculate finish time if we have duration
197
+ if (result.duration) {
198
+ const finishDate = new Date(startDate.getTime() + result.duration);
199
+ if (!isNaN(finishDate.getTime())) {
200
+ xrayTest.finish = finishDate.toISOString();
201
+ }
202
+ }
203
+ }
204
+ } catch (error) {
205
+ // Skip invalid timestamps silently
206
+ }
186
207
  }
187
208
 
188
209
  // Add error details if test failed
package/lib/xrayClient.js CHANGED
@@ -371,15 +371,51 @@ export async function importResults(cfg, xrayToken, xmlBuffer, testExecKey) {
371
371
  export async function importResultsXrayJson(cfg, xrayToken, xrayJson) {
372
372
  const url = "https://xray.cloud.getxray.app/api/v2/import/execution";
373
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;
374
+ try {
375
+ const response = await axios.post(url, xrayJson, {
376
+ httpsAgent,
377
+ headers: {
378
+ Authorization: `Bearer ${xrayToken}`,
379
+ "Content-Type": "application/json",
380
+ },
381
+ });
382
+
383
+ return response.data;
384
+ } catch (error) {
385
+ // Enhanced error reporting for Xray API issues
386
+ if (error.response) {
387
+ const status = error.response.status;
388
+ const data = error.response.data;
389
+
390
+ let errorMsg = `Xray API returned ${status} error`;
391
+
392
+ if (typeof data === 'string') {
393
+ errorMsg += `: ${data}`;
394
+ } else if (data?.error) {
395
+ errorMsg += `: ${data.error}`;
396
+ } else if (data?.message) {
397
+ errorMsg += `: ${data.message}`;
398
+ } else if (data) {
399
+ errorMsg += `: ${JSON.stringify(data)}`;
400
+ }
401
+
402
+ logger.error(errorMsg);
403
+
404
+ // Common issues help
405
+ if (status === 400) {
406
+ logger.warn('\n💡 Common causes of 400 errors:');
407
+ logger.warn(' • Missing test annotations in Playwright tests');
408
+ logger.warn(' • Invalid test key format (should be PROJ-123)');
409
+ logger.warn(' • Test execution key not found');
410
+ logger.warn(' • Malformed JSON structure');
411
+ logger.warn(' • Test keys reference non-existent tests\n');
412
+ }
413
+
414
+ throw new Error(errorMsg);
415
+ }
416
+
417
+ throw error;
418
+ }
383
419
  }
384
420
 
385
421
  // ─── Retry wrapper ─────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msalaam/xray-qe-toolkit",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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": {