@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.
@@ -1,20 +1,39 @@
1
1
  /**
2
- * Command: xqt import-results --file <path> [--testExecKey <key>]
2
+ * Command: xqt import-results --file <path> --env <environment>
3
3
  *
4
- * Imports test results into Xray Cloud and associates them with a Test Execution.
4
+ * Imports test execution results into Xray Cloud.
5
+ * Each invocation creates a NEW Test Execution — fresh execution per CI run.
5
6
  *
6
7
  * Supported formats:
7
- * - JUnit XML (.xml) - Standard JUnit or XUnit format
8
- * - Playwright JSON (.json) - Playwright JSON reporter output
8
+ * - Playwright JSON (.json) Playwright JSON reporter output
9
+ * - JUnit XML (.xml) Standard JUnit / XUnit format
9
10
  *
10
- * Designed to run in CI no human interaction required.
11
+ * The Xray JSON info object will include:
12
+ * - testPlanKey (from .xrayrc or --plan flag)
13
+ * - testEnvironments (from --env flag, default: defaultEnvironment from .xrayrc)
14
+ * - version / revision (from --version / --revision flags)
15
+ * - testExecutionKey (from --exec flag — import into a pre-created execution)
16
+ *
17
+ * Two modes:
18
+ * AUTO: xqt import-results --file results.json --env IOP-QA
19
+ * → Xray auto-creates a new Test Execution from the results
20
+ *
21
+ * PRE-CREATED: xqt create-execution --env IOP-QA (get back APIEE-9876)
22
+ * xqt import-results --file results.json --exec APIEE-9876
23
+ * → Results imported into the existing execution
24
+ *
25
+ * Designed for CI — no human interaction required.
11
26
  */
12
27
 
13
28
  import fs from "node:fs";
14
29
  import path from "node:path";
15
30
  import logger, { setVerbose } from "../lib/logger.js";
16
31
  import { loadConfig, validateConfig } from "../lib/config.js";
17
- import { authenticate, importResults as importResultsXml, importResultsXrayJson } from "../lib/xrayClient.js";
32
+ import {
33
+ authenticate,
34
+ importResultsMultipart,
35
+ importResults as importResultsXml,
36
+ } from "../lib/xrayClient.js";
18
37
  import { convertPlaywrightToXray } from "../lib/playwrightConverter.js";
19
38
 
20
39
  export default async function importResults(opts = {}) {
@@ -25,6 +44,7 @@ export default async function importResults(opts = {}) {
25
44
  const cfg = loadConfig({ envPath: opts.env });
26
45
  validateConfig(cfg);
27
46
 
47
+ // ── Resolve file path ──────────────────────────────────────────────────────
28
48
  const filePath = path.resolve(opts.file);
29
49
  if (!fs.existsSync(filePath)) {
30
50
  logger.error(`Results file not found: ${filePath}`);
@@ -34,112 +54,125 @@ export default async function importResults(opts = {}) {
34
54
  const fileName = path.basename(filePath);
35
55
  const fileExt = path.extname(filePath).toLowerCase();
36
56
 
37
- logger.info(`File: ${fileName}`);
38
- if (opts.testExecKey) {
39
- logger.info(`Test Execution: ${opts.testExecKey}`);
40
- }
57
+ // ── Resolve environment ────────────────────────────────────────────────────
58
+ const environment = opts.environment || cfg.defaultEnvironment || "IOP-DEV";
59
+ const testEnvironments = [environment];
60
+
61
+ // ── Resolve plan key ───────────────────────────────────────────────────────
62
+ const planKey = opts.plan || cfg.testPlanKey;
63
+
64
+ // ── Resolve pre-created execution key ─────────────────────────────────────
65
+ const execKey = opts.exec || null;
66
+
67
+ // ── Summary info ──────────────────────────────────────────────────────────
68
+ logger.info(`File: ${fileName}`);
69
+ logger.info(`Environment: ${environment}`);
70
+ if (execKey) logger.info(`Execution: ${execKey} (pre-created)`);
71
+ if (planKey) logger.info(`Test Plan: ${planKey}`);
72
+ if (opts.version) logger.info(`Version: ${opts.version}`);
73
+ if (opts.revision) logger.info(`Revision: ${opts.revision}`);
41
74
  logger.blank();
42
75
 
43
- // Authenticate
76
+ // ── Authenticate ───────────────────────────────────────────────────────────
44
77
  const xrayToken = await authenticate(cfg);
45
78
 
46
- // Detect format and import
79
+ // ── Build base Xray info object ────────────────────────────────────────────
80
+ const runTimestamp = new Date().toLocaleString();
81
+ const infoBase = {
82
+ summary: opts.summary || `${environment} Test Run — ${runTimestamp}`,
83
+ description: opts.description || `Automated test execution for ${environment}`,
84
+ testEnvironments,
85
+ // If a pre-created execution key is given, Xray will import INTO that execution
86
+ ...(execKey && { testExecutionKey: execKey }),
87
+ // Otherwise link to the plan (Xray will create a new execution under it)
88
+ ...(!execKey && planKey && { testPlanKey: planKey }),
89
+ ...(opts.version && { version: opts.version }),
90
+ ...(opts.revision && { revision: opts.revision }),
91
+ };
92
+
47
93
  let result;
94
+ let xrayJson;
48
95
 
49
- if (fileExt === '.json') {
50
- // Playwright JSON format
51
- logger.send(`Converting Playwright results to Xray format...`);
96
+ if (fileExt === ".json") {
97
+ // ── Playwright JSON ────────────────────────────────────────────────────
98
+ logger.send("Converting Playwright results to Xray format...");
52
99
 
53
- const playwrightJson = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
54
-
55
- // Convert to Xray format
56
- const xrayJson = convertPlaywrightToXray(playwrightJson, {
57
- testExecutionKey: opts.testExecKey,
100
+ const playwrightJson = JSON.parse(fs.readFileSync(filePath, "utf-8"));
101
+
102
+ xrayJson = convertPlaywrightToXray(playwrightJson, {
58
103
  projectKey: cfg.jiraProjectKey,
59
- summary: opts.summary || `Playwright Test Execution - ${new Date().toLocaleString()}`,
60
- description: opts.description,
61
- skipWithoutAnnotations: !!opts.testExecKey, // Skip tests without keys when updating existing execution
104
+ infoOverrides: infoBase,
62
105
  });
63
106
 
64
- // Check if we have tests to upload
65
107
  if (xrayJson.tests.length === 0) {
66
- logger.warn('⚠️ No tests with Xray annotations found.');
67
- logger.info('\nAdd annotations to your tests:');
68
- logger.info(` test.info().annotations.push({ type: 'xray', description: 'APIEE-XXXX' });\n`);
108
+ logger.warn("No tests with Xray annotations found.");
109
+ logger.info("Add annotations to your Playwright tests:");
110
+ logger.info(" test.info().annotations.push({ type: 'xray', description: 'PROJ-1234' });");
69
111
  process.exit(0);
70
112
  }
71
113
 
72
- const testsWithKeys = xrayJson.tests.filter(t => t.testKey).length;
73
- const testsWithoutKeys = xrayJson.tests.filter(t => !t.testKey).length;
114
+ const withKeys = xrayJson.tests.filter((t) => t.testKey).length;
115
+ const withoutKeys = xrayJson.tests.length - withKeys;
74
116
 
75
- logger.step(`Found ${testsWithKeys} test(s) with Xray annotations`);
76
-
77
- if (testsWithoutKeys > 0 && !opts.testExecKey) {
78
- logger.info(` ${testsWithoutKeys} test(s) without annotations will create new tests`);
79
- } else if (testsWithoutKeys > 0 && opts.testExecKey) {
80
- logger.warn(` Skipped ${testsWithoutKeys} test(s) without annotations`);
81
- }
117
+ logger.step(`${withKeys} test(s) with Xray keys, ${withoutKeys} without`);
82
118
 
83
- // Save debug output if verbose
84
119
  if (opts.verbose) {
85
- const debugPath = filePath.replace('.json', '-xray-debug.json');
120
+ const debugPath = filePath.replace(".json", "-xray-debug.json");
86
121
  fs.writeFileSync(debugPath, JSON.stringify(xrayJson, null, 2));
87
- logger.info(` Debug: Xray JSON saved to ${path.basename(debugPath)}`);
122
+ logger.info(`Debug Xray JSON saved to ${path.basename(debugPath)}`);
88
123
  }
89
124
  logger.blank();
90
125
 
91
- // Import to Xray
92
- logger.send(`Uploading ${xrayJson.tests.length} test result(s) to Xray...`);
93
- result = await importResultsXrayJson(cfg, xrayToken, xrayJson);
126
+ logger.send(`Uploading ${xrayJson.tests.length} result(s) to Xray...`);
127
+ result = await importResultsMultipart(cfg, xrayToken, xrayJson);
94
128
  } else {
95
- // JUnit XML format (default)
96
- logger.send(`Importing JUnit XML results...`);
97
-
129
+ // ── JUnit XML ──────────────────────────────────────────────────────────
130
+ logger.send("Importing JUnit XML results...");
98
131
  const xmlBuffer = fs.readFileSync(filePath);
99
- result = await importResultsXml(cfg, xrayToken, xmlBuffer, opts.testExecKey);
132
+ result = await importResultsXml(cfg, xrayToken, xmlBuffer, null, infoBase);
100
133
  }
101
134
 
102
135
  logger.success("Results imported successfully");
103
136
  logger.blank();
104
137
 
105
- // Display summary for JSON results
106
- if (fileExt === '.json' && xrayJson) {
107
- const summaryStats = calculateTestSummary(xrayJson.tests);
108
-
109
- logger.info('Summary:');
110
- logger.success(` ✓ Passed: ${summaryStats.passed}`)
111
- ;
112
- if (summaryStats.failed > 0) {
113
- logger.error(` ✗ Failed: ${summaryStats.failed}`);
114
- }
115
- if (summaryStats.skipped > 0) {
116
- logger.warn(` ⊘ Skipped: ${summaryStats.skipped}`);
117
- }
138
+ // ── Print summary ──────────────────────────────────────────────────────────
139
+ if (xrayJson?.tests) {
140
+ const stats = summarise(xrayJson.tests);
141
+ logger.info("Summary:");
142
+ logger.success(` PASSED: ${stats.passed}`);
143
+ if (stats.failed > 0) logger.error(` FAILED: ${stats.failed}`);
144
+ if (stats.skipped > 0) logger.warn(` SKIPPED: ${stats.skipped}`);
118
145
  logger.blank();
119
146
  }
120
147
 
121
- // Show link to Test Execution
122
- if (result?.key) {
123
- logger.link(`View: ${cfg.jiraUrl}/browse/${result.key}`);
124
- } else if (result?.testExecIssue?.key) {
125
- logger.link(`View: ${cfg.jiraUrl}/browse/${result.testExecIssue.key}`);
126
- } else if (opts.testExecKey) {
127
- logger.link(`View: ${cfg.jiraUrl}/browse/${opts.testExecKey}`);
148
+ // ── Show execution link ────────────────────────────────────────────────────
149
+ const resultKey =
150
+ execKey ||
151
+ result?.key ||
152
+ result?.testExecIssue?.key ||
153
+ result?.data?.key ||
154
+ null;
155
+
156
+ if (resultKey) {
157
+ logger.link(`Test Execution: ${cfg.jiraUrl}/browse/${resultKey}`);
158
+ }
159
+ if (planKey) {
160
+ logger.link(`Test Plan: ${cfg.jiraUrl}/browse/${planKey}`);
128
161
  }
129
162
 
130
163
  logger.blank();
131
164
  }
132
165
 
133
166
  /**
134
- * Calculate test result summary
135
- * @param {array} tests - Xray test results
136
- * @returns {object} Summary stats
167
+ * Calculate test result summary from Xray test objects.
168
+ * @param {Array} tests
169
+ * @returns {{ passed: number, failed: number, skipped: number }}
137
170
  */
138
- function calculateTestSummary(tests) {
171
+ function summarise(tests) {
139
172
  return {
140
- passed: tests.filter(t => t.status === 'PASSED').length,
141
- failed: tests.filter(t => t.status === 'FAILED').length,
142
- skipped: tests.filter(t => t.status === 'SKIPPED').length,
143
- total: tests.length,
173
+ passed: tests.filter((t) => t.status === "PASSED").length,
174
+ failed: tests.filter((t) => t.status === "FAILED").length,
175
+ skipped: tests.filter((t) => t.status === "SKIPPED").length,
144
176
  };
145
177
  }
178
+
package/commands/init.js CHANGED
@@ -1,18 +1,36 @@
1
1
  /**
2
- * Command: xray-qe init
2
+ * Command: xqt init
3
3
  *
4
- * Scaffolds a new project with starter templates:
5
- * - tests.json (test definitions)
6
- * - xray-mapping.json (empty mapping)
7
- * - .env.example (environment variable template)
8
- * - .xrayrc (optional project-level config)
4
+ * Scaffolds a per-repo QA test project structure.
5
+ * Idempotent — never overwrites existing files or folders.
6
+ *
7
+ * Creates:
8
+ * .npmrc — npm registry config (ADO feed)
9
+ * resources/ — Spec + business rules root
10
+ * resources/api-specs/ — OpenAPI / Swagger specifications
11
+ * resources/api-specs/openapi.yaml — OpenAPI spec placeholder
12
+ * resources/requirements/ — Business requirements & acceptance criteria
13
+ * resources/tickets/ — Exported JIRA tickets & Confluence pages
14
+ * resources/business-rules.yaml — Business rules template
15
+ * resources/README.md — Resources folder guide
16
+ * tests/models/ — Data models, fixtures, payloads
17
+ * tests/resources/ — Shared helpers, utilities, constants
18
+ * tests/services/ — Service-level test logic (setup/teardown)
19
+ * tests/specs/ — Playwright spec files
20
+ * playwright.config.ts — Pre-configured for JSON + JUnit reporters
21
+ * tests.json — Test definitions
22
+ * xray-mapping.json — Empty mapping
23
+ * .env.example — Credential template
24
+ * .xrayrc — Project-level config
25
+ * XQT-GUIDE.md — Comprehensive xqt usage guide
26
+ * SPEC-DRIVEN-APPROACH.md — Spec-driven QE process & workflow
27
+ * azure-pipelines.yml — ADO QA pipeline template
9
28
  */
10
29
 
11
30
  import fs from "node:fs";
12
31
  import path from "node:path";
13
32
  import { fileURLToPath } from "node:url";
14
- import logger from "../lib/logger.js";
15
- import { setVerbose } from "../lib/logger.js";
33
+ import logger, { setVerbose } from "../lib/logger.js";
16
34
 
17
35
  const __filename = fileURLToPath(import.meta.url);
18
36
  const __dirname = path.dirname(__filename);
@@ -27,19 +45,34 @@ export default async function init(opts = {}) {
27
45
  let created = 0;
28
46
  let skipped = 0;
29
47
 
30
- // Create knowledge/ directory structure
31
- const knowledgeDirs = [
32
- { path: "knowledge", desc: "Knowledge base folder" },
33
- { path: "knowledge/api-specs", desc: "API specifications (OpenAPI, Swagger)" },
34
- { path: "knowledge/requirements", desc: "Business requirements and docs" },
35
- { path: "knowledge/tickets", desc: "JIRA/Confluence exports" },
48
+ // ── Detect project name from package.json ─────────────────────────────────
49
+ let serviceName = path.basename(cwd);
50
+ try {
51
+ const pkgPath = path.join(cwd, "package.json");
52
+ if (fs.existsSync(pkgPath)) {
53
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
54
+ if (pkg.name) serviceName = pkg.name.replace(/^@[^/]+\//, ""); // strip scope
55
+ }
56
+ } catch { /* best-effort */ }
57
+
58
+ // ── Directory structure ───────────────────────────────────────────────────
59
+ const directories = [
60
+ { path: "resources", desc: "API spec + business rules" },
61
+ { path: "resources/api-specs", desc: "OpenAPI / Swagger specifications" },
62
+ { path: "resources/requirements", desc: "Business requirements & acceptance criteria" },
63
+ { path: "resources/tickets", desc: "Exported JIRA tickets & Confluence pages" },
64
+ { path: "tests", desc: "Test root directory" },
65
+ { path: "tests/models", desc: "Data models, fixtures, payloads" },
66
+ { path: "tests/resources", desc: "Shared helpers, utilities, constants" },
67
+ { path: "tests/services", desc: "Service-level setup/teardown helpers" },
68
+ { path: "tests/specs", desc: "Playwright spec files" },
36
69
  ];
37
70
 
38
- for (const dir of knowledgeDirs) {
71
+ for (const dir of directories) {
39
72
  const dirPath = path.join(cwd, dir.path);
40
73
  if (!fs.existsSync(dirPath)) {
41
74
  fs.mkdirSync(dirPath, { recursive: true });
42
- logger.success(`${dir.path}/ created (${dir.desc})`);
75
+ logger.success(`${dir.path}/ created ${dir.desc}`);
43
76
  created++;
44
77
  } else {
45
78
  logger.warn(`${dir.path}/ already exists — skipping`);
@@ -47,96 +80,251 @@ export default async function init(opts = {}) {
47
80
  }
48
81
  }
49
82
 
50
- // Copy knowledge README
51
- const knowledgeReadmeSrc = path.join(TEMPLATES, "knowledge-README.md");
52
- const knowledgeReadmeDest = path.join(cwd, "knowledge", "README.md");
53
- if (!fs.existsSync(knowledgeReadmeDest)) {
54
- fs.copyFileSync(knowledgeReadmeSrc, knowledgeReadmeDest);
55
- logger.success("knowledge/README.md created (Knowledge base guide)");
83
+ // .gitkeep files in leaf directories
84
+ for (const subdir of [
85
+ "resources/api-specs", "resources/requirements", "resources/tickets",
86
+ "tests/models", "tests/resources", "tests/services", "tests/specs",
87
+ ]) {
88
+ const gkPath = path.join(cwd, subdir, ".gitkeep");
89
+ if (!fs.existsSync(gkPath)) fs.writeFileSync(gkPath, "");
90
+ }
91
+
92
+ // ── .npmrc ────────────────────────────────────────────────────────────────
93
+ const npmrcDest = path.join(cwd, ".npmrc");
94
+ if (!fs.existsSync(npmrcDest)) {
95
+ fs.copyFileSync(path.join(TEMPLATES, ".npmrc"), npmrcDest);
96
+ logger.success(".npmrc created — npm registry config (ADO feed)");
56
97
  created++;
57
98
  } else {
58
- logger.warn("knowledge/README.md already exists — skipping");
99
+ logger.warn(".npmrc already exists — skipping");
59
100
  skipped++;
60
101
  }
61
102
 
62
- // Copy XQT guide — always written as XQT-GUIDE.md so the user's own README is never touched
63
- const readmeSrc = path.join(TEMPLATES, "README.template.md");
64
- const guideDest = path.join(cwd, "XQT-GUIDE.md");
65
- if (!fs.existsSync(guideDest)) {
66
- fs.copyFileSync(readmeSrc, guideDest);
67
- logger.success("XQT-GUIDE.md created (Getting started guide)");
103
+ // ── resources/ files ─────────────────────────────────────────────────────
104
+ // OpenAPI spec placeholder
105
+ const openapiDest = path.join(cwd, "resources", "api-specs", "openapi.yaml");
106
+ if (!fs.existsSync(openapiDest)) {
107
+ fs.writeFileSync(openapiDest, [
108
+ `openapi: "3.0.3"`,
109
+ `info:`,
110
+ ` title: ${serviceName}`,
111
+ ` version: "1.0.0"`,
112
+ ` description: Replace this template with your actual OpenAPI specification.`,
113
+ `paths: {}`,
114
+ ].join("\n") + "\n");
115
+ logger.success("resources/api-specs/openapi.yaml created — OpenAPI spec placeholder");
68
116
  created++;
69
117
  } else {
70
- logger.warn("XQT-GUIDE.md already exists — skipping");
118
+ logger.warn("resources/api-specs/openapi.yaml already exists — skipping");
71
119
  skipped++;
72
120
  }
73
121
 
74
- // Create .gitkeep files in knowledge subdirs to ensure they're tracked
75
- for (const subdir of ["api-specs", "requirements", "tickets"]) {
76
- const gitkeepPath = path.join(cwd, "knowledge", subdir, ".gitkeep");
77
- if (!fs.existsSync(gitkeepPath)) {
78
- fs.writeFileSync(gitkeepPath, "");
79
- }
122
+ // business-rules.yaml from template
123
+ const brSrc = path.join(TEMPLATES, "business-rules.yaml");
124
+ const brDest = path.join(cwd, "resources", "business-rules.yaml");
125
+ if (!fs.existsSync(brDest)) {
126
+ fs.copyFileSync(brSrc, brDest);
127
+ logger.success("resources/business-rules.yaml created — Business rules template");
128
+ created++;
129
+ } else {
130
+ logger.warn("resources/business-rules.yaml already exists — skipping");
131
+ skipped++;
80
132
  }
81
133
 
82
- const files = [
83
- { src: "tests.json", dest: "tests.json", desc: "Test definitions" },
84
- { src: "xray-mapping.json", dest: "xray-mapping.json", desc: "Xray mapping (empty)" },
85
- ];
134
+ // resources/README.md from template
135
+ const resReadmeDest = path.join(cwd, "resources", "README.md");
136
+ if (!fs.existsSync(resReadmeDest)) {
137
+ fs.copyFileSync(path.join(TEMPLATES, "resources-README.md"), resReadmeDest);
138
+ logger.success("resources/README.md created — Resources folder guide");
139
+ created++;
140
+ } else {
141
+ logger.warn("resources/README.md already exists — skipping");
142
+ skipped++;
143
+ }
86
144
 
87
- for (const file of files) {
88
- const destPath = path.join(cwd, file.dest);
89
- if (fs.existsSync(destPath)) {
90
- logger.warn(`${file.dest} already exists — skipping`);
91
- skipped++;
92
- } else {
93
- fs.copyFileSync(path.join(TEMPLATES, file.src), destPath);
94
- logger.success(`${file.dest} created (${file.desc})`);
95
- created++;
96
- }
145
+ // ── tests.json ────────────────────────────────────────────────────────────
146
+ const testsDest = path.join(cwd, "tests.json");
147
+ if (!fs.existsSync(testsDest)) {
148
+ fs.copyFileSync(path.join(TEMPLATES, "tests.json"), testsDest);
149
+ logger.success("tests.json created — Test definitions");
150
+ created++;
151
+ } else {
152
+ logger.warn("tests.json already exists — skipping");
153
+ skipped++;
154
+ }
155
+
156
+ // ── xray-mapping.json ─────────────────────────────────────────────────────
157
+ const mappingDest = path.join(cwd, "xray-mapping.json");
158
+ if (!fs.existsSync(mappingDest)) {
159
+ fs.copyFileSync(path.join(TEMPLATES, "xray-mapping.json"), mappingDest);
160
+ logger.success("xray-mapping.json created — Xray ID mapping (empty)");
161
+ created++;
162
+ } else {
163
+ logger.warn("xray-mapping.json already exists — skipping");
164
+ skipped++;
165
+ }
166
+
167
+ // ── playwright.config.ts ──────────────────────────────────────────────────
168
+ const playwrightConfigDest = path.join(cwd, "playwright.config.ts");
169
+ if (!fs.existsSync(playwrightConfigDest)) {
170
+ fs.writeFileSync(playwrightConfigDest, generatePlaywrightConfig(serviceName));
171
+ logger.success("playwright.config.ts created — Playwright config (JSON + JUnit reporters)");
172
+ created++;
173
+ } else {
174
+ logger.warn("playwright.config.ts already exists — skipping");
175
+ skipped++;
97
176
  }
98
177
 
99
- // Copy .env.example from package root
178
+ // ── .env.example ─────────────────────────────────────────────────────────
100
179
  const envExampleSrc = path.join(__dirname, "..", ".env.example");
101
180
  const envExampleDest = path.join(cwd, ".env.example");
102
181
  if (!fs.existsSync(envExampleDest)) {
103
182
  fs.copyFileSync(envExampleSrc, envExampleDest);
104
- logger.success(".env.example created");
183
+ logger.success(".env.example created — Credential template");
105
184
  created++;
106
185
  } else {
107
186
  logger.warn(".env.example already exists — skipping");
108
187
  skipped++;
109
188
  }
110
189
 
111
- // Create .xrayrc if it doesn't exist
190
+ // ── .xrayrc ───────────────────────────────────────────────────────────────
112
191
  const xrayrcPath = path.join(cwd, ".xrayrc");
113
192
  if (!fs.existsSync(xrayrcPath)) {
114
193
  const xrayrc = {
115
194
  testsPath: "tests.json",
116
195
  mappingPath: "xray-mapping.json",
117
- playwrightResultsPath: "playwright-results.json",
118
- knowledgePath: "knowledge",
119
- playwrightDir: "playwright-tests",
196
+ resourcesPath: "resources",
197
+ playwrightDir: "tests",
198
+ testPlanKey: "",
199
+ defaultEnvironment: "IOP-DEV",
200
+ environments: ["IOP-DEV", "IOP-QA", "IOP-PROD"],
201
+ folderRoot: `/${serviceName}`,
202
+ xrayRegion: "us",
120
203
  };
121
204
  fs.writeFileSync(xrayrcPath, JSON.stringify(xrayrc, null, 2));
122
- logger.success(".xrayrc created (project config)");
205
+ logger.success(".xrayrc created Project config");
123
206
  created++;
124
207
  } else {
125
208
  logger.warn(".xrayrc already exists — skipping");
126
209
  skipped++;
127
210
  }
128
211
 
129
- logger.success(`${created} file(s)/folder(s) created, ${skipped} skipped\n`);
130
-
131
- // Print next steps
132
- console.log("📖 Next steps:");
133
- console.log(" 1. Copy .env.example .env and fill in your credentials");
134
- console.log(" 2. Add API specs and docs to knowledge/ folder (see knowledge/README.md)");
135
- console.log(" 3. See XQT-GUIDE.md for the full workflow and command reference");
136
- console.log(" 4. Generate test cases: npx xray-qe gen-tests --ai (or edit tests.json manually)");
137
- console.log(" 5. Review tests: npx xray-qe edit-json");
138
- console.log(" 6. Push tests to Xray: npx xray-qe push-tests");
139
- console.log(" 7. Generate CI pipeline: npx xqt gen-pipeline");
140
- console.log(" 8. Run Playwright tests and import results: npx xqt import-results --file playwright-results.json");
141
- console.log("");
212
+ // ── XQT-GUIDE.md ─────────────────────────────────────────────────────────
213
+ const guideDest = path.join(cwd, "XQT-GUIDE.md");
214
+ const guideSrc = path.join(TEMPLATES, "README.template.md");
215
+ if (!fs.existsSync(guideDest)) {
216
+ let guideContent = fs.readFileSync(guideSrc, "utf8");
217
+ // Replace {{SERVICE_NAME}} placeholder
218
+ guideContent = guideContent.replace(/\{\{SERVICE_NAME\}\}/g, serviceName);
219
+ fs.writeFileSync(guideDest, guideContent);
220
+ logger.success("XQT-GUIDE.md created Comprehensive usage guide");
221
+ created++;
222
+ } else {
223
+ logger.warn("XQT-GUIDE.md already exists skipping");
224
+ skipped++;
225
+ }
226
+ // ── SPEC-DRIVEN-APPROACH.md ───────────────────────────────────────────────
227
+ const specDrivenDest = path.join(cwd, "SPEC-DRIVEN-APPROACH.md");
228
+ const specDrivenSrc = path.join(TEMPLATES, "SPEC-DRIVEN-APPROACH.md");
229
+ if (!fs.existsSync(specDrivenDest)) {
230
+ let specContent = fs.readFileSync(specDrivenSrc, "utf8");
231
+ specContent = specContent.replace(/\{\{SERVICE_NAME\}\}/g, serviceName);
232
+ fs.writeFileSync(specDrivenDest, specContent);
233
+ logger.success("SPEC-DRIVEN-APPROACH.md created — QE process & workflow guide");
234
+ created++;
235
+ } else {
236
+ logger.warn("SPEC-DRIVEN-APPROACH.md already exists — skipping");
237
+ skipped++;
238
+ }
239
+
240
+ // ── azure-pipelines.yml ───────────────────────────────────────────────────
241
+ const pipelineDest = path.join(cwd, "azure-pipelines.yml");
242
+ const pipelineSrc = path.join(TEMPLATES, "azure-pipelines.yml");
243
+ if (!fs.existsSync(pipelineDest)) {
244
+ let pipelineContent = fs.readFileSync(pipelineSrc, "utf8");
245
+ pipelineContent = pipelineContent.replace(/\{\{SERVICE_NAME\}\}/g, serviceName);
246
+ fs.writeFileSync(pipelineDest, pipelineContent);
247
+ logger.success(`azure-pipelines.yml created — ADO QA pipeline (variable group: ${serviceName}-qa)`);
248
+ created++;
249
+ } else {
250
+ logger.warn("azure-pipelines.yml already exists — skipping");
251
+ skipped++;
252
+ }
253
+
254
+ // ── Summary ───────────────────────────────────────────────────────────────
255
+ logger.blank();
256
+ logger.success(`Init complete: ${created} item(s) created, ${skipped} skipped`);
257
+ logger.blank();
258
+
259
+ console.log("Next steps:");
260
+ console.log(" 1. Copy .env.example → .env and fill in your credentials for local development");
261
+ console.log(" (XRAY_ID, XRAY_SECRET, JIRA_PROJECT_KEY, JIRA_URL, JIRA_API_TOKEN, JIRA_EMAIL)");
262
+ console.log(" 2. Add your OpenAPI spec to resources/openapi.yaml");
263
+ console.log(" 3. Define business rules in resources/business-rules.yaml");
264
+ console.log(" 4. In ADO Library, ensure these variable groups exist:");
265
+ console.log(` - xray-qa-shared (shared): xray_client_id, xray_client_secret,`);
266
+ console.log(" JIRA_PROJECT_KEY, JIRA_URL,");
267
+ console.log(" JIRA_API_TOKEN, JIRA_EMAIL,");
268
+ console.log(" XRAY_GRAPHQL_URL_US, XRAY_REST_URL");
269
+ console.log(` - ${serviceName}-qa (per-repo): base_url, gravitee_api_key`);
270
+ console.log(" 5. Register azure-pipelines.yml in ADO Pipelines (trigger: manual for regression)");
271
+ console.log(" 6. Create a Test Plan: npx xqt create-plan --summary 'My Service Tests'");
272
+ console.log(" then update the testPlanKey in .xrayrc");
273
+ console.log(" 7. Use Copilot + SPEC-DRIVEN-APPROACH.md to generate tests from your spec files");
274
+ console.log(" 8. Push tests to Xray: npx xqt push-tests");
275
+ console.log(" 9. Run tests and import results: npx xqt import-results --file test-results/results.json");
276
+ console.log(" 📖 See XQT-GUIDE.md for the xqt command reference");
277
+ console.log(" 📋 See SPEC-DRIVEN-APPROACH.md for the full QE process\n");
278
+ }
279
+
280
+ // ─── Generators ───────────────────────────────────────────────────────────────
281
+
282
+ function generatePlaywrightConfig(serviceName) {
283
+ return `import { defineConfig, devices } from '@playwright/test';
284
+ import * as path from 'path';
285
+
286
+ /**
287
+ * Playwright configuration for ${serviceName}
288
+ * Generated by @msalaam/xray-qe-toolkit
289
+ *
290
+ * Reporters:
291
+ * - JSON → test-results/results.json (consumed by xqt import-results)
292
+ * - JUnit XML → test-results/results.xml (consumed by xqt import-results --file results.xml)
293
+ * - HTML → playwright-report/ (artifact in CI)
294
+ */
295
+ export default defineConfig({
296
+ testDir: './tests/specs',
297
+ outputDir: './test-results/artifacts',
298
+ timeout: 30000,
299
+ retries: process.env.CI ? 1 : 0,
300
+ workers: process.env.CI ? 2 : undefined,
301
+ fullyParallel: false,
302
+
303
+ reporter: [
304
+ ['json', { outputFile: 'test-results/results.json' }],
305
+ ['junit', { outputFile: 'test-results/results.xml' }],
306
+ ['html', { outputFolder: 'playwright-report', open: 'never' }],
307
+ ],
308
+
309
+ use: {
310
+ baseURL: process.env.BASE_URL || 'http://localhost:3000',
311
+ trace: 'retain-on-failure',
312
+ screenshot: 'only-on-failure',
313
+ },
314
+
315
+ projects: [
316
+ {
317
+ name: 'api',
318
+ use: {
319
+ ...devices['Desktop Chrome'],
320
+ },
321
+ },
322
+ ],
323
+ });
324
+ `;
142
325
  }
326
+
327
+
328
+
329
+
330
+