@msalaam/xray-qe-toolkit 1.4.1 → 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/lib/xrayClient.js CHANGED
@@ -90,6 +90,9 @@ export async function createIssue(cfg, issueType, fields) {
90
90
  issuetype: { name: issueType },
91
91
  ...(fields.priority ? { priority: { name: fields.priority } } : {}),
92
92
  ...(fields.labels && fields.labels.length > 0 ? { labels: fields.labels } : {}),
93
+ ...(fields.fixVersions && fields.fixVersions.length > 0
94
+ ? { fixVersions: fields.fixVersions.map((v) => ({ name: v })) }
95
+ : {}),
93
96
  },
94
97
  };
95
98
 
@@ -219,19 +222,22 @@ export async function getTests(cfg, xrayToken, opts = {}) {
219
222
  }
220
223
 
221
224
  /**
222
- * Get a Test Plan by issue ID (includes its tests).
225
+ * Get a Test Plan by issue ID (includes its tests, paginated).
223
226
  * @param {object} cfg
224
227
  * @param {string} xrayToken
225
228
  * @param {string} issueId
229
+ * @param {object} [opts] { limit?: number, start?: number }
226
230
  * @returns {Promise<object>}
227
231
  */
228
- export async function getTestPlan(cfg, xrayToken, issueId) {
232
+ export async function getTestPlan(cfg, xrayToken, issueId, opts = {}) {
233
+ const limit = opts.limit ?? 100;
234
+ const start = opts.start ?? 0;
229
235
  const data = await graphql(cfg, xrayToken, `
230
- query ($issueId: String!) {
236
+ query ($issueId: String!, $limit: Int!, $start: Int) {
231
237
  getTestPlan(issueId: $issueId) {
232
238
  issueId
233
239
  projectId
234
- tests(limit: 100) {
240
+ tests(limit: $limit, start: $start) {
235
241
  total
236
242
  results {
237
243
  issueId
@@ -246,7 +252,7 @@ export async function getTestPlan(cfg, xrayToken, issueId) {
246
252
  }
247
253
  }
248
254
  }
249
- `, { issueId: String(issueId) });
255
+ `, { issueId: String(issueId), limit, start });
250
256
  return data.getTestPlan;
251
257
  }
252
258
 
@@ -373,8 +379,58 @@ export async function getProjectSettings(cfg, xrayToken, projectId) {
373
379
  return data.getProjectSettings;
374
380
  }
375
381
 
376
- // ─── Xray GraphQLTest mutations ────────────────────────────────────────────
382
+ // ─── JIRA RESTIssue search ──────────────────────────────────────────────────
383
+
384
+ /**
385
+ * Search JIRA issues via JQL.
386
+ * @param {object} cfg
387
+ * @param {string} jql
388
+ * @param {string[]} [fields] Fields to return (default: summary, id, key)
389
+ * @param {number} [maxResults]
390
+ * @returns {Promise<object[]>} Array of issue objects
391
+ */
392
+ export async function searchIssues(cfg, jql, fields = ["summary", "id", "key"], maxResults = 10) {
393
+ const response = await axios.get(
394
+ `${cfg.jiraUrl}/rest/api/3/search`,
395
+ {
396
+ httpsAgent,
397
+ headers: jiraHeaders(cfg),
398
+ params: { jql, fields: fields.join(","), maxResults },
399
+ }
400
+ );
401
+ return response.data.issues || [];
402
+ }
403
+
404
+ // ─── Xray GraphQL — Test Set queries ──────────────────────────────────────────
377
405
 
406
+ /**
407
+ * Get Test Sets in a project (paginated).
408
+ * @param {object} cfg
409
+ * @param {string} xrayToken
410
+ * @param {object} [opts] { projectId, jql, limit, start }
411
+ * @returns {Promise<{total, results}>}
412
+ */
413
+ export async function getTestSets(cfg, xrayToken, opts = {}) {
414
+ const data = await graphql(cfg, xrayToken, `
415
+ query ($projectId: String, $jql: String, $limit: Int!, $start: Int) {
416
+ getTestSets(projectId: $projectId, jql: $jql, limit: $limit, start: $start) {
417
+ total
418
+ results {
419
+ issueId
420
+ projectId
421
+ }
422
+ }
423
+ }
424
+ `, {
425
+ projectId: opts.projectId || null,
426
+ jql: opts.jql || null,
427
+ limit: Math.min(opts.limit || 100, 100),
428
+ start: opts.start || 0,
429
+ });
430
+ return data.getTestSets;
431
+ }
432
+
433
+ // ─── Xray GraphQL — Test mutations ────────────────────────────────────────────
378
434
  /**
379
435
  * Set a test issue's type via Xray GraphQL.
380
436
  * @param {object} cfg
@@ -495,6 +551,31 @@ export async function updateTestFolder(cfg, xrayToken, issueId, folderPath) {
495
551
  `, { issueId: String(issueId), folderPath });
496
552
  }
497
553
 
554
+ // ─── Xray GraphQL — Precondition mutations ────────────────────────────────────
555
+
556
+ /**
557
+ * Link precondition issues to a test.
558
+ * @param {object} cfg
559
+ * @param {string} xrayToken
560
+ * @param {string} testIssueId Numeric Xray issue ID of the test
561
+ * @param {string[]} preconditionIssueIds Numeric Xray issue IDs of preconditions
562
+ * @returns {Promise<{addedPreconditions: string[], warnings: string[]}>}
563
+ */
564
+ export async function addPreconditionsToTest(cfg, xrayToken, testIssueId, preconditionIssueIds) {
565
+ const data = await graphql(cfg, xrayToken, `
566
+ mutation ($issueId: String!, $preconditionIssueIds: [String]!) {
567
+ addPreconditionsToTest(issueId: $issueId, preconditionIssueIds: $preconditionIssueIds) {
568
+ addedPreconditions
569
+ warnings
570
+ }
571
+ }
572
+ `, {
573
+ issueId: String(testIssueId),
574
+ preconditionIssueIds: preconditionIssueIds.map(String),
575
+ });
576
+ return data.addPreconditionsToTest;
577
+ }
578
+
498
579
  // ─── Xray GraphQL — Test Plan mutations ───────────────────────────────────────
499
580
 
500
581
  /**
@@ -566,6 +647,80 @@ export async function removeTestsFromTestPlan(cfg, xrayToken, planIssueId, testI
566
647
  });
567
648
  }
568
649
 
650
+ /**
651
+ * Remove tests from a Test Plan.
652
+ * @param {object} cfg
653
+ * @param {string} xrayToken
654
+ * @param {string} planIssueId
655
+ * @param {string[]} testIssueIds
656
+ */
657
+ export async function removeTestsFromTestPlanById(cfg, xrayToken, planIssueId, testIssueIds) {
658
+ await graphql(cfg, xrayToken, `
659
+ mutation ($issueId: String!, $testIssueIds: [String]!) {
660
+ removeTestsFromTestPlan(issueId: $issueId, testIssueIds: $testIssueIds) {
661
+ warnings
662
+ }
663
+ }
664
+ `, {
665
+ issueId: String(planIssueId),
666
+ testIssueIds: testIssueIds.map(String),
667
+ });
668
+ }
669
+
670
+ // ─── Xray GraphQL — Test Set mutations ────────────────────────────────────────
671
+
672
+ /**
673
+ * Create a new Test Set issue via Xray GraphQL.
674
+ *
675
+ * Test Sets are persistent groupings of tests by feature/area.
676
+ * They live across sprints — Test Plans reference Test Sets per sprint.
677
+ *
678
+ * @param {object} cfg
679
+ * @param {string} xrayToken
680
+ * @param {object} opts { summary, projectId? }
681
+ * @returns {Promise<{testSet: {issueId: string, projectId: string}, warnings: string[]}>}
682
+ */
683
+ export async function createTestSet(cfg, xrayToken, opts) {
684
+ const data = await graphql(cfg, xrayToken, `
685
+ mutation ($projectId: String!, $summary: String!) {
686
+ createTestSet(projectId: $projectId, summary: $summary) {
687
+ testSet {
688
+ issueId
689
+ projectId
690
+ }
691
+ warnings
692
+ }
693
+ }
694
+ `, {
695
+ projectId: opts.projectId || cfg.jiraProjectKey,
696
+ summary: opts.summary,
697
+ });
698
+ return data.createTestSet;
699
+ }
700
+
701
+ /**
702
+ * Add tests to an existing Test Set.
703
+ * @param {object} cfg
704
+ * @param {string} xrayToken
705
+ * @param {string} testSetIssueId Numeric Xray issue ID of the Test Set
706
+ * @param {string[]} testIssueIds Numeric Xray issue IDs of tests to add
707
+ * @returns {Promise<{addedTests: string[], warnings: string[]}>}
708
+ */
709
+ export async function addTestsToTestSet(cfg, xrayToken, testSetIssueId, testIssueIds) {
710
+ const data = await graphql(cfg, xrayToken, `
711
+ mutation ($issueId: String!, $testIssueIds: [String]!) {
712
+ addTestsToTestSet(issueId: $issueId, testIssueIds: $testIssueIds) {
713
+ addedTests
714
+ warnings
715
+ }
716
+ }
717
+ `, {
718
+ issueId: String(testSetIssueId),
719
+ testIssueIds: testIssueIds.map(String),
720
+ });
721
+ return data.addTestsToTestSet;
722
+ }
723
+
569
724
  // ─── Xray GraphQL — Folder mutations ──────────────────────────────────────────
570
725
 
571
726
  /**
@@ -909,11 +1064,6 @@ export async function withRetry(fn, opts = {}) {
909
1064
  let lastError;
910
1065
  for (let attempt = 0; attempt < maxRetries; attempt++) {
911
1066
  try {
912
- const delay = baseDelay * Math.pow(2, attempt);
913
- if (attempt > 0) {
914
- logger.wait(`Retry ${attempt}/${maxRetries - 1} after ${delay}ms...`);
915
- }
916
- await sleep(delay);
917
1067
  return await fn();
918
1068
  } catch (err) {
919
1069
  lastError = err;
@@ -928,6 +1078,11 @@ export async function withRetry(fn, opts = {}) {
928
1078
  }
929
1079
  throw err;
930
1080
  }
1081
+ if (attempt < maxRetries - 1) {
1082
+ const delay = baseDelay * Math.pow(2, attempt);
1083
+ logger.wait(`Retry ${attempt + 1}/${maxRetries - 1} after ${delay}ms...`);
1084
+ await sleep(delay);
1085
+ }
931
1086
  }
932
1087
  }
933
1088
  throw lastError;
@@ -1008,12 +1163,14 @@ export async function createTestExecution(cfg, xrayToken, opts) {
1008
1163
  environments = [],
1009
1164
  testIssueIds = [],
1010
1165
  testPlanKey,
1166
+ fixVersion,
1011
1167
  } = opts;
1012
1168
 
1013
1169
  // 1. Create JIRA issue
1014
1170
  const issue = await createIssue(cfg, "Test Execution", {
1015
1171
  summary: summary || `Test Execution — ${new Date().toLocaleString()}`,
1016
1172
  description: description || summary || "",
1173
+ ...(fixVersion ? { fixVersions: [fixVersion] } : {}),
1017
1174
  });
1018
1175
 
1019
1176
  logger.step(`Test Execution created: ${issue.key}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msalaam/xray-qe-toolkit",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "QE toolkit for Xray Cloud — test management, tests.json standardisation, Playwright result import, and CI pipeline integration.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "license": "UNLICENSED",
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.27.1",
33
+ "@msalaam/xray-qe-toolkit": "^1.5.0",
33
34
  "axios": "^1.13.4",
34
35
  "commander": "^13.1.0",
35
36
  "dotenv": "^17.2.3",
@@ -44,7 +44,7 @@
44
44
  "type": "array",
45
45
  "items": {
46
46
  "type": "string",
47
- "enum": ["regression", "smoke", "edge", "critical", "integration", "e2e", "security", "performance"]
47
+ "enum": ["regression", "smoke", "edge", "critical", "integration", "e2e", "security", "performance", "contract", "functional", "negative", "positive", "boundary", "acceptance", "sanity", "data-driven", "exploratory", "accessibility"]
48
48
  },
49
49
  "description": "QE-assigned tags for categorisation. Also used as labels on the Xray Test issue."
50
50
  },
@@ -60,8 +60,27 @@
60
60
  "pattern": "^/"
61
61
  },
62
62
  "testSet": {
63
- "type": "string",
64
- "description": "Test Set name in Jira. Tests with the same testSet value are grouped into the same Xray Test Set. Typically matches the feature name from business-rules.yaml."
63
+ "oneOf": [
64
+ {
65
+ "type": "string",
66
+ "description": "Name of a single Xray Test Set this test belongs to."
67
+ },
68
+ {
69
+ "type": "array",
70
+ "items": { "type": "string" },
71
+ "minItems": 1,
72
+ "description": "Names of multiple Xray Test Sets this test belongs to."
73
+ }
74
+ ],
75
+ "description": "Xray Test Set name(s) — string or array of strings. Tests are grouped into Test Set issues in JIRA. xqt push creates sets automatically and adds the test to each."
76
+ },
77
+ "preconditions": {
78
+ "type": "array",
79
+ "items": {
80
+ "type": "string",
81
+ "pattern": "^[A-Z][A-Z0-9_]+-[0-9]+$"
82
+ },
83
+ "description": "JIRA issue keys of Xray Precondition issues linked to this test (e.g. [\"PROJ-5\"]). xqt push calls addPreconditionsToTest on each push."
65
84
  },
66
85
  "requirementKeys": {
67
86
  "type": "array",
@@ -107,7 +126,7 @@
107
126
  },
108
127
  "steps": {
109
128
  "type": "array",
110
- "description": "Ordered manual test steps (used for Manual test type).",
129
+ "description": "Ordered test steps. Dual purpose: (1) uploaded to Xray as Manual test steps visible in JIRA, (2) used by code generators to scaffold Playwright test bodies. Required when testType is Manual.",
111
130
  "items": {
112
131
  "type": "object",
113
132
  "required": ["action", "expected_result"],
@@ -115,21 +134,121 @@
115
134
  "action": {
116
135
  "type": "string",
117
136
  "minLength": 1,
118
- "description": "What action to perform in this step."
137
+ "description": "Human-readable description of the action (e.g. 'Send GET /v1/path/{param} with a valid value'). Used as the step title in Xray and as a generation hint."
119
138
  },
120
139
  "data": {
121
140
  "type": "string",
122
- "description": "Input data or parameters for the step."
141
+ "description": "Input data or environment variable references for the step (e.g. 'clientNo: env.TEST_CLIENT_NO')."
123
142
  },
124
143
  "expected_result": {
125
144
  "type": "string",
126
145
  "minLength": 1,
127
- "description": "Expected outcome of the step."
146
+ "description": "Expected outcome (e.g. 'HTTP 200, Content-Type: application/json, body matches MySchema')."
128
147
  }
129
148
  }
130
149
  }
131
150
  }
132
151
  }
152
+ },
153
+ "spec": {
154
+ "type": "object",
155
+ "description": "Structured test specification used for Playwright test code generation. Contains the operation, expected response values, and input data hints.",
156
+ "properties": {
157
+ "operation": {
158
+ "type": "object",
159
+ "required": ["method", "path"],
160
+ "properties": {
161
+ "method": {
162
+ "type": "string",
163
+ "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
164
+ "description": "HTTP method."
165
+ },
166
+ "path": {
167
+ "type": "string",
168
+ "description": "API path, with path parameters in {curly} notation (e.g. /v1/resource/{id})."
169
+ },
170
+ "operationId": {
171
+ "type": "string",
172
+ "description": "OpenAPI operationId for traceability."
173
+ }
174
+ }
175
+ },
176
+ "expectations": {
177
+ "type": "object",
178
+ "description": "Expected response values used to generate assertions.",
179
+ "properties": {
180
+ "statusCodes": {
181
+ "type": "array",
182
+ "items": { "type": "integer" },
183
+ "description": "List of acceptable HTTP status codes."
184
+ },
185
+ "contentType": {
186
+ "type": "string",
187
+ "description": "Expected Content-Type header value."
188
+ },
189
+ "responseSchemaHints": {
190
+ "type": "array",
191
+ "items": { "type": "string" },
192
+ "description": "OpenAPI schema refs for the expected response body (e.g. ['#/components/schemas/MyResponse'])."
193
+ }
194
+ }
195
+ },
196
+ "dataHints": {
197
+ "type": "object",
198
+ "description": "Input data hints used to scaffold test fixtures and parameter substitution.",
199
+ "properties": {
200
+ "pathParams": {
201
+ "type": "array",
202
+ "items": { "type": "string" },
203
+ "description": "Names of path parameters (matched to env[] values by convention)."
204
+ },
205
+ "queryParams": {
206
+ "type": "array",
207
+ "items": { "type": "string" },
208
+ "description": "Names of query parameters."
209
+ },
210
+ "body": {
211
+ "type": "string",
212
+ "description": "Request body description or JSON schema ref (empty string for no body)."
213
+ },
214
+ "env": {
215
+ "type": "array",
216
+ "items": { "type": "string" },
217
+ "description": "Environment variable names required to run the test (e.g. ['TEST_CLIENT_NO'])."
218
+ }
219
+ }
220
+ }
221
+ }
222
+ },
223
+ "contract": {
224
+ "type": "object",
225
+ "description": "OpenAPI contract validation specifics. Only required when testKind is 'contract'.",
226
+ "properties": {
227
+ "operationId": {
228
+ "type": "string",
229
+ "description": "OpenAPI operationId this contract test covers."
230
+ },
231
+ "expectedStatus": {
232
+ "type": "integer",
233
+ "description": "HTTP status code the contract expects."
234
+ },
235
+ "expectedContentType": {
236
+ "type": "string",
237
+ "description": "Expected Content-Type header."
238
+ },
239
+ "responseSchemaRef": {
240
+ "type": "string",
241
+ "description": "OpenAPI schema $ref for the response body (e.g. '#/components/schemas/MyResponse')."
242
+ },
243
+ "validationScope": {
244
+ "type": "array",
245
+ "items": {
246
+ "type": "string",
247
+ "enum": ["status-code", "content-type", "required-fields", "type-checks", "headers"]
248
+ },
249
+ "description": "Which contract assertions to enforce."
250
+ }
251
+ }
133
252
  }
134
253
  }
135
254
  }
@@ -152,6 +152,7 @@ After running `npx xqt init`:
152
152
  "tags": ["smoke", "regression"],
153
153
  "folder": "/{{SERVICE_NAME}}/HealthCheck/Validation",
154
154
  "testSet": "Health Check",
155
+ "preconditions": ["APIEE-50"],
155
156
  "requirementKeys": [],
156
157
  "xray": {
157
158
  "summary": "Service returns 200 OK when healthy",
@@ -175,7 +176,8 @@ After running `npx xqt init`:
175
176
  | `skip` | boolean | — | `true` to exclude from push-tests |
176
177
  | `tags` | string[] | — | QE tags for categorisation and filtering |
177
178
  | `folder` | string | — | Xray repository folder path — must start with `/` |
178
- | `testSet` | string | — | Test Set name in Jira — groups tests by feature |
179
+ | `testSet` | string **or** string[] | — | Test Set name(s) in Jira — use a string or an array to assign to multiple sets |
180
+ | `preconditions` | string[] | — | JIRA keys of Xray Precondition issues to link (e.g. `["APIEE-50"]`) |
179
181
  | `requirementKeys` | string[] | — | JIRA issue keys this test covers (creates coverage links) |
180
182
  | `xray.summary` | string | Yes | Test case title (JIRA issue summary) |
181
183
  | `xray.description` | string | — | Detailed description |
@@ -213,14 +215,19 @@ npx xqt create-plan --summary "{{SERVICE_NAME}} v2 Regression"
213
215
  npx xqt create-plan --summary "Sprint 12 Smoke Tests" --version "2024.12"
214
216
  ```
215
217
 
216
- ### `xqt push-tests`
218
+ ### `xqt push-tests` (alias: `push`)
217
219
 
218
220
  Create or update tests in Xray Cloud, then sync the Test Plan membership and folder structure.
219
221
 
222
+ - New tests are **bulk-imported** via the Xray REST API when there are ≥ `bulkImportThreshold` (default: 50) to create — significantly faster for large suites
223
+ - Test Plan membership is **bi-directionally synced**: new tests are added and tests removed from `tests.json` are removed from the plan
224
+ - Test Sets are **deduplicated by name** — safe to run on a fresh clone, existing sets are found in JIRA before creating new ones
225
+ - `preconditions` are linked after every create or update
226
+
220
227
  ```bash
221
- npx xqt push-tests
222
- npx xqt push-tests --plan APIEE-1234 # override plan key
223
- npx xqt push-tests --verbose
228
+ npx xqt push
229
+ npx xqt push --plan APIEE-1234 # override plan key
230
+ npx xqt push --verbose
224
231
  ```
225
232
 
226
233
  ### `xqt pull-tests`
@@ -232,15 +239,15 @@ npx xqt pull-tests --plan APIEE-1234
232
239
  npx xqt pull-tests --project APIEE --limit 200
233
240
  ```
234
241
 
235
- ### `xqt import-results`
242
+ ### `xqt import-results` (alias: `import`)
236
243
 
237
244
  Import test execution results into Xray. Creates a **new** Test Execution each time.
238
245
 
239
246
  ```bash
240
- npx xqt import-results --file test-results/results.json --env IOP-QA
241
- npx xqt import-results --file test-results/results.xml --env IOP-PROD
242
- npx xqt import-results --file test-results/results.json --env IOP-DEV \
243
- --plan APIEE-1234 --version "2024.12" --revision "a1b2c3d"
247
+ npx xqt import --file test-results/results.json --env IOP-QA
248
+ npx xqt import --file test-results/results.xml --env IOP-PROD
249
+ npx xqt import --file test-results/results.json --env IOP-DEV \
250
+ --plan APIEE-1234 --fix-version "2.5.0" --revision "a1b2c3d"
244
251
  ```
245
252
 
246
253
  **Options:**
@@ -250,10 +257,31 @@ npx xqt import-results --file test-results/results.json --env IOP-DEV \
250
257
  | `--file <path>` | Path to results file (`.json` = Playwright, `.xml` = JUnit) |
251
258
  | `--env <label>` | Environment label: `IOP-DEV`, `IOP-QA`, `IOP-PROD` |
252
259
  | `--plan <key>` | Test Plan key (overrides `.xrayrc`) |
253
- | `--version <ver>` | Fix version / release label |
254
- | `--revision <sha>` | Build number or git SHA |
260
+ | `--exec <key>` | Import INTO an existing execution (from `xqt exec`) |
261
+ | `--fix-version <ver>` | JIRA Fix Version name to stamp on the execution (e.g. `2.5.0`) |
262
+ | `--version <ver>` | Xray version label (free-form, separate from Fix Version) |
263
+ | `--revision <sha>` | Build number or git SHA (enables traceability to a specific commit) |
255
264
  | `--summary <text>` | Custom execution summary |
256
265
 
266
+ ### `xqt create-execution` (alias: `exec`)
267
+
268
+ Pre-create a Test Execution before running tests (for controlled test selection).
269
+
270
+ ```bash
271
+ EXEC_KEY=$(npx xqt exec --env IOP-QA --quiet)
272
+ npx playwright test
273
+ npx xqt import --file test-results/results.json --exec $EXEC_KEY
274
+ ```
275
+
276
+ | Flag | Description |
277
+ |---|---|
278
+ | `--env <label>` | Environment label |
279
+ | `--plan <key>` | Test Plan to link to |
280
+ | `--tests <ids>` | Comma-separated testIds or JIRA keys |
281
+ | `--summary <text>` | Custom execution title |
282
+ | `--fix-version <ver>` | JIRA Fix Version to stamp on the execution |
283
+ | `--quiet` | Print only the execution key |
284
+
257
285
  ### `xqt sync-folders`
258
286
 
259
287
  Sync the Xray repository folder structure from `folder` fields in tests.json.
@@ -306,7 +334,10 @@ npx xqt gen-pipeline --output .azure/ci.yml
306
334
 
307
335
  Tests live in **Test Sets** in Jira, grouped by feature (matching the feature name in `business-rules.yaml`).
308
336
 
309
- - Created automatically by `push-tests` from the `testSet` field in `tests.json`
337
+ - Assign a test to one set: `"testSet": "Health Check"`
338
+ - Assign to multiple sets: `"testSet": ["Health Check", "Smoke"]`
339
+ - Created automatically by `push` from the `testSet` field in `tests.json`
340
+ - **Deduplicated by name** — if `xray-mapping.json` is lost (e.g. fresh clone), sets are found by searching JIRA before creating a new one
310
341
  - Persistent across sprints — they represent what tests exist
311
342
  - Link Test Sets to Test Plans when entering a sprint
312
343
 
@@ -314,17 +345,20 @@ Tests live in **Test Sets** in Jira, grouped by feature (matching the feature na
314
345
 
315
346
  A Test Plan scopes testing for a specific sprint or release.
316
347
 
317
- - Create per sprint: `npx xqt create-plan --summary "Sprint 12 — {{SERVICE_NAME}}"`
348
+ - Create per sprint: `npx xqt plan --summary "Sprint 12 — {{SERVICE_NAME}}"`
318
349
  - Link relevant Test Sets to the Test Plan in Jira
319
350
  - The key is stored in `.xrayrc` (`testPlanKey`)
320
- - `import-results` links executions to the active plan
351
+ - `import` links executions to the active plan
352
+ - **`xqt push` performs a bi-directional sync** — new tests are added and tests removed from `tests.json` are removed from the plan
321
353
 
322
354
  ### Test Executions (ephemeral)
323
355
 
324
356
  A Test Execution represents one CI run.
325
- - Created automatically by `xqt import-results`
357
+ - Created automatically by `xqt import`
358
+ - **Pre-created** by `xqt exec` for controlled test selection (filter which tests run)
326
359
  - Tagged with the environment (`IOP-DEV`, `IOP-QA`, `IOP-PROD`)
327
360
  - Linked to the Test Plan
361
+ - Stamped with `--fix-version` and `--revision` for release traceability
328
362
 
329
363
  ### Summary
330
364
 
@@ -348,7 +382,7 @@ Set the default environment and allowed values in `.xrayrc`:
348
382
  Override per run with `--env`:
349
383
 
350
384
  ```bash
351
- npx xqt import-results --file results.json --env IOP-PROD
385
+ npx xqt import --file results.json --env IOP-PROD
352
386
  ```
353
387
 
354
388
  ---
@@ -387,12 +421,12 @@ The generated `playwright.config.ts` configures three reporters:
387
421
 
388
422
  ```bash
389
423
  # After running: npx playwright test
390
- npx xqt import-results --file test-results/results.json --env IOP-QA
424
+ npx xqt import --file test-results/results.json --env IOP-QA
391
425
  ```
392
426
 
393
427
  ### Test steps in Xray
394
428
 
395
- Steps defined with `test.step()` are automatically mapped to Xray step results:
429
+ Steps defined with `test.step()` are automatically mapped to Xray step results. **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.
396
430
 
397
431
  ```typescript
398
432
  test('Create and retrieve user', async ({ request }) => {
@@ -481,9 +515,9 @@ Key import step (runs even on test failure):
481
515
  ```yaml
482
516
  - script: |
483
517
  REVISION=$(echo "$(Build.SourceVersion)" | cut -c1-8)
484
- npx xqt import-results \
518
+ npx xqt import \
485
519
  --file test-results/results.json \
486
- --version "$(Build.BuildNumber)" \
520
+ --fix-version "$(Build.BuildNumber)" \
487
521
  --revision "$REVISION"
488
522
  condition: succeededOrFailed()
489
523
  env:
@@ -551,11 +585,34 @@ Auto-managed by `push-tests` — do not edit manually.
551
585
  "defaultEnvironment": "IOP-DEV",
552
586
  "environments": ["IOP-DEV", "IOP-QA", "IOP-PROD"],
553
587
  "folderRoot": "/{{SERVICE_NAME}}",
554
- "xrayRegion": "us"
588
+ "xrayRegion": "us",
589
+ "bulkImportThreshold": 50,
590
+ "statusMapping": {
591
+ "interrupted": "ABORTED",
592
+ "skipped": "TODO"
593
+ }
555
594
  }
556
595
  ```
557
596
 
558
- ### xrayRegion values
597
+ | Field | Description |
598
+ |---|---|
599
+ | `testPlanKey` | Default Test Plan key |
600
+ | `defaultEnvironment` | Fallback environment when `--env` is not provided |
601
+ | `environments` | Allowed environment labels |
602
+ | `folderRoot` | Base folder path in Xray Test Repository |
603
+ | `xrayRegion` | `us` (default), `eu`, or `au` |
604
+ | `bulkImportThreshold` | Min new-test count to switch to bulk REST import (default: `50`) |
605
+ | `statusMapping` | Override default Playwright → Xray status mapping |
606
+
607
+ #### Playwright status mapping
608
+
609
+ By default: `passed→PASSED`, `failed→FAILED`, `timedOut→FAILED`, `interrupted→ABORTED`, `skipped→TODO`. Override any value:
610
+
611
+ ```json
612
+ { "statusMapping": { "interrupted": "FAILED", "skipped": "TODO" } }
613
+ ```
614
+
615
+ You can also set `XQT_BULK_THRESHOLD=<n>` as an environment variable to override the bulk threshold at runtime.
559
616
 
560
617
  | Value | GraphQL endpoint |
561
618
  |---|---|