@msalaam/xray-qe-toolkit 1.5.0 → 1.6.1
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 +76 -20
- package/bin/cli.js +4 -1
- package/commands/createExecution.js +1 -0
- package/commands/importResults.js +2 -0
- package/commands/init.js +2 -0
- package/commands/pushTests.js +6 -7
- package/lib/config.js +7 -0
- package/lib/jsonFile.js +12 -0
- package/lib/playwrightConverter.js +68 -45
- package/lib/testCaseBuilder.js +305 -99
- package/lib/xrayClient.js +119 -18
- package/package.json +2 -1
- package/schema/tests.schema.json +21 -2
- package/templates/README.template.md +132 -23
- package/templates/tests.json +5341 -103
- package/templates/SPEC-DRIVEN-APPROACH.md +0 -372
package/README.md
CHANGED
|
@@ -194,12 +194,14 @@ Optionally create a Test Execution linked to the plan in a single command.
|
|
|
194
194
|
```bash
|
|
195
195
|
npx xqt push
|
|
196
196
|
npx xqt push --plan APIEE-1234
|
|
197
|
+
npx xqt push --bulk # force bulk import regardless of count
|
|
197
198
|
npx xqt push --plan APIEE-1234 --create-execution --execution-env IOP-QA
|
|
198
199
|
```
|
|
199
200
|
|
|
200
201
|
| Flag | Description |
|
|
201
202
|
|---|---|
|
|
202
203
|
| `--plan <key>` | Test Plan key (overrides `.xrayrc testPlanKey`) |
|
|
204
|
+
| `--bulk` | Force bulk Xray REST import regardless of new-test count (useful when count is below `bulkImportThreshold`) |
|
|
203
205
|
| `--create-execution` | Create a Test Execution linked to the Test Plan after pushing |
|
|
204
206
|
| `--execution-env <label>` | Environment label for the created execution (e.g. `IOP-QA`) |
|
|
205
207
|
| `--execution-summary <text>` | Custom summary for the created execution |
|
|
@@ -255,6 +257,7 @@ npx xqt exec --env IOP-QA --plan APIEE-1234 --tests TC-001,TC-002
|
|
|
255
257
|
| `--plan <key>` | Test Plan to link to (overrides `.xrayrc`) |
|
|
256
258
|
| `--tests <ids>` | Comma-separated testIds or JIRA keys (default: all mapped) |
|
|
257
259
|
| `--summary <text>` | Custom execution title |
|
|
260
|
+
| `--fix-version <ver>` | JIRA Fix Version to stamp on the execution (e.g. `2.5.0`) |
|
|
258
261
|
| `--quiet` | Print only the execution key |
|
|
259
262
|
|
|
260
263
|
> `xqt import` creates an execution automatically — this command is only needed when pre-selecting a specific subset of tests.
|
|
@@ -281,12 +284,12 @@ npx xqt import --file test-results/results.json --exec APIEE-9876
|
|
|
281
284
|
# JUnit XML
|
|
282
285
|
npx xqt import --file test-results/results.xml --env IOP-PROD
|
|
283
286
|
|
|
284
|
-
# Full options
|
|
287
|
+
# Full options — with Fix Version and git SHA
|
|
285
288
|
npx xqt import \
|
|
286
289
|
--file test-results/results.json \
|
|
287
290
|
--env IOP-QA \
|
|
288
291
|
--plan APIEE-1234 \
|
|
289
|
-
--version "
|
|
292
|
+
--fix-version "2.5.0" \
|
|
290
293
|
--revision "a1b2c3d4"
|
|
291
294
|
```
|
|
292
295
|
|
|
@@ -296,7 +299,8 @@ npx xqt import \
|
|
|
296
299
|
| `--env <label>` | Environment label (default: `defaultEnvironment` from `.xrayrc`) |
|
|
297
300
|
| `--plan <key>` | Test Plan key (overrides `.xrayrc`; used when no `--exec`) |
|
|
298
301
|
| `--exec <key>` | Import INTO an existing execution (from `xqt exec`) |
|
|
299
|
-
| `--version <ver>` | Fix
|
|
302
|
+
| `--fix-version <ver>` | JIRA Fix Version name to stamp on the execution (e.g. `2.5.0`) |
|
|
303
|
+
| `--version <ver>` | Xray version label (free-form, separate from Fix Version) |
|
|
300
304
|
| `--revision <rev>` | Build number or git SHA |
|
|
301
305
|
| `--summary <text>` | Custom execution summary |
|
|
302
306
|
|
|
@@ -394,23 +398,53 @@ JIRA_EMAIL=your.email@company.com
|
|
|
394
398
|
|
|
395
399
|
```json
|
|
396
400
|
{
|
|
397
|
-
"testsPath":
|
|
398
|
-
"mappingPath":
|
|
399
|
-
"testPlanKey":
|
|
400
|
-
"defaultEnvironment":
|
|
401
|
-
"environments":
|
|
402
|
-
"folderRoot":
|
|
403
|
-
"xrayRegion":
|
|
401
|
+
"testsPath": "tests.json",
|
|
402
|
+
"mappingPath": "xray-mapping.json",
|
|
403
|
+
"testPlanKey": "APIEE-1234",
|
|
404
|
+
"defaultEnvironment": "IOP-DEV",
|
|
405
|
+
"environments": ["IOP-DEV", "IOP-QA", "IOP-PROD"],
|
|
406
|
+
"folderRoot": "/MyService",
|
|
407
|
+
"xrayRegion": "us",
|
|
408
|
+
"bulkImportThreshold": 50,
|
|
409
|
+
"statusMapping": {
|
|
410
|
+
"interrupted": "ABORTED",
|
|
411
|
+
"skipped": "TODO"
|
|
412
|
+
}
|
|
404
413
|
}
|
|
405
414
|
```
|
|
406
415
|
|
|
407
416
|
| Field | Description |
|
|
408
417
|
|---|---|
|
|
409
|
-
| `testPlanKey` | Default Test Plan key for push
|
|
418
|
+
| `testPlanKey` | Default Test Plan key for `push` and `import` |
|
|
410
419
|
| `defaultEnvironment` | Fallback environment when `--env` is not provided |
|
|
411
420
|
| `environments` | Allowed environment labels |
|
|
412
421
|
| `folderRoot` | Base folder path in Xray Test Repository |
|
|
413
|
-
| `xrayRegion` | `us` (default) or `
|
|
422
|
+
| `xrayRegion` | `us` (default), `eu`, or `au` |
|
|
423
|
+
| `bulkImportThreshold` | Min new-test count to use the Xray bulk REST API (default: `50`) — faster for large suites |
|
|
424
|
+
| `statusMapping` | Override default Playwright → Xray status mapping (see below) |
|
|
425
|
+
|
|
426
|
+
#### Playwright status mapping
|
|
427
|
+
|
|
428
|
+
By default the converter maps:
|
|
429
|
+
|
|
430
|
+
| Playwright status | Xray status |
|
|
431
|
+
|---|---|
|
|
432
|
+
| `passed` | `PASSED` |
|
|
433
|
+
| `failed` | `FAILED` |
|
|
434
|
+
| `timedOut` | `FAILED` |
|
|
435
|
+
| `interrupted` | `ABORTED` |
|
|
436
|
+
| `skipped` | `TODO` |
|
|
437
|
+
|
|
438
|
+
Override any value in `.xrayrc`:
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"statusMapping": {
|
|
443
|
+
"skipped": "TODO",
|
|
444
|
+
"interrupted": "FAILED"
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
414
448
|
|
|
415
449
|
---
|
|
416
450
|
|
|
@@ -432,6 +466,7 @@ JIRA_EMAIL=your.email@company.com
|
|
|
432
466
|
"tags": ["smoke", "regression"],
|
|
433
467
|
"folder": "/MyService/HealthCheck/Validation",
|
|
434
468
|
"testSet": "Health Check",
|
|
469
|
+
"preconditions": ["APIEE-50"],
|
|
435
470
|
"requirementKeys": [],
|
|
436
471
|
"xray": {
|
|
437
472
|
"summary": "Service returns 200 OK when healthy",
|
|
@@ -455,7 +490,8 @@ JIRA_EMAIL=your.email@company.com
|
|
|
455
490
|
| `skip` | boolean | — | `true` to exclude from `push-tests` |
|
|
456
491
|
| `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
492
|
| `folder` | string | — | Xray repository folder path — must start with `/` |
|
|
458
|
-
| `testSet` | string | — | Test Set name in Jira —
|
|
493
|
+
| `testSet` | string **or** string[] | — | Test Set name(s) in Jira — a single string or an array to assign the test to multiple Test Sets |
|
|
494
|
+
| `preconditions` | string[] | — | JIRA keys of Xray Precondition issues to link to this test (e.g. `["APIEE-50"]`) |
|
|
459
495
|
| `requirementKeys` | string[] | — | JIRA keys this test covers (creates traceability links) |
|
|
460
496
|
| `xray.summary` | string | Yes | Test case title (JIRA issue summary) |
|
|
461
497
|
| `xray.description` | string | — | Detailed description |
|
|
@@ -472,17 +508,17 @@ JIRA_EMAIL=your.email@company.com
|
|
|
472
508
|
|
|
473
509
|
Tests live in **Test Sets** in Jira. Each Test Set groups related tests by feature or area — they persist across every sprint.
|
|
474
510
|
|
|
475
|
-
Set the `testSet` field on each test in `tests.json
|
|
511
|
+
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
512
|
|
|
477
513
|
```json
|
|
478
514
|
{ "test_id": "TC-001", "testSet": "Client Lookup", ... }
|
|
479
515
|
{ "test_id": "TC-002", "testSet": "Client Lookup", ... }
|
|
480
|
-
{ "test_id": "TC-003", "testSet": "Health Check",
|
|
516
|
+
{ "test_id": "TC-003", "testSet": ["Health Check", "Smoke"], ... }
|
|
481
517
|
```
|
|
482
518
|
|
|
483
519
|
When you run `xqt push`:
|
|
484
520
|
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
|
|
521
|
+
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
522
|
3. Tests are added to their Test Set (idempotent — Xray ignores tests already in the set)
|
|
487
523
|
4. Test Set → JIRA key mappings (e.g. `"Client Lookup" → APIEE-99`) are stored in `xray-mapping.json` under `_testSets`
|
|
488
524
|
|
|
@@ -509,6 +545,8 @@ A Test Plan scopes testing work for a specific sprint or release.
|
|
|
509
545
|
- Key stored in `.xrayrc` (`testPlanKey`)
|
|
510
546
|
- `xqt import` links executions to the active plan
|
|
511
547
|
|
|
548
|
+
`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.
|
|
549
|
+
|
|
512
550
|
### Test Executions (ephemeral per run)
|
|
513
551
|
|
|
514
552
|
A Test Execution represents a single CI run.
|
|
@@ -582,7 +620,7 @@ reporter: [
|
|
|
582
620
|
|
|
583
621
|
### Test steps → Xray step results
|
|
584
622
|
|
|
585
|
-
Steps created with `test.step()` are mapped to Xray step results automatically
|
|
623
|
+
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
624
|
|
|
587
625
|
```typescript
|
|
588
626
|
test('Create and retrieve user', async ({ request }) => {
|
|
@@ -636,7 +674,9 @@ Generate a template: `npx xqt pipeline`
|
|
|
636
674
|
- script: |
|
|
637
675
|
npx xqt import \
|
|
638
676
|
--file test-results/results.json \
|
|
639
|
-
--env $(XQT_ENV)
|
|
677
|
+
--env $(XQT_ENV) \
|
|
678
|
+
--fix-version $(BUILD_BUILDNUMBER) \
|
|
679
|
+
--revision $(Build.SourceVersion)
|
|
640
680
|
displayName: Import results to Xray
|
|
641
681
|
env:
|
|
642
682
|
XRAY_ID: $(XRAY_ID)
|
|
@@ -658,6 +698,7 @@ Generate a template: `npx xqt pipeline`
|
|
|
658
698
|
| `JIRA_API_TOKEN` | JIRA API token |
|
|
659
699
|
| `JIRA_EMAIL` | JIRA user email |
|
|
660
700
|
| `XQT_ENV` | Environment label (`IOP-DEV` / `IOP-QA` / `IOP-PROD`) |
|
|
701
|
+
| `XQT_BULK_THRESHOLD` | Override bulk-import threshold (default: `50`) |
|
|
661
702
|
|
|
662
703
|
### Pre-create execution in pipeline (Mode B)
|
|
663
704
|
|
|
@@ -701,7 +742,7 @@ const { key } = await createTestExecution(cfg, token, {
|
|
|
701
742
|
});
|
|
702
743
|
```
|
|
703
744
|
|
|
704
|
-
Key exports: `authenticate`, `createIssue`, `updateIssue`, `getIssue`, `getTest`, `getTests`, `getTestPlan`, `createTestPlan`, `addTestsToTestPlan`, `addTestsToTestExecution`, `createTestExecution`, `importResultsMultipart`, `importTestsBulk`, `syncTestPlan`, `syncFolders`, `buildAndPush`, `loadMapping`, `saveMapping`, `loadConfig`, `validateConfig`.
|
|
745
|
+
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
746
|
|
|
706
747
|
---
|
|
707
748
|
|
|
@@ -752,7 +793,22 @@ test.info().annotations.push({ type: 'xray', description: 'APIEE-1234' });
|
|
|
752
793
|
**Validate fails in CI**
|
|
753
794
|
Fix schema errors before pushing. Common causes: missing `testId`, invalid `priority`, malformed `folder` path (must start with `/`).
|
|
754
795
|
|
|
755
|
-
|
|
796
|
+
**`xqt push` — Test Repository folder errors**
|
|
797
|
+
```
|
|
798
|
+
GraphQL errors: User doesn't have permissions to view/edit test repository for project
|
|
799
|
+
```
|
|
800
|
+
The JIRA user in `JIRA_EMAIL` needs **Test Repository** write access in Xray. Fix in Jira:
|
|
801
|
+
> **Project Settings → Xray → Test Repository** → add your role or user to the allowed list.
|
|
802
|
+
Folder sync errors are non-fatal — tests are still created/updated correctly.
|
|
803
|
+
|
|
804
|
+
**Test Set creation returns HTTP 400**
|
|
805
|
+
Ensure the Xray **Test Set** issue type exists in your JIRA project and that the user has permission to create it.
|
|
806
|
+
> **Project Settings → Issue Types** — "Test Set" must be present.
|
|
807
|
+
|
|
808
|
+
**`updateTestType` "not found" on new tests**
|
|
809
|
+
Xray Cloud is eventually consistent — a newly created issue may not be immediately visible to the GraphQL API. `xqt push` automatically retries with backoff. If failures persist, rerun `xqt push` (existing tests update, not duplicate).
|
|
810
|
+
|
|
811
|
+
|
|
756
812
|
All previous multi-word commands (`push-tests`, `pull-tests`, `create-plan`, `create-execution`, `import-results`, `sync-folders`, `gen-pipeline`, `mcp-server`) remain valid as aliases.
|
|
757
813
|
|
|
758
814
|
**EU region**
|
package/bin/cli.js
CHANGED
|
@@ -67,6 +67,7 @@ program
|
|
|
67
67
|
"Create/update tests in Xray Cloud, sync to Test Plan, sync folder structure"
|
|
68
68
|
)
|
|
69
69
|
.option("--plan <key>", "Test Plan key (overrides .xrayrc testPlanKey)")
|
|
70
|
+
.option("--bulk", "Force bulk Xray REST import regardless of new-test count")
|
|
70
71
|
.option("--create-execution", "Create a Test Execution linked to the Test Plan after pushing")
|
|
71
72
|
.option("--execution-env <label>", "Environment label for created execution (e.g. IOP-QA)")
|
|
72
73
|
.option("--execution-summary <text>", "Custom summary for created execution")
|
|
@@ -100,6 +101,7 @@ program
|
|
|
100
101
|
.option("--tests <ids>", "Comma-separated testIds or JIRA keys to include (default: all mapped tests)")
|
|
101
102
|
.option("--summary <text>", "Custom Test Execution summary")
|
|
102
103
|
.option("--description <text>", "Custom description")
|
|
104
|
+
.option("--fix-version <version>", "JIRA Fix Version to stamp on the execution (e.g. '2.5.0')")
|
|
103
105
|
.option("--quiet", "Print ONLY the execution key (for CI variable capture)")
|
|
104
106
|
.action(async (opts, cmd) => {
|
|
105
107
|
const mod = await import("../commands/createExecution.js");
|
|
@@ -117,7 +119,8 @@ program
|
|
|
117
119
|
.option("--plan <key>", "Test Plan key to associate execution with (overrides .xrayrc)")
|
|
118
120
|
.option("--exec <key>", "Import INTO an existing Test Execution key (from xqt exec)")
|
|
119
121
|
.option("--version <version>", "Fix version / release version")
|
|
120
|
-
.option("--revision <revision>", "Revision / build number")
|
|
122
|
+
.option("--revision <revision>", "Revision / build number (e.g. git SHA)")
|
|
123
|
+
.option("--fix-version <version>", "JIRA Fix Version name to stamp on the Test Execution (e.g. '2.5.0')")
|
|
121
124
|
.option("--summary <text>", "Custom Test Execution summary")
|
|
122
125
|
.option("--description <text>", "Custom Test Execution description")
|
|
123
126
|
.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)) {
|
|
@@ -85,7 +84,7 @@ export default async function pushTests(opts = {}) {
|
|
|
85
84
|
|
|
86
85
|
// ── Build & push tests ─────────────────────────────────────────────────────
|
|
87
86
|
logger.send("Pushing tests to Xray...");
|
|
88
|
-
const result = await buildAndPush(cfg, tests, mapping, xrayToken);
|
|
87
|
+
const result = await buildAndPush(cfg, tests, mapping, xrayToken, { forceBulk: !!opts.bulk });
|
|
89
88
|
|
|
90
89
|
logger.blank();
|
|
91
90
|
logger.success(
|
|
@@ -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) {
|