@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.
- package/.env.example +23 -9
- package/README.md +721 -1322
- package/bin/cli.js +97 -51
- package/commands/createExecution.js +112 -23
- package/commands/createPlan.js +121 -0
- package/commands/editJson.js +1 -1
- package/commands/genPipeline.js +1 -1
- package/commands/genTests.js +18 -18
- package/commands/importResults.js +107 -74
- package/commands/init.js +258 -70
- package/commands/pullTests.js +128 -0
- package/commands/pushTests.js +87 -43
- package/commands/status.js +108 -0
- package/commands/syncFolders.js +62 -0
- package/commands/validate.js +136 -0
- package/lib/config.js +50 -13
- package/lib/index.js +43 -4
- package/lib/playwrightConverter.js +91 -173
- package/lib/testCaseBuilder.js +116 -55
- package/lib/xrayClient.js +779 -202
- package/package.json +5 -4
- package/schema/business-rules.schema.json +110 -0
- package/schema/tests.schema.json +42 -16
- package/templates/README.template.md +570 -158
- package/templates/SPEC-DRIVEN-APPROACH.md +372 -0
- package/templates/azure-pipelines.yml +129 -77
- package/templates/business-rules.yaml +83 -0
- package/templates/resources-README.md +112 -0
- package/templates/tests.json +69 -51
- package/commands/genPostman.js +0 -70
- package/lib/postmanGenerator.js +0 -304
- package/templates/knowledge-README.md +0 -121
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Command: xqt import-results --file <path>
|
|
2
|
+
* Command: xqt import-results --file <path> --env <environment>
|
|
3
3
|
*
|
|
4
|
-
* Imports test results into Xray Cloud
|
|
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
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* - Playwright JSON (.json) — Playwright JSON reporter output
|
|
9
|
+
* - JUnit XML (.xml) — Standard JUnit / XUnit format
|
|
9
10
|
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
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 ===
|
|
50
|
-
// Playwright JSON
|
|
51
|
-
logger.send(
|
|
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,
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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(
|
|
67
|
-
logger.info(
|
|
68
|
-
logger.info(
|
|
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
|
|
73
|
-
const
|
|
114
|
+
const withKeys = xrayJson.tests.filter((t) => t.testKey).length;
|
|
115
|
+
const withoutKeys = xrayJson.tests.length - withKeys;
|
|
74
116
|
|
|
75
|
-
logger.step(
|
|
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(
|
|
120
|
+
const debugPath = filePath.replace(".json", "-xray-debug.json");
|
|
86
121
|
fs.writeFileSync(debugPath, JSON.stringify(xrayJson, null, 2));
|
|
87
|
-
logger.info(`
|
|
122
|
+
logger.info(`Debug Xray JSON saved to ${path.basename(debugPath)}`);
|
|
88
123
|
}
|
|
89
124
|
logger.blank();
|
|
90
125
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
96
|
-
logger.send(
|
|
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,
|
|
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
|
-
//
|
|
106
|
-
if (
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
logger.
|
|
110
|
-
logger.
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 {
|
|
136
|
-
* @returns {
|
|
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
|
|
171
|
+
function summarise(tests) {
|
|
139
172
|
return {
|
|
140
|
-
passed: tests.filter(t => t.status ===
|
|
141
|
-
failed: tests.filter(t => t.status ===
|
|
142
|
-
skipped: tests.filter(t => t.status ===
|
|
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:
|
|
2
|
+
* Command: xqt init
|
|
3
3
|
*
|
|
4
|
-
* Scaffolds a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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("
|
|
99
|
+
logger.warn(".npmrc already exists — skipping");
|
|
59
100
|
skipped++;
|
|
60
101
|
}
|
|
61
102
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
if (!fs.existsSync(
|
|
66
|
-
fs.
|
|
67
|
-
|
|
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("
|
|
118
|
+
logger.warn("resources/api-specs/openapi.yaml already exists — skipping");
|
|
71
119
|
skipped++;
|
|
72
120
|
}
|
|
73
121
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
|