@msalaam/xray-qe-toolkit 1.3.3 → 1.4.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/README.md CHANGED
@@ -22,6 +22,8 @@
22
22
  - [import-results](#xray-qe-import-results)
23
23
  - [gen-pipeline](#xray-qe-gen-pipeline)
24
24
  - [mcp-server](#xray-qe-mcp-server)
25
+ - [compare-openapi](#xray-qe-compare-openapi)
26
+ - [update-snapshot](#xray-qe-update-snapshot)
25
27
  - [Configuration](#configuration)
26
28
  - [Environment Variables (.env)](#environment-variables-env)
27
29
  - [Project Config (.xrayrc)](#project-config-xrayrc)
@@ -49,8 +51,9 @@
49
51
  4. **Postman collection generation** from test definitions
50
52
  5. **CI pipeline template** for Azure DevOps (Newman + Xray import)
51
53
  6. **Playwright integration** — import Playwright JSON results with automatic test mapping
52
- 7. **Modular architecture** — every function is importable for programmatic use
53
- 8. **AI-ready scaffolds** — optional AI assistance for test generation (manual workflow fully supported)
54
+ 7. **OpenAPI contract enforcement** — `compare-openapi` diffs a live spec against a QA snapshot and fails the pipeline on breaking changes; `update-snapshot` promotes a new baseline deliberately
55
+ 8. **Modular architecture** — every function is importable for programmatic use
56
+ 9. **AI-ready scaffolds** — optional AI assistance for test generation (manual workflow fully supported)
54
57
 
55
58
  ### QE Review Gate Philosophy
56
59
 
@@ -559,6 +562,100 @@ npx xqt mcp-server --port 3100
559
562
 
560
563
  ---
561
564
 
565
+ ### `xqt compare-openapi`
566
+
567
+ Compare an OpenAPI spec against an approved QA snapshot and fail the pipeline if breaking changes are detected.
568
+
569
+ ```bash
570
+ # Basic comparison — exits 1 on breaking changes
571
+ npx xqt compare-openapi \
572
+ --current ../api-repo/openapi.yaml \
573
+ --snapshot ./openapi.snapshot.yaml
574
+
575
+ # Save a JSON diff report as a pipeline artifact
576
+ npx xqt compare-openapi \
577
+ --current ../api-repo/openapi.yaml \
578
+ --snapshot ./openapi.snapshot.yaml \
579
+ --report openapi-diff-report.json
580
+ ```
581
+
582
+ | Option | Description | Required |
583
+ |----------------------|------------------------------------------------------------------|----------|
584
+ | `--current <path>` | Path to the current (live) OpenAPI spec | Yes |
585
+ | `--snapshot <path>` | Path to the approved QA baseline snapshot | Yes |
586
+ | `--report <path>` | Write full diff results to a JSON file (useful as CI artifact) | No |
587
+
588
+ **Behaviour:**
589
+ - Snapshot = approved QA contract baseline
590
+ - Current = proposed/live spec
591
+ - Exits `0` if no breaking changes; logs a count of non-breaking differences
592
+ - Exits `1` on any breaking change and prints the full diff
593
+ - If `--report` is specified and breaking changes are found, a JSON report is written
594
+
595
+ **Example Azure Pipelines step (from your test repo pipeline):**
596
+
597
+ ```yaml
598
+ steps:
599
+ - checkout: self
600
+
601
+ - checkout: git://Project/portfolio-api
602
+ path: api-repo
603
+
604
+ - script: |
605
+ npx xqt compare-openapi \
606
+ --current api-repo/openapi.yaml \
607
+ --snapshot openapi.snapshot.yaml \
608
+ --report openapi-diff-report.json
609
+ displayName: Compare OpenAPI Contracts
610
+
611
+ - publish: openapi-diff-report.json
612
+ artifact: openapi-diff
613
+ condition: failed()
614
+ ```
615
+
616
+ **Governance model:**
617
+ - Dev repo is never modified by QE
618
+ - QE test repo pipeline checks out the API repo and enforces the contract
619
+ - Breaking change → pipeline fails → dev must fix spec or raise a contract change review
620
+ - QE then runs `update-snapshot` to promote the new baseline
621
+
622
+ ---
623
+
624
+ ### `xqt update-snapshot`
625
+
626
+ Overwrite the QA snapshot baseline with the current spec. **Always a deliberate, manual action — never called automatically.**
627
+
628
+ ```bash
629
+ npx xqt update-snapshot \
630
+ --current ../api-repo/openapi.yaml \
631
+ --snapshot ./openapi.snapshot.yaml
632
+ ```
633
+
634
+ | Option | Description | Required |
635
+ |----------------------|-----------------------------------------------------|----------|
636
+ | `--current <path>` | Path to the current (live) OpenAPI spec to promote | Yes |
637
+ | `--snapshot <path>` | Path to the snapshot file to overwrite | Yes |
638
+
639
+ **Workflow:**
640
+ 1. Dev raises a contract change review
641
+ 2. QE approves the new contract
642
+ 3. QE runs `update-snapshot` locally
643
+ 4. QE raises a PR in the test repo with the updated snapshot
644
+ 5. PR is reviewed and merged — new baseline is established
645
+
646
+ ```bash
647
+ # After merging the approved contract change:
648
+ npx xqt update-snapshot \
649
+ --current ../api-repo/openapi.yaml \
650
+ --snapshot ./openapi.snapshot.yaml
651
+
652
+ git add openapi.snapshot.yaml
653
+ git commit -m "chore: promote OpenAPI snapshot to v2.5.0"
654
+ git push
655
+ ```
656
+
657
+ ---
658
+
562
659
  ## Configuration
563
660
 
564
661
  ### Environment Variables (.env)
@@ -754,6 +851,21 @@ Use this checklist to align a new team's board and Xray configuration with the t
754
851
  │ 10. newman run collection.postman.json --reporters junit │
755
852
  │ 11. npx xqt import-results --file results.xml --testExecKey QE-123│
756
853
  │ │
854
+ ├─────────────────────────────────────────────────────────────────────────┤
855
+ │ CONTRACT ENFORCEMENT (TEST REPO PIPELINE) │
856
+ │ │
857
+ │ 12. Checkout API repo │
858
+ │ 13. npx xqt compare-openapi │
859
+ │ --current api-repo/openapi.yaml │
860
+ │ --snapshot openapi.snapshot.yaml │
861
+ │ → Fails pipeline on breaking changes │
862
+ │ │
863
+ │ (When QE approves a contract change) │
864
+ │ 14. npx xqt update-snapshot │
865
+ │ --current api-repo/openapi.yaml │
866
+ │ --snapshot openapi.snapshot.yaml │
867
+ │ → Commit updated snapshot + raise PR │
868
+ │ │
757
869
  └─────────────────────────────────────────────────────────────────────────┘
758
870
  ```
759
871
 
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * @msalaam/xray-qe-toolkit — CLI Entry Point
@@ -16,6 +16,8 @@
16
16
  * import-results Import test results (JUnit XML or Playwright JSON) into Xray
17
17
  * gen-pipeline Generate an Azure Pipelines YAML template
18
18
  * mcp-server Start Model Context Protocol server for agent integration
19
+ * compare-openapi Compare OpenAPI specs and fail if breaking changes are detected
20
+ * update-snapshot Overwrite the QA snapshot baseline with the current spec
19
21
  */
20
22
 
21
23
  import { Command } from "commander";
@@ -131,6 +133,27 @@ program
131
133
  await mod.default({ ...opts, ...cmd.optsWithGlobals() });
132
134
  });
133
135
 
136
+ program
137
+ .command("compare-openapi")
138
+ .description("Compare OpenAPI specs against a QA snapshot and fail if breaking changes are detected")
139
+ .requiredOption("--current <path>", "Path to the current (live) OpenAPI spec")
140
+ .requiredOption("--snapshot <path>", "Path to the approved QA snapshot")
141
+ .option("--report <path>", "Write a JSON diff report to this path (useful as a pipeline artifact)")
142
+ .action(async (opts, cmd) => {
143
+ const mod = await import("../commands/compareOpenapi.js");
144
+ await mod.default({ ...opts, ...cmd.optsWithGlobals() });
145
+ });
146
+
147
+ program
148
+ .command("update-snapshot")
149
+ .description("Overwrite the QA snapshot baseline with the current spec (always a deliberate, manual action)")
150
+ .requiredOption("--current <path>", "Path to the current (live) OpenAPI spec to promote")
151
+ .requiredOption("--snapshot <path>", "Path to the snapshot file to overwrite")
152
+ .action(async (opts, cmd) => {
153
+ const mod = await import("../commands/updateSnapshot.js");
154
+ await mod.default({ ...opts, ...cmd.optsWithGlobals() });
155
+ });
156
+
134
157
  // ─── Parse & run ───────────────────────────────────────────────────────────────
135
158
 
136
159
  program.parseAsync(process.argv).catch((err) => {
@@ -0,0 +1,78 @@
1
+ import { diff } from 'openapi-diff';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Compare two OpenAPI specs and detect breaking changes.
7
+ * @param {string} currentPath - Path to the current (live) OpenAPI spec.
8
+ * @param {string} snapshotPath - Path to the approved QA snapshot.
9
+ * @param {string|null} reportPath - Optional path to write a JSON diff report.
10
+ */
11
+ export async function compareOpenApiSpecs(currentPath, snapshotPath, reportPath = null) {
12
+ const resolvedCurrent = path.resolve(currentPath);
13
+ const resolvedSnapshot = path.resolve(snapshotPath);
14
+
15
+ if (!fs.existsSync(resolvedCurrent)) {
16
+ throw new Error(`Current OpenAPI spec not found: ${resolvedCurrent}`);
17
+ }
18
+
19
+ if (!fs.existsSync(resolvedSnapshot)) {
20
+ throw new Error(`Snapshot OpenAPI spec not found: ${resolvedSnapshot}`);
21
+ }
22
+
23
+ const currentSpec = fs.readFileSync(resolvedCurrent, 'utf-8');
24
+ const snapshotSpec = fs.readFileSync(resolvedSnapshot, 'utf-8');
25
+
26
+ const result = await diff({
27
+ sourceSpec: { content: snapshotSpec }, // approved baseline
28
+ destinationSpec: { content: currentSpec } // proposed change
29
+ });
30
+
31
+ const summary = {
32
+ breaking: result.breakingDifferencesFound,
33
+ breakingDifferences: result.breakingDifferences,
34
+ nonBreakingDifferences: result.nonBreakingDifferences
35
+ };
36
+
37
+ if (reportPath && result.breakingDifferencesFound) {
38
+ const resolvedReport = path.resolve(reportPath);
39
+ fs.writeFileSync(resolvedReport, JSON.stringify(summary, null, 2), 'utf-8');
40
+ console.log(`📄 Diff report written to: ${resolvedReport}`);
41
+ }
42
+
43
+ return summary;
44
+ }
45
+
46
+ export default async function compareOpenapi(opts) {
47
+ const { current, snapshot, report } = opts;
48
+
49
+ try {
50
+ const result = await compareOpenApiSpecs(current, snapshot, report || null);
51
+
52
+ if (result.breaking) {
53
+ console.error('\n❌ Breaking changes detected:\n');
54
+ console.error(JSON.stringify(result.breakingDifferences, null, 2));
55
+
56
+ const count = result.breakingDifferences?.length ?? 0;
57
+ console.error(`\n${count} breaking difference(s) found.`);
58
+
59
+ if (!report) {
60
+ console.error('\nTip: use --report <path> to save a full diff report as a pipeline artifact.');
61
+ }
62
+
63
+ process.exit(1);
64
+ }
65
+
66
+ const nonBreaking = result.nonBreakingDifferences?.length ?? 0;
67
+ console.log('\n✅ No breaking changes detected.');
68
+
69
+ if (nonBreaking > 0) {
70
+ console.log(`ℹ️ ${nonBreaking} non-breaking difference(s) found (safe to proceed).`);
71
+ }
72
+
73
+ process.exit(0);
74
+ } catch (err) {
75
+ console.error(`\n❌ ${err.message}`);
76
+ process.exit(1);
77
+ }
78
+ }
@@ -2,7 +2,7 @@
2
2
  * Command: xray-qe gen-pipeline [--output <path>]
3
3
  *
4
4
  * Copies the Azure Pipelines YAML template into the consuming project.
5
- * The template runs Newman and imports results — no QE logic in CI.
5
+ * The template runs Playwright and imports JSON results — no QE logic in CI.
6
6
  */
7
7
 
8
8
  import fs from "node:fs";
@@ -34,8 +34,11 @@ export default async function genPipeline(opts = {}) {
34
34
  console.log(" 1. Set pipeline variables in Azure DevOps:");
35
35
  console.log(" XRAY_ID, XRAY_SECRET, JIRA_PROJECT_KEY, JIRA_URL,");
36
36
  console.log(" JIRA_API_TOKEN, JIRA_EMAIL, TEST_EXEC_KEY");
37
- console.log(" 2. Commit the generated file to your repo");
38
- console.log(" 3. Create a pipeline in Azure DevOps pointing to this YAML");
37
+ console.log(" 2. Optionally set API_BASE_URL for Playwright tests");
38
+ console.log(" 3. Commit the generated file to your repo");
39
+ console.log(" 4. Create a pipeline in Azure DevOps pointing to this YAML");
40
+ console.log(" 5. Playwright tests must have xray annotations to map results:");
41
+ console.log(" test.info().annotations.push({ type: 'xray', description: 'PROJ-123' })");
39
42
  console.log("");
40
43
  console.log("⚠️ Remember: edit-json and QE review gates are NOT part of CI.");
41
44
  console.log(" Run those steps locally before committing.\n");
package/commands/init.js CHANGED
@@ -114,7 +114,7 @@ export default async function init(opts = {}) {
114
114
  const xrayrc = {
115
115
  testsPath: "tests.json",
116
116
  mappingPath: "xray-mapping.json",
117
- collectionPath: "collection.postman.json",
117
+ playwrightResultsPath: "playwright-results.json",
118
118
  knowledgePath: "knowledge",
119
119
  playwrightDir: "playwright-tests",
120
120
  };
@@ -136,7 +136,7 @@ export default async function init(opts = {}) {
136
136
  console.log(" 4. Generate test cases: npx xray-qe gen-tests --ai (or edit tests.json manually)");
137
137
  console.log(" 5. Review tests: npx xray-qe edit-json");
138
138
  console.log(" 6. Push tests to Xray: npx xray-qe push-tests");
139
- console.log(" 7. Generate Postman collection: npx xray-qe gen-postman --ai");
140
- console.log(" 8. Generate CI pipeline: npx xray-qe gen-pipeline");
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
141
  console.log("");
142
142
  }
@@ -0,0 +1,34 @@
1
+ import fse from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Copy the current OpenAPI spec over the snapshot baseline.
6
+ * This is intentionally a manual, explicit action — never called automatically.
7
+ *
8
+ * @param {string} currentPath - Path to the current (live) OpenAPI spec.
9
+ * @param {string} snapshotPath - Path to the snapshot file to overwrite.
10
+ */
11
+ export async function updateSnapshot(currentPath, snapshotPath) {
12
+ const resolvedCurrent = path.resolve(currentPath);
13
+ const resolvedSnapshot = path.resolve(snapshotPath);
14
+
15
+ if (!(await fse.pathExists(resolvedCurrent))) {
16
+ throw new Error(`Current OpenAPI spec not found: ${resolvedCurrent}`);
17
+ }
18
+
19
+ await fse.copy(resolvedCurrent, resolvedSnapshot, { overwrite: true });
20
+ console.log(`✅ Snapshot updated: ${resolvedSnapshot}`);
21
+ console.log(` Source: ${resolvedCurrent}`);
22
+ }
23
+
24
+ export default async function updateSnapshotCmd(opts) {
25
+ const { current, snapshot } = opts;
26
+
27
+ try {
28
+ await updateSnapshot(current, snapshot);
29
+ console.log('\nBaseline established. Commit the updated snapshot to your test repo to record the new contract.');
30
+ } catch (err) {
31
+ console.error(`\n❌ ${err.message}`);
32
+ process.exit(1);
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@msalaam/xray-qe-toolkit",
3
- "version": "1.3.3",
4
- "description": "Full QE workflow toolkit for Xray Cloud integration — test management, Postman generation, CI pipeline scaffolding, and browser-based review gates for API regression projects.",
3
+ "version": "1.4.0",
4
+ "description": "Full QE workflow toolkit for Xray Cloud integration — test management, Playwright integration, CI pipeline scaffolding, and browser-based review gates for API regression projects.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "xqt": "./bin/cli.js"
@@ -35,7 +35,10 @@
35
35
  "commander": "^13.1.0",
36
36
  "dotenv": "^17.2.3",
37
37
  "express": "^5.1.0",
38
- "open": "^10.1.2"
38
+ "fs-extra": "^11.3.3",
39
+ "open": "^10.1.2",
40
+ "openapi-diff": "^0.24.1",
41
+ "yaml": "^2.8.2"
39
42
  },
40
43
  "devDependencies": {
41
44
  "ajv": "^8.17.1"
@@ -63,13 +63,7 @@ npx xqt edit-json
63
63
  npx xqt push-tests
64
64
  ```
65
65
 
66
- ### 5. Generate Postman Collection
67
-
68
- ```bash
69
- npx xqt gen-postman
70
- ```
71
-
72
- ### 6. Set Up CI Pipeline
66
+ ### 5. Set Up CI Pipeline
73
67
 
74
68
  ```bash
75
69
  # Generate Azure Pipelines template
@@ -84,10 +78,11 @@ npx xqt gen-pipeline
84
78
  | `npx xqt gen-tests` | Generate test cases from knowledge/ |
85
79
  | `npx xqt edit-json` | Review/edit tests in browser |
86
80
  | `npx xqt push-tests` | Push tests to Xray Cloud |
87
- | `npx xqt gen-postman` | Generate Postman collection |
88
81
  | `npx xqt gen-pipeline` | Generate CI pipeline template |
89
82
  | `npx xqt create-execution` | Create Test Execution issue |
90
- | `npx xqt import-results` | Import test results (CI) |
83
+ | `npx xqt import-results` | Import Playwright JSON results (CI) |
84
+ | `npx xqt compare-openapi` | Diff live spec vs QA snapshot — fails on breaking changes |
85
+ | `npx xqt update-snapshot` | Promote current spec as new QA baseline (manual only) |
91
86
 
92
87
  Run any command with `--help` for details:
93
88
  ```bash
@@ -101,8 +96,18 @@ npx xqt gen-tests --help
101
96
  2. Generate tests (AI or manual)
102
97
  3. Review in edit-json
103
98
  4. Push to Xray
104
- 5. Generate Postman collection
105
- 6. Run in CI (newman + import-results)
99
+ 5. Generate CI pipeline: npx xqt gen-pipeline
100
+ 6. Run Playwright tests in CI
101
+ 7. Import results: npx xqt import-results --file playwright-results.json
102
+
103
+ Contract Enforcement (from your test repo pipeline):
104
+ 8. Checkout API repo in pipeline
105
+ 9. npx xqt compare-openapi --current api-repo/openapi.yaml --snapshot openapi.snapshot.yaml
106
+ → Fail pipeline on breaking changes
107
+
108
+ When a contract change is approved:
109
+ 10. npx xqt update-snapshot --current api-repo/openapi.yaml --snapshot openapi.snapshot.yaml
110
+ → Commit updated snapshot + raise PR to establish new baseline
106
111
  ```
107
112
 
108
113
  ## Files
@@ -111,7 +116,7 @@ npx xqt gen-tests --help
111
116
  |------|---------|
112
117
  | `tests.json` | Test definitions (source of truth) |
113
118
  | `xray-mapping.json` | Maps test IDs to JIRA keys |
114
- | `collection.postman.json` | Generated Postman collection |
119
+ | `playwright-results.json` | Playwright JSON report (CI output) |
115
120
  | `.env` | Credentials (DO NOT COMMIT) |
116
121
  | `.xrayrc` | Project config |
117
122
  | `knowledge/` | API specs and requirements |
@@ -3,8 +3,8 @@
3
3
  # Generated by @msalaam/xray-qe-toolkit
4
4
  # ──────────────────────────────────────────────────────────────
5
5
  #
6
- # This pipeline runs Newman against the generated Postman collection
7
- # and imports results into Xray Cloud.
6
+ # This pipeline runs Playwright tests and imports the JSON results
7
+ # into Xray Cloud via xqt import-results.
8
8
  #
9
9
  # IMPORTANT: edit-json and QE review gates are NOT part of CI.
10
10
  # Those steps must be run locally before committing.
@@ -12,6 +12,9 @@
12
12
  # Required pipeline variables (set in Azure DevOps UI → Variables):
13
13
  # XRAY_ID, XRAY_SECRET, JIRA_PROJECT_KEY, JIRA_URL,
14
14
  # JIRA_API_TOKEN, JIRA_EMAIL, TEST_EXEC_KEY
15
+ #
16
+ # Optional pipeline variables:
17
+ # API_BASE_URL — base URL passed to Playwright tests
15
18
  # ──────────────────────────────────────────────────────────────
16
19
 
17
20
  trigger:
@@ -24,7 +27,7 @@ pool:
24
27
  vmImage: "ubuntu-latest"
25
28
 
26
29
  variables:
27
- NODE_VERSION: "18.x"
30
+ NODE_VERSION: "20.x"
28
31
 
29
32
  steps:
30
33
  - task: NodeTool@0
@@ -37,17 +40,24 @@ steps:
37
40
  displayName: "Install dependencies"
38
41
 
39
42
  - script: |
40
- npx newman run collection.postman.json \
41
- --reporters cli,junit \
42
- --reporter-junit-export results.xml \
43
- --suppress-exit-code
44
- displayName: "Run Newman tests"
43
+ npx playwright install --with-deps chromium
44
+ displayName: "Install Playwright browsers"
45
+
46
+ - script: |
47
+ npx playwright test \
48
+ --reporter=list,json \
49
+ --output=playwright-results
50
+ displayName: "Run Playwright tests"
51
+ continueOnError: true
52
+ env:
53
+ PLAYWRIGHT_JSON_OUTPUT_NAME: playwright-results.json
54
+ API_BASE_URL: $(API_BASE_URL)
45
55
 
46
56
  - script: |
47
57
  npx xqt import-results \
48
- --file results.xml \
58
+ --file playwright-results.json \
49
59
  --testExecKey $(TEST_EXEC_KEY)
50
- displayName: "Import results to Xray"
60
+ displayName: "Import Playwright results to Xray"
51
61
  env:
52
62
  XRAY_ID: $(XRAY_ID)
53
63
  XRAY_SECRET: $(XRAY_SECRET)
@@ -57,9 +67,18 @@ steps:
57
67
  JIRA_EMAIL: $(JIRA_EMAIL)
58
68
 
59
69
  - task: PublishTestResults@2
60
- displayName: "Publish JUnit results"
70
+ displayName: "Publish Playwright JUnit results"
71
+ condition: always()
61
72
  inputs:
62
73
  testResultsFormat: "JUnit"
63
- testResultsFiles: "results.xml"
74
+ testResultsFiles: "playwright-results/results.xml"
64
75
  mergeTestResults: true
65
76
  failTaskOnFailedTests: true
77
+
78
+ - task: PublishPipelineArtifact@1
79
+ displayName: "Upload Playwright HTML report"
80
+ condition: always()
81
+ inputs:
82
+ targetPath: "playwright-report"
83
+ artifact: "playwright-report"
84
+ publishLocation: "pipeline"