@msalaam/xray-qe-toolkit 1.5.0 → 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 +58 -19
- package/bin/cli.js +3 -1
- package/commands/createExecution.js +1 -0
- package/commands/importResults.js +2 -0
- package/commands/init.js +2 -0
- package/commands/pushTests.js +5 -6
- package/lib/config.js +7 -0
- package/lib/jsonFile.js +12 -0
- package/lib/playwrightConverter.js +68 -45
- package/lib/testCaseBuilder.js +297 -96
- package/lib/xrayClient.js +94 -11
- package/package.json +2 -1
- package/schema/tests.schema.json +21 -2
- package/templates/README.template.md +80 -23
- package/templates/tests.json +5193 -98
- package/templates/SPEC-DRIVEN-APPROACH.md +0 -372
package/README.md
CHANGED
|
@@ -255,6 +255,7 @@ npx xqt exec --env IOP-QA --plan APIEE-1234 --tests TC-001,TC-002
|
|
|
255
255
|
| `--plan <key>` | Test Plan to link to (overrides `.xrayrc`) |
|
|
256
256
|
| `--tests <ids>` | Comma-separated testIds or JIRA keys (default: all mapped) |
|
|
257
257
|
| `--summary <text>` | Custom execution title |
|
|
258
|
+
| `--fix-version <ver>` | JIRA Fix Version to stamp on the execution (e.g. `2.5.0`) |
|
|
258
259
|
| `--quiet` | Print only the execution key |
|
|
259
260
|
|
|
260
261
|
> `xqt import` creates an execution automatically — this command is only needed when pre-selecting a specific subset of tests.
|
|
@@ -281,12 +282,12 @@ npx xqt import --file test-results/results.json --exec APIEE-9876
|
|
|
281
282
|
# JUnit XML
|
|
282
283
|
npx xqt import --file test-results/results.xml --env IOP-PROD
|
|
283
284
|
|
|
284
|
-
# Full options
|
|
285
|
+
# Full options — with Fix Version and git SHA
|
|
285
286
|
npx xqt import \
|
|
286
287
|
--file test-results/results.json \
|
|
287
288
|
--env IOP-QA \
|
|
288
289
|
--plan APIEE-1234 \
|
|
289
|
-
--version "
|
|
290
|
+
--fix-version "2.5.0" \
|
|
290
291
|
--revision "a1b2c3d4"
|
|
291
292
|
```
|
|
292
293
|
|
|
@@ -296,7 +297,8 @@ npx xqt import \
|
|
|
296
297
|
| `--env <label>` | Environment label (default: `defaultEnvironment` from `.xrayrc`) |
|
|
297
298
|
| `--plan <key>` | Test Plan key (overrides `.xrayrc`; used when no `--exec`) |
|
|
298
299
|
| `--exec <key>` | Import INTO an existing execution (from `xqt exec`) |
|
|
299
|
-
| `--version <ver>` | Fix
|
|
300
|
+
| `--fix-version <ver>` | JIRA Fix Version name to stamp on the execution (e.g. `2.5.0`) |
|
|
301
|
+
| `--version <ver>` | Xray version label (free-form, separate from Fix Version) |
|
|
300
302
|
| `--revision <rev>` | Build number or git SHA |
|
|
301
303
|
| `--summary <text>` | Custom execution summary |
|
|
302
304
|
|
|
@@ -394,23 +396,53 @@ JIRA_EMAIL=your.email@company.com
|
|
|
394
396
|
|
|
395
397
|
```json
|
|
396
398
|
{
|
|
397
|
-
"testsPath":
|
|
398
|
-
"mappingPath":
|
|
399
|
-
"testPlanKey":
|
|
400
|
-
"defaultEnvironment":
|
|
401
|
-
"environments":
|
|
402
|
-
"folderRoot":
|
|
403
|
-
"xrayRegion":
|
|
399
|
+
"testsPath": "tests.json",
|
|
400
|
+
"mappingPath": "xray-mapping.json",
|
|
401
|
+
"testPlanKey": "APIEE-1234",
|
|
402
|
+
"defaultEnvironment": "IOP-DEV",
|
|
403
|
+
"environments": ["IOP-DEV", "IOP-QA", "IOP-PROD"],
|
|
404
|
+
"folderRoot": "/MyService",
|
|
405
|
+
"xrayRegion": "us",
|
|
406
|
+
"bulkImportThreshold": 50,
|
|
407
|
+
"statusMapping": {
|
|
408
|
+
"interrupted": "ABORTED",
|
|
409
|
+
"skipped": "TODO"
|
|
410
|
+
}
|
|
404
411
|
}
|
|
405
412
|
```
|
|
406
413
|
|
|
407
414
|
| Field | Description |
|
|
408
415
|
|---|---|
|
|
409
|
-
| `testPlanKey` | Default Test Plan key for push
|
|
416
|
+
| `testPlanKey` | Default Test Plan key for `push` and `import` |
|
|
410
417
|
| `defaultEnvironment` | Fallback environment when `--env` is not provided |
|
|
411
418
|
| `environments` | Allowed environment labels |
|
|
412
419
|
| `folderRoot` | Base folder path in Xray Test Repository |
|
|
413
|
-
| `xrayRegion` | `us` (default) or `
|
|
420
|
+
| `xrayRegion` | `us` (default), `eu`, or `au` |
|
|
421
|
+
| `bulkImportThreshold` | Min new-test count to use the Xray bulk REST API (default: `50`) — faster for large suites |
|
|
422
|
+
| `statusMapping` | Override default Playwright → Xray status mapping (see below) |
|
|
423
|
+
|
|
424
|
+
#### Playwright status mapping
|
|
425
|
+
|
|
426
|
+
By default the converter maps:
|
|
427
|
+
|
|
428
|
+
| Playwright status | Xray status |
|
|
429
|
+
|---|---|
|
|
430
|
+
| `passed` | `PASSED` |
|
|
431
|
+
| `failed` | `FAILED` |
|
|
432
|
+
| `timedOut` | `FAILED` |
|
|
433
|
+
| `interrupted` | `ABORTED` |
|
|
434
|
+
| `skipped` | `TODO` |
|
|
435
|
+
|
|
436
|
+
Override any value in `.xrayrc`:
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
{
|
|
440
|
+
"statusMapping": {
|
|
441
|
+
"skipped": "TODO",
|
|
442
|
+
"interrupted": "FAILED"
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
414
446
|
|
|
415
447
|
---
|
|
416
448
|
|
|
@@ -432,6 +464,7 @@ JIRA_EMAIL=your.email@company.com
|
|
|
432
464
|
"tags": ["smoke", "regression"],
|
|
433
465
|
"folder": "/MyService/HealthCheck/Validation",
|
|
434
466
|
"testSet": "Health Check",
|
|
467
|
+
"preconditions": ["APIEE-50"],
|
|
435
468
|
"requirementKeys": [],
|
|
436
469
|
"xray": {
|
|
437
470
|
"summary": "Service returns 200 OK when healthy",
|
|
@@ -455,7 +488,8 @@ JIRA_EMAIL=your.email@company.com
|
|
|
455
488
|
| `skip` | boolean | — | `true` to exclude from `push-tests` |
|
|
456
489
|
| `tags` | string[] | — | QE tags for categorisation and filtering. Allowed: `regression`, `smoke`, `edge`, `critical`, `integration`, `e2e`, `security`, `performance`, `contract`, `functional`, `negative`, `positive`, `boundary`, `acceptance`, `sanity`, `data-driven`, `exploratory`, `accessibility` |
|
|
457
490
|
| `folder` | string | — | Xray repository folder path — must start with `/` |
|
|
458
|
-
| `testSet` | string | — | Test Set name in Jira —
|
|
491
|
+
| `testSet` | string **or** string[] | — | Test Set name(s) in Jira — a single string or an array to assign the test to multiple Test Sets |
|
|
492
|
+
| `preconditions` | string[] | — | JIRA keys of Xray Precondition issues to link to this test (e.g. `["APIEE-50"]`) |
|
|
459
493
|
| `requirementKeys` | string[] | — | JIRA keys this test covers (creates traceability links) |
|
|
460
494
|
| `xray.summary` | string | Yes | Test case title (JIRA issue summary) |
|
|
461
495
|
| `xray.description` | string | — | Detailed description |
|
|
@@ -472,17 +506,17 @@ JIRA_EMAIL=your.email@company.com
|
|
|
472
506
|
|
|
473
507
|
Tests live in **Test Sets** in Jira. Each Test Set groups related tests by feature or area — they persist across every sprint.
|
|
474
508
|
|
|
475
|
-
Set the `testSet` field on each test in `tests.json
|
|
509
|
+
Set the `testSet` field on each test in `tests.json` — use a string or an array of strings to assign the test to multiple sets:
|
|
476
510
|
|
|
477
511
|
```json
|
|
478
512
|
{ "test_id": "TC-001", "testSet": "Client Lookup", ... }
|
|
479
513
|
{ "test_id": "TC-002", "testSet": "Client Lookup", ... }
|
|
480
|
-
{ "test_id": "TC-003", "testSet": "Health Check",
|
|
514
|
+
{ "test_id": "TC-003", "testSet": ["Health Check", "Smoke"], ... }
|
|
481
515
|
```
|
|
482
516
|
|
|
483
517
|
When you run `xqt push`:
|
|
484
518
|
1. Tests are created/updated in Xray
|
|
485
|
-
2. For each unique `testSet` value, the Test Set JIRA issue is **created automatically** if it doesn't exist yet
|
|
519
|
+
2. For each unique `testSet` value, the Test Set JIRA issue is **created automatically** if it doesn't exist yet (safe to run on a fresh clone — existing sets are found by name, not duplicated)
|
|
486
520
|
3. Tests are added to their Test Set (idempotent — Xray ignores tests already in the set)
|
|
487
521
|
4. Test Set → JIRA key mappings (e.g. `"Client Lookup" → APIEE-99`) are stored in `xray-mapping.json` under `_testSets`
|
|
488
522
|
|
|
@@ -509,6 +543,8 @@ A Test Plan scopes testing work for a specific sprint or release.
|
|
|
509
543
|
- Key stored in `.xrayrc` (`testPlanKey`)
|
|
510
544
|
- `xqt import` links executions to the active plan
|
|
511
545
|
|
|
546
|
+
`xqt push` performs a **bi-directional sync** with the Test Plan: new tests are added **and** tests removed from `tests.json` are removed from the plan. Use `--plan <key>` to override the key.
|
|
547
|
+
|
|
512
548
|
### Test Executions (ephemeral per run)
|
|
513
549
|
|
|
514
550
|
A Test Execution represents a single CI run.
|
|
@@ -582,7 +618,7 @@ reporter: [
|
|
|
582
618
|
|
|
583
619
|
### Test steps → Xray step results
|
|
584
620
|
|
|
585
|
-
Steps created with `test.step()` are mapped to Xray step results automatically
|
|
621
|
+
Steps created with `test.step()` are mapped to Xray step results automatically. Screenshots captured on a **failing step** are attached directly to that step's evidence in Xray — giving per-step failure screenshots in the Xray UI. Traces and other attachments are attached at the test level.
|
|
586
622
|
|
|
587
623
|
```typescript
|
|
588
624
|
test('Create and retrieve user', async ({ request }) => {
|
|
@@ -636,7 +672,9 @@ Generate a template: `npx xqt pipeline`
|
|
|
636
672
|
- script: |
|
|
637
673
|
npx xqt import \
|
|
638
674
|
--file test-results/results.json \
|
|
639
|
-
--env $(XQT_ENV)
|
|
675
|
+
--env $(XQT_ENV) \
|
|
676
|
+
--fix-version $(BUILD_BUILDNUMBER) \
|
|
677
|
+
--revision $(Build.SourceVersion)
|
|
640
678
|
displayName: Import results to Xray
|
|
641
679
|
env:
|
|
642
680
|
XRAY_ID: $(XRAY_ID)
|
|
@@ -658,6 +696,7 @@ Generate a template: `npx xqt pipeline`
|
|
|
658
696
|
| `JIRA_API_TOKEN` | JIRA API token |
|
|
659
697
|
| `JIRA_EMAIL` | JIRA user email |
|
|
660
698
|
| `XQT_ENV` | Environment label (`IOP-DEV` / `IOP-QA` / `IOP-PROD`) |
|
|
699
|
+
| `XQT_BULK_THRESHOLD` | Override bulk-import threshold (default: `50`) |
|
|
661
700
|
|
|
662
701
|
### Pre-create execution in pipeline (Mode B)
|
|
663
702
|
|
|
@@ -701,7 +740,7 @@ const { key } = await createTestExecution(cfg, token, {
|
|
|
701
740
|
});
|
|
702
741
|
```
|
|
703
742
|
|
|
704
|
-
Key exports: `authenticate`, `createIssue`, `updateIssue`, `getIssue`, `getTest`, `getTests`, `getTestPlan`, `createTestPlan`, `addTestsToTestPlan`, `addTestsToTestExecution`, `createTestExecution`, `importResultsMultipart`, `importTestsBulk`, `syncTestPlan`, `syncFolders`, `buildAndPush`, `loadMapping`, `saveMapping`, `loadConfig`, `validateConfig`.
|
|
743
|
+
Key exports: `authenticate`, `createIssue`, `updateIssue`, `getIssue`, `getTest`, `getTests`, `getTestPlan`, `createTestPlan`, `addTestsToTestPlan`, `removeTestsFromTestPlan`, `addTestsToTestExecution`, `createTestExecution`, `importResultsMultipart`, `importTestsBulk`, `waitForImportJob`, `searchIssues`, `getTestSets`, `addPreconditionsToTest`, `syncTestPlan`, `syncFolders`, `buildAndPush`, `loadMapping`, `saveMapping`, `loadConfig`, `validateConfig`.
|
|
705
744
|
|
|
706
745
|
---
|
|
707
746
|
|
package/bin/cli.js
CHANGED
|
@@ -100,6 +100,7 @@ program
|
|
|
100
100
|
.option("--tests <ids>", "Comma-separated testIds or JIRA keys to include (default: all mapped tests)")
|
|
101
101
|
.option("--summary <text>", "Custom Test Execution summary")
|
|
102
102
|
.option("--description <text>", "Custom description")
|
|
103
|
+
.option("--fix-version <version>", "JIRA Fix Version to stamp on the execution (e.g. '2.5.0')")
|
|
103
104
|
.option("--quiet", "Print ONLY the execution key (for CI variable capture)")
|
|
104
105
|
.action(async (opts, cmd) => {
|
|
105
106
|
const mod = await import("../commands/createExecution.js");
|
|
@@ -117,7 +118,8 @@ program
|
|
|
117
118
|
.option("--plan <key>", "Test Plan key to associate execution with (overrides .xrayrc)")
|
|
118
119
|
.option("--exec <key>", "Import INTO an existing Test Execution key (from xqt exec)")
|
|
119
120
|
.option("--version <version>", "Fix version / release version")
|
|
120
|
-
.option("--revision <revision>", "Revision / build number")
|
|
121
|
+
.option("--revision <revision>", "Revision / build number (e.g. git SHA)")
|
|
122
|
+
.option("--fix-version <version>", "JIRA Fix Version name to stamp on the Test Execution (e.g. '2.5.0')")
|
|
121
123
|
.option("--summary <text>", "Custom Test Execution summary")
|
|
122
124
|
.option("--description <text>", "Custom Test Execution description")
|
|
123
125
|
.action(async (opts, cmd) => {
|
|
@@ -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
|
@@ -19,6 +19,7 @@ import fs from "node:fs";
|
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import logger, { setVerbose } from "../lib/logger.js";
|
|
21
21
|
import { loadConfig, validateConfig } from "../lib/config.js";
|
|
22
|
+
import { readJsonFile } from "../lib/jsonFile.js";
|
|
22
23
|
import { authenticate, getIssue, createTestExecution } from "../lib/xrayClient.js";
|
|
23
24
|
import {
|
|
24
25
|
buildAndPush,
|
|
@@ -44,7 +45,7 @@ export default async function pushTests(opts = {}) {
|
|
|
44
45
|
process.exit(1);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const testsConfig =
|
|
48
|
+
const testsConfig = readJsonFile(cfg.testsPath);
|
|
48
49
|
const allTests = testsConfig.tests || [];
|
|
49
50
|
const tests = allTests.filter((t) => !t.skip);
|
|
50
51
|
const skippedCount = allTests.length - tests.length;
|
|
@@ -61,9 +62,7 @@ export default async function pushTests(opts = {}) {
|
|
|
61
62
|
try {
|
|
62
63
|
const require = createRequire(import.meta.url);
|
|
63
64
|
const Ajv = require("ajv");
|
|
64
|
-
const schema =
|
|
65
|
-
fs.readFileSync(new URL("../schema/tests.schema.json", import.meta.url), "utf8")
|
|
66
|
-
);
|
|
65
|
+
const schema = readJsonFile(new URL("../schema/tests.schema.json", import.meta.url));
|
|
67
66
|
const ajv = new Ajv({ allErrors: true });
|
|
68
67
|
const validate = ajv.compile(schema);
|
|
69
68
|
if (!validate(testsConfig)) {
|
|
@@ -103,11 +102,11 @@ export default async function pushTests(opts = {}) {
|
|
|
103
102
|
logger.save(`Mapping saved to ${cfg.mappingPath}`);
|
|
104
103
|
|
|
105
104
|
// ── Sync Test Sets ──────────────────────────────────────────────────
|
|
106
|
-
const testsWithSets = tests.filter((t) => t.testSet);
|
|
105
|
+
const testsWithSets = tests.filter((t) => t.testSet && (Array.isArray(t.testSet) ? t.testSet.length > 0 : true));
|
|
107
106
|
|
|
108
107
|
if (testsWithSets.length > 0) {
|
|
109
108
|
logger.blank();
|
|
110
|
-
const uniqueSets = [...new Set(testsWithSets.
|
|
109
|
+
const uniqueSets = [...new Set(testsWithSets.flatMap((t) => Array.isArray(t.testSet) ? t.testSet : [t.testSet]))];
|
|
111
110
|
logger.send(`Syncing ${uniqueSets.length} Test Set(s) (${testsWithSets.length} test(s))...`);
|
|
112
111
|
try {
|
|
113
112
|
const setResult = await syncTestSets(cfg, xrayToken, tests, mapping, cfg.mappingPath);
|
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) {
|