@msalaam/xray-qe-toolkit 1.3.4 → 1.4.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.
@@ -0,0 +1,112 @@
1
+ # Resources — AI Test Generation Sources
2
+
3
+ This folder contains documentation and specifications used by the AI agent to generate test cases for Xray.
4
+
5
+ ---
6
+
7
+ ## Folder Structure
8
+
9
+ ```
10
+ resources/
11
+ ├── api-specs/ OpenAPI/Swagger specifications (YAML/JSON)
12
+ ├── requirements/ Business requirements, acceptance criteria, logic docs
13
+ └── tickets/ Exported JIRA tickets, Confluence pages, user stories
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Supported File Types
19
+
20
+ ### `api-specs/`
21
+ - **OpenAPI 3.x / Swagger 2.0** — `.yaml`, `.yml`, `.json`
22
+ - **GraphQL schemas** — `.graphql`, `.gql`
23
+
24
+ Files in this folder drive model, service, assertion, and spec generation via the spec-driven workflow.
25
+
26
+ ### `requirements/`
27
+ - **Markdown** — `.md`
28
+ - **Plain text** — `.txt`
29
+ - **Word documents** — `.docx`
30
+ - **PDFs** — `.pdf`
31
+
32
+ Business logic, acceptance criteria, workflow descriptions, and domain rules. Used to generate test scenarios, edge cases, and validation logic.
33
+
34
+ ### `tickets/`
35
+ - **JIRA exports** — `.json`, `.xml`
36
+ - **Confluence HTML exports** — `.html`
37
+ - **User stories** — `.md`, `.txt`
38
+
39
+ Ticket acceptance criteria and linked documentation. Used to ensure test coverage aligns with stories and epics.
40
+
41
+ ---
42
+
43
+ ## How the AI Agent Uses These Files
44
+
45
+ 1. **Test Case Generation** (AI agent reads these files and populates `tests.json`)
46
+ - Reads `openapi.yaml` (API shape) and `business-rules.yaml` (domain rules)
47
+ - Produces `tests.json` entries with stable `test_id` values for Xray traceability
48
+ - Each `business-rules.yaml` rule maps 1:1 to a `tests.json` entry
49
+ - See **[SPEC-DRIVEN-APPROACH.md](../SPEC-DRIVEN-APPROACH.md)** for the full workflow
50
+
51
+ 2. **Xray Sync** (after `tests.json` is generated)
52
+ - `npx xqt validate` then `npx xqt push-tests`
53
+ - Creates Xray Test issues + Test Sets in Jira
54
+ - Populates `xray-mapping.json` with issue keys
55
+
56
+ 3. **Playwright Test Creation** (after Xray sync)
57
+ - Copilot skill reads `tests.json` + `xray-mapping.json`
58
+ - Creates one Playwright `test()` call per `tests.json` entry
59
+ - Real Jira keys in test titles from day one
60
+
61
+ 4. **Continuous Refinement**
62
+ - As you add/update specs and business rules, re-run the generation workflow
63
+ - The toolkit is **idempotent** — existing tests are updated, retired rules get `skip: true`
64
+ - Always validate before pushing: `npx xqt validate` then `npx xqt push-tests`
65
+
66
+ ---
67
+
68
+ ## Best Practices
69
+
70
+ ✅ **DO:**
71
+ - Keep specs up to date with your API implementation
72
+ - Organize files by feature/domain for easier tracking
73
+ - Use descriptive filenames (e.g., `auth-api-v2.yaml`, `payment-flows.md`)
74
+ - Commit this folder to source control (Git)
75
+
76
+ ⚠️ **DON'T:**
77
+ - Store credentials or secrets in these files
78
+ - Include massive binary files (>10MB) — extract relevant sections
79
+ - Mix unrelated specs in the same file
80
+ - Forget to update specs when the API changes
81
+
82
+ ---
83
+
84
+ ## Example Workflow
85
+
86
+ ```bash
87
+ # 1. Add your OpenAPI spec
88
+ cp ~/Downloads/my-api.yaml resources/openapi.yaml
89
+
90
+ # 2. Populate business rules (or generate a draft from the spec)
91
+ # Use Copilot + SPEC-DRIVEN-APPROACH.md to generate resources/business-rules.yaml
92
+
93
+ # 3. Validate knowledge artifacts
94
+ npx xqt validate --schema business-rules
95
+
96
+ # 4. Use Copilot (spec-driven workflow) to generate models, services, assertions, and specs
97
+ # See SPEC-DRIVEN-APPROACH.md for the full step-by-step
98
+
99
+ # 5. Validate tests.json
100
+ npx xqt validate
101
+
102
+ # 6. Push tests to Xray
103
+ npx xqt push-tests
104
+
105
+ # 7. Run tests and import results
106
+ npx playwright test
107
+ npx xqt import-results --file test-results/results.json
108
+ ```
109
+
110
+ ---
111
+
112
+ **Note:** This folder is the knowledge input for the spec-driven workflow. See **[SPEC-DRIVEN-APPROACH.md](../SPEC-DRIVEN-APPROACH.md)** for the complete greenfield/brownfield generation process.
@@ -1,74 +1,92 @@
1
1
  {
2
- "testExecution": {
3
- "summary": "My Project - Regression Suite",
4
- "description": "Automated regression tests for My Project API"
2
+ "testPlan": {
3
+ "key": "",
4
+ "summary": "My Service Test Plan"
5
5
  },
6
6
  "tests": [
7
7
  {
8
- "test_id": "TC-API-001",
8
+ "test_id": "TC-MYSVC-BR-001",
9
+ "type": "api",
10
+ "skip": false,
11
+ "tags": ["smoke", "regression"],
12
+ "folder": "/MyService/HealthCheck/Validation",
13
+ "testSet": "Health Check",
14
+ "requirementKeys": [],
15
+ "xray": {
16
+ "summary": "Service returns 200 OK when healthy",
17
+ "description": "Given Service is running with all dependencies reachable, when GET /health is called, then Response is 200 with body { \"status\": \"ok\" }",
18
+ "testType": "Generic",
19
+ "definition": "Automated Playwright API test: GET /health — Service returns 200 OK when healthy",
20
+ "priority": "High",
21
+ "labels": ["smoke", "regression"]
22
+ }
23
+ },
24
+ {
25
+ "test_id": "TC-MYSVC-BR-002",
26
+ "type": "api",
27
+ "skip": false,
28
+ "tags": ["regression", "security"],
29
+ "folder": "/MyService/ExampleFeature/Authorization",
30
+ "testSet": "Example Feature",
31
+ "requirementKeys": [],
32
+ "xray": {
33
+ "summary": "Unauthenticated requests are rejected",
34
+ "description": "Given No Authorization header is provided, when Any protected endpoint is called, then Response is 401 Unauthorized",
35
+ "testType": "Generic",
36
+ "definition": "Automated Playwright API test: GET /api/v1/resource — Unauthenticated requests are rejected",
37
+ "priority": "Highest",
38
+ "labels": ["regression", "security"]
39
+ }
40
+ },
41
+ {
42
+ "test_id": "TC-MYSVC-BR-003",
9
43
  "type": "api",
10
44
  "skip": false,
11
45
  "tags": ["regression", "smoke"],
46
+ "folder": "/MyService/ExampleFeature/BusinessLogic",
47
+ "testSet": "Example Feature",
48
+ "requirementKeys": [],
12
49
  "xray": {
13
- "summary": "Verify successful response for valid request",
14
- "description": "Test that the API returns the expected data and status code for a valid, well-formed request.",
50
+ "summary": "Valid resource creation returns 201 with resource id",
51
+ "description": "Given Authenticated user with create permission, when POST /api/v1/resource with valid body, then Response is 201 with { \"id\": \"<uuid>\" } and Location header",
52
+ "testType": "Generic",
53
+ "definition": "Automated Playwright API test: POST /api/v1/resource — Valid resource creation returns 201 with resource id",
15
54
  "priority": "High",
16
- "labels": ["API", "Positive", "Regression"],
17
- "steps": [
18
- {
19
- "action": "Send GET request to /api/resource with valid parameters",
20
- "data": "Method: GET, Headers: Authorization: Bearer {token}, Params: id=1",
21
- "expected_result": "API returns HTTP 200 with resource object"
22
- },
23
- {
24
- "action": "Validate response body structure",
25
- "data": "Response body JSON",
26
- "expected_result": "Response contains expected fields: id, name, status"
27
- }
28
- ]
55
+ "labels": ["regression", "smoke"]
29
56
  }
30
57
  },
31
58
  {
32
- "test_id": "TC-API-002",
59
+ "test_id": "TC-MYSVC-BR-004",
33
60
  "type": "api",
34
61
  "skip": false,
35
- "tags": ["regression", "negative"],
62
+ "tags": ["regression", "edge"],
63
+ "folder": "/MyService/ExampleFeature/Validation",
64
+ "testSet": "Example Feature",
65
+ "requirementKeys": [],
36
66
  "xray": {
37
- "summary": "Verify error response for invalid request",
38
- "description": "Test that the API returns a descriptive error when invalid or missing input is provided.",
39
- "priority": "Medium",
40
- "labels": ["API", "Negative", "Validation"],
41
- "steps": [
42
- {
43
- "action": "Send POST request to /api/resource with missing required fields",
44
- "data": "Method: POST, Headers: Authorization: Bearer {token}, Body: {}",
45
- "expected_result": "API returns HTTP 400 Bad Request"
46
- },
47
- {
48
- "action": "Validate error response body",
49
- "data": "Response body JSON",
50
- "expected_result": "Response contains 'error' field with a descriptive message string"
51
- }
52
- ]
67
+ "summary": "Missing required fields return 400 with field-level errors",
68
+ "description": "Given Authenticated user, when POST /api/v1/resource with missing required fields, then Response is 400 with errors array listing each missing field",
69
+ "testType": "Generic",
70
+ "definition": "Automated Playwright API test: POST /api/v1/resource — Missing required fields return 400 with field-level errors",
71
+ "priority": "High",
72
+ "labels": ["regression", "edge"]
53
73
  }
54
74
  },
55
75
  {
56
- "test_id": "TC-API-003",
76
+ "test_id": "TC-MYSVC-BR-005",
57
77
  "type": "api",
58
- "skip": true,
59
- "tags": ["edge", "performance"],
78
+ "skip": false,
79
+ "tags": ["regression", "edge"],
80
+ "folder": "/MyService/ExampleFeature/Edge",
81
+ "testSet": "Example Feature",
82
+ "requirementKeys": [],
60
83
  "xray": {
61
- "summary": "Verify API handles large payload within acceptable time",
62
- "description": "Test that the API processes a large request payload without timeout or degraded response.",
63
- "priority": "Low",
64
- "labels": ["API", "Performance", "Edge"],
65
- "steps": [
66
- {
67
- "action": "Send POST request to /api/resource with large payload",
68
- "data": "Method: POST, Headers: Authorization: Bearer {token}, Body: large JSON array with 1000 items",
69
- "expected_result": "API returns HTTP 200 within 3000ms"
70
- }
71
- ]
84
+ "summary": "Resource id not found returns 404",
85
+ "description": "Given Authenticated user, when GET /api/v1/resource/{id} with non-existent id, then Response is 404 with standard error body",
86
+ "testType": "Generic",
87
+ "definition": "Automated Playwright API test: GET /api/v1/resource/{id} — Resource id not found returns 404",
88
+ "priority": "Medium",
89
+ "labels": ["regression", "edge"]
72
90
  }
73
91
  }
74
92
  ]
@@ -1,70 +0,0 @@
1
- /**
2
- * Command: xray-qe gen-postman [--base-url <url>] [--ai] [--spec <path>] [--knowledge <path>]
3
- *
4
- * Generates a Postman Collection v2.1 JSON file from tests.json.
5
- * Optionally uses xray-mapping.json to embed JIRA keys for better traceability.
6
- * With --ai flag, enables AI-assisted generation (requires connected provider).
7
- */
8
-
9
- import fs from "node:fs";
10
- import logger, { setVerbose } from "../lib/logger.js";
11
- import { loadConfig } from "../lib/config.js";
12
- import { generate } from "../lib/postmanGenerator.js";
13
- import { loadMapping } from "../lib/testCaseBuilder.js";
14
-
15
- export default async function genPostman(opts = {}) {
16
- if (opts.verbose) setVerbose(true);
17
-
18
- logger.rocket("@msalaam/xray-qe-toolkit — Generate Postman Collection\n");
19
-
20
- const cfg = loadConfig({ envPath: opts.env });
21
-
22
- if (!fs.existsSync(cfg.testsPath)) {
23
- logger.error(`tests.json not found at ${cfg.testsPath}`);
24
- logger.info("Run 'npx xray-qe init' to create a starter tests.json");
25
- process.exit(1);
26
- }
27
-
28
- const testsConfig = JSON.parse(fs.readFileSync(cfg.testsPath, "utf8"));
29
-
30
- // Load mapping to embed JIRA keys when available
31
- const mapping = loadMapping(cfg.mappingPath);
32
-
33
- // Filter out skipped tests
34
- const filteredConfig = {
35
- ...testsConfig,
36
- tests: (testsConfig.tests || []).filter((t) => !t.skip),
37
- };
38
-
39
- // AI flag handling
40
- if (opts.ai) {
41
- logger.info("🤖 AI-assisted generation enabled");
42
- logger.warn("AI provider not yet connected — using schema-driven generation as fallback");
43
- logger.blank();
44
- }
45
-
46
- // Knowledge path override
47
- const knowledgePath = opts.knowledge || cfg.knowledgePath;
48
- if (opts.knowledge) {
49
- logger.info(`Using custom knowledge path: ${knowledgePath}`);
50
- }
51
-
52
- // Spec file handling (schema-driven generation without AI)
53
- if (opts.spec && fs.existsSync(opts.spec)) {
54
- logger.info(`Using OpenAPI spec: ${opts.spec}`);
55
- logger.info("📝 Schema-driven endpoint inference enabled");
56
- // TODO: Parse OpenAPI spec and enhance endpoint/method/body inference
57
- logger.blank();
58
- }
59
-
60
- generate(filteredConfig, cfg.collectionPath, {
61
- baseUrl: opts.baseUrl || "{{baseUrl}}",
62
- mapping: mapping,
63
- });
64
-
65
- logger.blank();
66
- logger.success(`Collection saved to ${cfg.collectionPath}`);
67
- logger.info("Import into Postman or run with Newman:");
68
- console.log(` npx newman run ${cfg.collectionPath}`);
69
- logger.blank();
70
- }
@@ -1,304 +0,0 @@
1
- /**
2
- * @msalaam/xray-qe-toolkit — Postman Collection Generator
3
- *
4
- * Generates a Postman Collection v2.1 JSON from tests.json.
5
- * Each test becomes a request folder; steps map to request items with
6
- * pre-request scripts and test assertions.
7
- *
8
- * The output is a working starting point — QEs or agents can further
9
- * refine the generated collection.
10
- */
11
-
12
- import fs from "node:fs";
13
- import path from "node:path";
14
- import logger from "./logger.js";
15
-
16
- /**
17
- * Generate a Postman v2.1 collection from tests.json config.
18
- *
19
- * @param {object} testsConfig Parsed tests.json content
20
- * @param {string} outputPath Where to write collection.postman.json
21
- * @param {object} [opts] Options
22
- * @param {string} [opts.baseUrl] Base URL placeholder (default: "{{baseUrl}}")
23
- * @param {object} [opts.mapping] xray-mapping.json content (for embedding JIRA keys)
24
- * @returns {object} The generated collection object
25
- */
26
- export function generate(testsConfig, outputPath, opts = {}) {
27
- const baseUrl = opts.baseUrl || "{{baseUrl}}";
28
- const mapping = opts.mapping || {};
29
- const tests = testsConfig.tests || [];
30
-
31
- // Filter for API tests only (type: "api" or unset for backward compatibility)
32
- const apiTests = tests.filter((t) => !t.type || t.type === "api");
33
-
34
- const collection = {
35
- info: {
36
- name: testsConfig.testExecution?.summary || "Generated Test Collection",
37
- description:
38
- (testsConfig.testExecution?.description || "Auto-generated by @msalaam/xray-qe-toolkit") +
39
- "\n\n⚠️ SCAFFOLD: This is a generated starting point. Review and enhance each request with proper assertions, environment variables, and edge cases before use.",
40
- schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
41
- _postman_id: generateUUID(),
42
- },
43
- variable: [
44
- {
45
- key: "baseUrl",
46
- value: "https://api.example.com",
47
- type: "string",
48
- },
49
- {
50
- key: "authToken",
51
- value: "",
52
- type: "string",
53
- },
54
- ],
55
- auth: {
56
- type: "bearer",
57
- bearer: [
58
- {
59
- key: "token",
60
- value: "{{authToken}}",
61
- type: "string",
62
- },
63
- ],
64
- },
65
- item: apiTests.map((test) => buildTestFolder(test, baseUrl, mapping)),
66
- };
67
-
68
- fs.writeFileSync(outputPath, JSON.stringify(collection, null, 2));
69
- logger.save(`Collection written to ${path.basename(outputPath)}`);
70
- logger.info(`${apiTests.length} API test(s) → ${countRequests(collection)} request(s)`);
71
- if (tests.length > apiTests.length) {
72
- logger.info(`${tests.length - apiTests.length} non-API tests skipped (use type: "api" to include)`);
73
- }
74
-
75
- return collection;
76
- }
77
-
78
- // ─── Internal builders ────────────────────────────────────────────────────────
79
-
80
- /**
81
- * Convert a single test definition into a Postman folder with request items.
82
- */
83
- function buildTestFolder(test, baseUrl, mapping = {}) {
84
- // Use JIRA key if available in mapping, otherwise use test_id
85
- const testKey = mapping[test.test_id]?.key || test.test_id;
86
-
87
- const folder = {
88
- name: `[${testKey}] ${test.xray.summary}`,
89
- description: test.xray.description || "",
90
- item: [],
91
- };
92
-
93
- if (!test.xray.steps || test.xray.steps.length === 0) {
94
- // No steps — create a single placeholder request
95
- folder.item.push(buildPlaceholderRequest(test, baseUrl));
96
- return folder;
97
- }
98
-
99
- test.xray.steps.forEach((step, idx) => {
100
- folder.item.push(buildStepRequest(test, step, idx + 1, baseUrl));
101
- });
102
-
103
- return folder;
104
- }
105
-
106
- /**
107
- * Build a request item from a test step.
108
- */
109
- function buildStepRequest(test, step, stepNum, baseUrl) {
110
- // Try to infer HTTP method and path from the step action/data
111
- const { method, endpoint } = inferEndpoint(step);
112
-
113
- const item = {
114
- name: `Step ${stepNum}: ${truncate(step.action, 80)}`,
115
- request: {
116
- method,
117
- header: [
118
- {
119
- key: "Content-Type",
120
- value: "application/json",
121
- },
122
- ],
123
- url: {
124
- raw: `${baseUrl}${endpoint}`,
125
- host: [baseUrl],
126
- path: endpoint.split("/").filter(Boolean),
127
- },
128
- },
129
- event: [],
130
- };
131
-
132
- // Add pre-request script with step data context
133
- if (step.data) {
134
- item.event.push({
135
- listen: "prerequest",
136
- script: {
137
- type: "text/javascript",
138
- exec: [
139
- `// Step Data: ${step.data}`,
140
- `// Action: ${step.action}`,
141
- `console.log("Executing: ${escapeJs(step.action)}");`,
142
- ],
143
- },
144
- });
145
-
146
- // Try to extract request body from step data
147
- const body = extractBody(step.data);
148
- if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
149
- item.request.body = {
150
- mode: "raw",
151
- raw: body,
152
- options: { raw: { language: "json" } },
153
- };
154
- }
155
- }
156
-
157
- // Add test script with expected result assertion
158
- if (step.expected_result) {
159
- item.event.push({
160
- listen: "test",
161
- script: {
162
- type: "text/javascript",
163
- exec: buildTestScript(step),
164
- },
165
- });
166
- }
167
-
168
- return item;
169
- }
170
-
171
- /**
172
- * Build a placeholder request when a test has no steps.
173
- */
174
- function buildPlaceholderRequest(test, baseUrl) {
175
- return {
176
- name: test.xray.summary,
177
- request: {
178
- method: "GET",
179
- header: [],
180
- url: {
181
- raw: `${baseUrl}/TODO`,
182
- host: [baseUrl],
183
- path: ["TODO"],
184
- },
185
- description: `TODO: Configure this request for test ${test.test_id}`,
186
- },
187
- };
188
- }
189
-
190
- /**
191
- * Infer HTTP method and endpoint from step action/data text.
192
- */
193
- function inferEndpoint(step) {
194
- const combined = `${step.action} ${step.data || ""}`;
195
- const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
196
-
197
- let method = "GET";
198
- let endpoint = "/api/TODO";
199
-
200
- // Look for explicit HTTP method
201
- for (const m of methods) {
202
- if (combined.toUpperCase().includes(m)) {
203
- method = m;
204
- break;
205
- }
206
- }
207
-
208
- // Look for a path pattern like /api/something or /endpoint
209
- const pathMatch = combined.match(/(?:\/[\w{}\-]+){1,10}/);
210
- if (pathMatch) {
211
- endpoint = pathMatch[0];
212
- }
213
-
214
- return { method, endpoint };
215
- }
216
-
217
- /**
218
- * Build Postman test script lines from an expected result.
219
- */
220
- function buildTestScript(step) {
221
- const lines = [
222
- `// Expected: ${step.expected_result}`,
223
- "",
224
- ];
225
-
226
- const expected = step.expected_result.toLowerCase();
227
-
228
- // Infer status code assertions
229
- const statusMatch = expected.match(/(\d{3})\s/);
230
- if (statusMatch) {
231
- lines.push(`pm.test("Status code is ${statusMatch[1]}", function () {`);
232
- lines.push(` pm.response.to.have.status(${statusMatch[1]});`);
233
- lines.push(`});`);
234
- lines.push("");
235
- }
236
-
237
- // Check for schema vSCAFFOLDation mentions
238
- if (expected.includes("schema") || expected.includes("required fields")) {
239
- lines.push(`pm.test("Response has expected schema", function () {`);
240
- lines.push(` const json = pm.response.json();`);
241
- lines.push(` // TODO: Add schema assertions based on expected fields`);
242
- lines.push(` pm.expect(json).to.be.an("object");`);
243
- lines.push(`});`);
244
- lines.push("");
245
- }
246
-
247
- // Check for error message mentions
248
- if (expected.includes("error") || expected.includes("400") || expected.includes("401")) {
249
- lines.push(`pm.test("Error response format", function () {`);
250
- lines.push(` const json = pm.response.json();`);
251
- lines.push(` pm.expect(json).to.have.property("error");`);
252
- lines.push(`});`);
253
- lines.push("");
254
- }
255
-
256
- // Always add a general assertion
257
- lines.push(`pm.test("${escapeJs(truncate(step.expected_result, 100))}", function () {`);
258
- lines.push(` pm.response.to.be.ok;`);
259
- lines.push(`});`);
260
-
261
- return lines;
262
- }
263
-
264
- /**
265
- * Try to extract a JSON body from step data text.
266
- */
267
- function extractBody(data) {
268
- // Look for JSON object in the data field
269
- const jsonMatch = data.match(/\{[\s\S]*\}/);
270
- if (jsonMatch) {
271
- try {
272
- JSON.parse(jsonMatch[0]);
273
- return jsonMatch[0];
274
- } catch {
275
- return jsonMatch[0]; // Return raw even if not valid JSON
276
- }
277
- }
278
- return null;
279
- }
280
-
281
- // ─── Utilities ────────────────────────────────────────────────────────────────
282
-
283
- function truncate(str, max) {
284
- return str.length > max ? str.slice(0, max - 3) + "..." : str;
285
- }
286
-
287
- function escapeJs(str) {
288
- return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
289
- }
290
-
291
- function generateUUID() {
292
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
293
- const r = (Math.random() * 16) | 0;
294
- return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
295
- });
296
- }
297
-
298
- function countRequests(collection) {
299
- let count = 0;
300
- for (const folder of collection.item) {
301
- count += folder.item ? folder.item.length : 1;
302
- }
303
- return count;
304
- }