@keber/qa-framework 1.0.4
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/CHANGELOG.md +53 -0
- package/README.md +233 -0
- package/agent-instructions/00-module-analysis.md +263 -0
- package/agent-instructions/01-spec-generation.md +278 -0
- package/agent-instructions/02-test-plan-generation.md +202 -0
- package/agent-instructions/03-test-case-generation.md +147 -0
- package/agent-instructions/04-automation-generation.md +310 -0
- package/agent-instructions/04b-test-stabilization.md +306 -0
- package/agent-instructions/05-ado-integration.md +244 -0
- package/agent-instructions/06-maintenance.md +125 -0
- package/docs/architecture.md +227 -0
- package/docs/comparison-matrix.md +131 -0
- package/docs/final-report.md +279 -0
- package/docs/folder-structure-guide.md +291 -0
- package/docs/generalization-decisions.md +203 -0
- package/docs/installation.md +239 -0
- package/docs/spec-driven-philosophy.md +170 -0
- package/docs/usage-with-agent.md +203 -0
- package/examples/module-example/README.md +34 -0
- package/examples/module-example/suppliers/00-inventory.md +56 -0
- package/examples/module-example/suppliers/suppliers-create.spec.ts +148 -0
- package/integrations/ado-powershell/README.md +75 -0
- package/integrations/ado-powershell/pipelines/azure-pipeline-qa.yml +133 -0
- package/integrations/ado-powershell/scripts/create-testplan-from-mapping.ps1 +114 -0
- package/integrations/ado-powershell/scripts/inject-ado-ids.ps1 +96 -0
- package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +93 -0
- package/integrations/playwright/README.md +68 -0
- package/integrations/playwright-azure-reporter/README.md +88 -0
- package/package.json +57 -0
- package/qa-framework.config.json +87 -0
- package/scripts/cli.js +74 -0
- package/scripts/generate.js +92 -0
- package/scripts/init.js +322 -0
- package/scripts/validate.js +184 -0
- package/templates/automation-scaffold/.env.example +56 -0
- package/templates/automation-scaffold/fixtures/auth.ts +77 -0
- package/templates/automation-scaffold/fixtures/test-helpers.ts +85 -0
- package/templates/automation-scaffold/global-setup.ts +106 -0
- package/templates/automation-scaffold/package.json +24 -0
- package/templates/automation-scaffold/playwright.config.ts +85 -0
- package/templates/defect-report.md +101 -0
- package/templates/execution-report.md +116 -0
- package/templates/session-summary.md +73 -0
- package/templates/specification/00-inventory.md +81 -0
- package/templates/specification/01-business-rules.md +90 -0
- package/templates/specification/02-workflows.md +114 -0
- package/templates/specification/03-roles-permissions.md +49 -0
- package/templates/specification/04-test-data.md +104 -0
- package/templates/specification/05-test-scenarios.md +226 -0
- package/templates/test-case.md +81 -0
- package/templates/test-plan.md +130 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/module-example/suppliers/suppliers-create.spec.ts
|
|
3
|
+
*
|
|
4
|
+
* Example Playwright E2E spec for the fictional "Suppliers" module.
|
|
5
|
+
* Demonstrates all required keber/qa-framework patterns:
|
|
6
|
+
* - EXEC_IDX for test data uniqueness
|
|
7
|
+
* - P0 priority tagging
|
|
8
|
+
* - Skip pattern with DEF reference
|
|
9
|
+
* - Fast-fail textContent assertions
|
|
10
|
+
* - Auth via storageState (set up by global-setup.ts)
|
|
11
|
+
*
|
|
12
|
+
* This file is for REFERENCE ONLY. Copy it, adapt selectors, and remove
|
|
13
|
+
* all fictional data before using in a real project.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { test, expect } from '@playwright/test';
|
|
17
|
+
|
|
18
|
+
// EXEC_IDX: unique per minute-window — prevents data collisions between runs
|
|
19
|
+
const EXEC_IDX = Math.floor(Date.now() / 60_000) % 100_000;
|
|
20
|
+
|
|
21
|
+
test.describe('Suppliers > Create @P0', () => {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* [TC-SUP-CR-001] Create supplier — happy path @P0
|
|
25
|
+
* Verifies the complete create flow for a valid supplier.
|
|
26
|
+
*/
|
|
27
|
+
test('[TC-SUP-CR-001] Create supplier — happy path @P0', async ({ page }) => {
|
|
28
|
+
const supplierName = `QA-Supplier-${EXEC_IDX}`;
|
|
29
|
+
const supplierRut = `${EXEC_IDX}-K`;
|
|
30
|
+
const supplierEmail = `qa-supplier-${EXEC_IDX}@example.com`;
|
|
31
|
+
|
|
32
|
+
// Navigate to suppliers module
|
|
33
|
+
await page.goto('/suppliers');
|
|
34
|
+
await expect(page.locator('.supplier-grid')).toBeVisible();
|
|
35
|
+
|
|
36
|
+
// Open create modal
|
|
37
|
+
await page.locator('button:has-text("New Supplier")').click();
|
|
38
|
+
await expect(page.locator('.supplier-modal')).toBeVisible();
|
|
39
|
+
|
|
40
|
+
// Fill form
|
|
41
|
+
await page.locator('#supplier-name').fill(supplierName);
|
|
42
|
+
await page.locator('#supplier-rut').fill(supplierRut);
|
|
43
|
+
await page.locator('#supplier-email').fill(supplierEmail);
|
|
44
|
+
await page.locator('#supplier-type').selectOption('Nacional');
|
|
45
|
+
|
|
46
|
+
// Save
|
|
47
|
+
await page.locator('button:has-text("Save")').click();
|
|
48
|
+
|
|
49
|
+
// Assert success
|
|
50
|
+
const toast = page.locator('.toast.toast-success');
|
|
51
|
+
await expect(toast).toBeVisible({ timeout: 5_000 });
|
|
52
|
+
const toastText = await toast.textContent();
|
|
53
|
+
expect(toastText).toContain('Supplier saved');
|
|
54
|
+
|
|
55
|
+
// Assert record appears in grid
|
|
56
|
+
await expect(page.locator(`.supplier-grid:has-text("${supplierName}")`)).toBeVisible();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* [TC-SUP-CR-002] Create supplier — required fields validation @P0
|
|
61
|
+
* Verifies that the form prevents submission when required fields are empty.
|
|
62
|
+
*/
|
|
63
|
+
test('[TC-SUP-CR-002] Create supplier — required fields validation @P0', async ({ page }) => {
|
|
64
|
+
await page.goto('/suppliers');
|
|
65
|
+
await page.locator('button:has-text("New Supplier")').click();
|
|
66
|
+
await expect(page.locator('.supplier-modal')).toBeVisible();
|
|
67
|
+
|
|
68
|
+
// Attempt save with empty form
|
|
69
|
+
await page.locator('button:has-text("Save")').click();
|
|
70
|
+
|
|
71
|
+
// Assert validation messages appear
|
|
72
|
+
await expect(page.locator('.field-error:has-text("Name is required")')).toBeVisible();
|
|
73
|
+
await expect(page.locator('.field-error:has-text("RUT is required")')).toBeVisible();
|
|
74
|
+
|
|
75
|
+
// Assert form was NOT submitted (modal still open)
|
|
76
|
+
await expect(page.locator('.supplier-modal')).toBeVisible();
|
|
77
|
+
const toast = page.locator('.toast.toast-success');
|
|
78
|
+
await expect(toast).not.toBeVisible();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* [TC-SUP-CR-003] Create supplier — duplicate RUT rejected @P0
|
|
83
|
+
*
|
|
84
|
+
* NOTE: test.skip active — DEF-001: Duplicate RUT check not enforced server-side.
|
|
85
|
+
* Reactivate when ADO #99001 is resolved.
|
|
86
|
+
*/
|
|
87
|
+
test('[TC-SUP-CR-003] Create supplier — duplicate RUT rejected @P0', async ({ page }) => {
|
|
88
|
+
test.skip(true,
|
|
89
|
+
'DEF-001: Duplicate RUT validation not enforced. Reactivate when ADO #99001 is resolved.'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// --- SKIPPED: steps below will not run until DEF-001 is fixed ---
|
|
93
|
+
await page.goto('/suppliers');
|
|
94
|
+
|
|
95
|
+
// Create first supplier
|
|
96
|
+
await page.locator('button:has-text("New Supplier")').click();
|
|
97
|
+
await page.locator('#supplier-name').fill(`QA-Dup-${EXEC_IDX}`);
|
|
98
|
+
await page.locator('#supplier-rut').fill('12345678-9');
|
|
99
|
+
await page.locator('#supplier-email').fill(`qa-dup-${EXEC_IDX}@example.com`);
|
|
100
|
+
await page.locator('button:has-text("Save")').click();
|
|
101
|
+
await expect(page.locator('.toast.toast-success')).toBeVisible();
|
|
102
|
+
|
|
103
|
+
// Attempt to create duplicate
|
|
104
|
+
await page.locator('button:has-text("New Supplier")').click();
|
|
105
|
+
await page.locator('#supplier-name').fill(`QA-Dup2-${EXEC_IDX}`);
|
|
106
|
+
await page.locator('#supplier-rut').fill('12345678-9'); // same RUT
|
|
107
|
+
await page.locator('#supplier-email').fill(`qa-dup2-${EXEC_IDX}@example.com`);
|
|
108
|
+
await page.locator('button:has-text("Save")').click();
|
|
109
|
+
|
|
110
|
+
await expect(page.locator('.toast.toast-error:has-text("RUT already exists")')).toBeVisible();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test.describe('Suppliers > List @P1', () => {
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* [TC-SUP-LS-001] List loads and displays supplier grid @P1
|
|
119
|
+
*/
|
|
120
|
+
test('[TC-SUP-LS-001] List loads and displays supplier grid @P1', async ({ page }) => {
|
|
121
|
+
await page.goto('/suppliers');
|
|
122
|
+
|
|
123
|
+
// Grid must render without console errors
|
|
124
|
+
await expect(page.locator('.supplier-grid')).toBeVisible();
|
|
125
|
+
|
|
126
|
+
// Search input present
|
|
127
|
+
await expect(page.locator('#supplier-search')).toBeVisible();
|
|
128
|
+
|
|
129
|
+
// Export button present
|
|
130
|
+
await expect(page.locator('button:has-text("Export")')).toBeVisible();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* [TC-SUP-LS-002] Unauthenticated user redirected to login @P0
|
|
135
|
+
*/
|
|
136
|
+
test('[TC-SUP-LS-002] Unauthenticated user redirected to login @P0', async ({ browser }) => {
|
|
137
|
+
// Create a fresh context with NO stored auth state
|
|
138
|
+
const ctx = await browser.newContext({ storageState: undefined });
|
|
139
|
+
const page = await ctx.newPage();
|
|
140
|
+
|
|
141
|
+
await page.goto(`${process.env.QA_BASE_URL}/suppliers`);
|
|
142
|
+
|
|
143
|
+
// Should redirect to login
|
|
144
|
+
await expect(page).toHaveURL(/\/login/);
|
|
145
|
+
await ctx.close();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Integration — ADO PowerShell Scripts
|
|
2
|
+
|
|
3
|
+
PowerShell scripts for Azure DevOps Test Plan management.
|
|
4
|
+
All scripts are parameterized and project-agnostic.
|
|
5
|
+
|
|
6
|
+
## Scripts
|
|
7
|
+
|
|
8
|
+
| Script | Purpose |
|
|
9
|
+
|--------|---------|
|
|
10
|
+
| `scripts/inject-ado-ids.ps1` | Prepend ADO Work Item IDs to spec file test titles |
|
|
11
|
+
| `scripts/create-testplan-from-mapping.ps1` | Create ADO Test Cases from a TC mapping JSON file |
|
|
12
|
+
| `scripts/sync-ado-titles.ps1` | Sync spec file test titles back to ADO WI titles |
|
|
13
|
+
| `pipelines/azure-pipeline-qa.yml` | CI pipeline template for running Playwright + ADO sync |
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- PowerShell 7+ (recommended) or Windows PowerShell 5.1
|
|
18
|
+
- Azure DevOps Personal Access Token (PAT) with `Work Items: Read & Write` scope
|
|
19
|
+
- PAT stored as `$env:AZURE_TOKEN` or passed via `-Token` parameter
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
# 1. Generate ado-ids.json mapping (from your TC mapping file)
|
|
25
|
+
.\scripts\create-testplan-from-mapping.ps1 `
|
|
26
|
+
-OrgUrl "https://dev.azure.com/your-org" `
|
|
27
|
+
-ProjectName "YourProject" `
|
|
28
|
+
-PlanId 22304 `
|
|
29
|
+
-MappingFile "qa/08-azure-integration/tc-mapping.json" `
|
|
30
|
+
-Token $env:AZURE_TOKEN
|
|
31
|
+
|
|
32
|
+
# 2. Inject ADO IDs into spec files (idempotent)
|
|
33
|
+
.\scripts\inject-ado-ids.ps1 `
|
|
34
|
+
-SpecDir "qa/07-automation/e2e" `
|
|
35
|
+
-MappingFile "qa/08-azure-integration/ado-ids.json"
|
|
36
|
+
|
|
37
|
+
# 3. Run tests (ADO reporter publishes results automatically)
|
|
38
|
+
npx playwright test
|
|
39
|
+
|
|
40
|
+
# 4. (Optional) Sync titles back to ADO
|
|
41
|
+
.\scripts\sync-ado-titles.ps1 `
|
|
42
|
+
-OrgUrl "https://dev.azure.com/your-org" `
|
|
43
|
+
-ProjectName "YourProject" `
|
|
44
|
+
-MappingFile "qa/08-azure-integration/ado-ids.json" `
|
|
45
|
+
-Token $env:AZURE_TOKEN
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Security rules
|
|
49
|
+
|
|
50
|
+
- NEVER hardcode tokens in script files
|
|
51
|
+
- NEVER commit `.env` files or generated `*.json` files that contain tokens
|
|
52
|
+
- Always pass `-Token` as a parameter from `$env:AZURE_TOKEN` or a CI secret variable
|
|
53
|
+
- PAT scope: minimum `Work Items: Read & Write`, `Test Management: Read & Write`
|
|
54
|
+
|
|
55
|
+
## File: tc-mapping.json (input)
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"planId": 22304,
|
|
60
|
+
"suiteId": 22305,
|
|
61
|
+
"testCases": [
|
|
62
|
+
{ "localId": "TC-SUP-CR-001", "title": "Create supplier @P0", "specFile": "suppliers/create.spec.ts" },
|
|
63
|
+
{ "localId": "TC-SUP-CR-002", "title": "Create supplier - required fields @P0", "specFile": "suppliers/create.spec.ts" }
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## File: ado-ids.json (output after create-testplan)
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
[
|
|
72
|
+
{ "localId": "TC-SUP-CR-001", "adoWiId": 22957, "specFile": "suppliers/create.spec.ts" },
|
|
73
|
+
{ "localId": "TC-SUP-CR-002", "adoWiId": 22958, "specFile": "suppliers/create.spec.ts" }
|
|
74
|
+
]
|
|
75
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------
|
|
2
|
+
# keber/qa-framework — Azure Pipeline QA Template
|
|
3
|
+
# -----------------------------------------------------------------------
|
|
4
|
+
# Parameterized CI pipeline for running Playwright E2E tests and
|
|
5
|
+
# optionally syncing results to Azure DevOps Test Plans.
|
|
6
|
+
#
|
|
7
|
+
# Required Pipeline Library Variable Group: "QA-Variables"
|
|
8
|
+
# QA_BASE_URL — Target application URL for QA environment
|
|
9
|
+
# QA_USER_EMAIL — Default test user email
|
|
10
|
+
# QA_USER_PASSWORD — Default test user password (secret)
|
|
11
|
+
# AZURE_TOKEN — ADO Personal Access Token (secret)
|
|
12
|
+
# ADO_ORG_URL — https://dev.azure.com/your-org
|
|
13
|
+
# ADO_PROJECT_NAME — ADO project name
|
|
14
|
+
# ADO_PLAN_ID — Test Plan ID
|
|
15
|
+
# -----------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
trigger:
|
|
18
|
+
branches:
|
|
19
|
+
include:
|
|
20
|
+
- main
|
|
21
|
+
- qa
|
|
22
|
+
- release/*
|
|
23
|
+
|
|
24
|
+
pr:
|
|
25
|
+
branches:
|
|
26
|
+
include:
|
|
27
|
+
- main
|
|
28
|
+
|
|
29
|
+
variables:
|
|
30
|
+
- group: QA-Variables
|
|
31
|
+
- name: NODE_VERSION
|
|
32
|
+
value: '20.x'
|
|
33
|
+
- name: QA_DIR
|
|
34
|
+
value: 'qa/07-automation'
|
|
35
|
+
- name: ADO_SYNC_DISABLED
|
|
36
|
+
value: 'false'
|
|
37
|
+
|
|
38
|
+
pool:
|
|
39
|
+
vmImage: 'ubuntu-latest'
|
|
40
|
+
|
|
41
|
+
stages:
|
|
42
|
+
# -----------------------------------------------------------------------
|
|
43
|
+
# Stage 1: Install & Setup
|
|
44
|
+
# -----------------------------------------------------------------------
|
|
45
|
+
- stage: Setup
|
|
46
|
+
displayName: 'Install Dependencies'
|
|
47
|
+
jobs:
|
|
48
|
+
- job: Install
|
|
49
|
+
displayName: 'npm install + Playwright browsers'
|
|
50
|
+
steps:
|
|
51
|
+
- task: NodeTool@0
|
|
52
|
+
inputs:
|
|
53
|
+
versionSpec: $(NODE_VERSION)
|
|
54
|
+
displayName: 'Use Node.js $(NODE_VERSION)'
|
|
55
|
+
|
|
56
|
+
- script: |
|
|
57
|
+
cd $(QA_DIR)
|
|
58
|
+
npm ci
|
|
59
|
+
npx playwright install chromium --with-deps
|
|
60
|
+
displayName: 'Install npm deps and Playwright Chromium'
|
|
61
|
+
|
|
62
|
+
- task: CacheBeta@1
|
|
63
|
+
inputs:
|
|
64
|
+
key: 'playwright | "$(Agent.OS)" | $(QA_DIR)/package-lock.json'
|
|
65
|
+
path: '~/.cache/ms-playwright'
|
|
66
|
+
displayName: 'Cache Playwright browsers'
|
|
67
|
+
|
|
68
|
+
# -----------------------------------------------------------------------
|
|
69
|
+
# Stage 2: E2E Tests
|
|
70
|
+
# -----------------------------------------------------------------------
|
|
71
|
+
- stage: E2E
|
|
72
|
+
displayName: 'E2E Tests'
|
|
73
|
+
dependsOn: Setup
|
|
74
|
+
jobs:
|
|
75
|
+
- job: PlaywrightTests
|
|
76
|
+
displayName: 'Run Playwright suite'
|
|
77
|
+
timeoutInMinutes: 60
|
|
78
|
+
steps:
|
|
79
|
+
- task: NodeTool@0
|
|
80
|
+
inputs:
|
|
81
|
+
versionSpec: $(NODE_VERSION)
|
|
82
|
+
|
|
83
|
+
- script: |
|
|
84
|
+
cd $(QA_DIR)
|
|
85
|
+
npm ci
|
|
86
|
+
npx playwright install chromium --with-deps
|
|
87
|
+
displayName: 'Restore dependencies'
|
|
88
|
+
|
|
89
|
+
- script: |
|
|
90
|
+
cd $(QA_DIR)
|
|
91
|
+
npx playwright test --grep @P0
|
|
92
|
+
displayName: 'Run P0 smoke suite'
|
|
93
|
+
env:
|
|
94
|
+
QA_BASE_URL: $(QA_BASE_URL)
|
|
95
|
+
QA_USER_EMAIL: $(QA_USER_EMAIL)
|
|
96
|
+
QA_USER_PASSWORD: $(QA_USER_PASSWORD)
|
|
97
|
+
AZURE_TOKEN: $(AZURE_TOKEN)
|
|
98
|
+
ADO_ORG_URL: $(ADO_ORG_URL)
|
|
99
|
+
ADO_PROJECT_NAME: $(ADO_PROJECT_NAME)
|
|
100
|
+
ADO_PLAN_ID: $(ADO_PLAN_ID)
|
|
101
|
+
ADO_SYNC_DISABLED: $(ADO_SYNC_DISABLED)
|
|
102
|
+
CI: 'true'
|
|
103
|
+
|
|
104
|
+
- script: |
|
|
105
|
+
cd $(QA_DIR)
|
|
106
|
+
npx playwright test --grep "@P1|@P2"
|
|
107
|
+
displayName: 'Run P1/P2 regression suite'
|
|
108
|
+
condition: succeededOrFailed()
|
|
109
|
+
env:
|
|
110
|
+
QA_BASE_URL: $(QA_BASE_URL)
|
|
111
|
+
QA_USER_EMAIL: $(QA_USER_EMAIL)
|
|
112
|
+
QA_USER_PASSWORD: $(QA_USER_PASSWORD)
|
|
113
|
+
AZURE_TOKEN: $(AZURE_TOKEN)
|
|
114
|
+
ADO_ORG_URL: $(ADO_ORG_URL)
|
|
115
|
+
ADO_PROJECT_NAME: $(ADO_PROJECT_NAME)
|
|
116
|
+
ADO_PLAN_ID: $(ADO_PLAN_ID)
|
|
117
|
+
CI: 'true'
|
|
118
|
+
|
|
119
|
+
- task: PublishTestResults@2
|
|
120
|
+
condition: always()
|
|
121
|
+
inputs:
|
|
122
|
+
testResultsFormat: 'JUnit'
|
|
123
|
+
testResultsFiles: '$(QA_DIR)/test-results/junit*.xml'
|
|
124
|
+
mergeTestResults: true
|
|
125
|
+
testRunTitle: 'Playwright E2E — $(Build.SourceBranchName)'
|
|
126
|
+
displayName: 'Publish test results'
|
|
127
|
+
|
|
128
|
+
- task: PublishBuildArtifacts@1
|
|
129
|
+
condition: always()
|
|
130
|
+
inputs:
|
|
131
|
+
pathToPublish: '$(QA_DIR)/playwright-report'
|
|
132
|
+
artifactName: 'playwright-report'
|
|
133
|
+
displayName: 'Publish Playwright HTML report'
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Creates Azure DevOps Test Cases from a local tc-mapping.json file.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Reads a tc-mapping.json file containing local TC definitions and creates
|
|
7
|
+
corresponding Test Case Work Items in a specified ADO Test Plan/Suite.
|
|
8
|
+
Outputs ado-ids.json with the resulting WI IDs for use by inject-ado-ids.ps1.
|
|
9
|
+
|
|
10
|
+
.PARAMETER OrgUrl
|
|
11
|
+
Azure DevOps organization URL (e.g. https://dev.azure.com/your-org).
|
|
12
|
+
|
|
13
|
+
.PARAMETER ProjectName
|
|
14
|
+
ADO project name.
|
|
15
|
+
|
|
16
|
+
.PARAMETER PlanId
|
|
17
|
+
ADO Test Plan ID.
|
|
18
|
+
|
|
19
|
+
.PARAMETER SuiteId
|
|
20
|
+
ADO Test Suite ID. If omitted, uses the plan's root suite.
|
|
21
|
+
|
|
22
|
+
.PARAMETER MappingFile
|
|
23
|
+
Path to tc-mapping.json input file.
|
|
24
|
+
|
|
25
|
+
.PARAMETER OutputFile
|
|
26
|
+
Path to write ado-ids.json output. Default: ado-ids.json in same dir as MappingFile.
|
|
27
|
+
|
|
28
|
+
.PARAMETER Token
|
|
29
|
+
ADO Personal Access Token. Prefer passing via $env:AZURE_TOKEN.
|
|
30
|
+
|
|
31
|
+
.EXAMPLE
|
|
32
|
+
.\create-testplan-from-mapping.ps1 `
|
|
33
|
+
-OrgUrl "https://dev.azure.com/your-org" `
|
|
34
|
+
-ProjectName "YourProject" `
|
|
35
|
+
-PlanId 22304 `
|
|
36
|
+
-MappingFile "qa/08-azure-integration/tc-mapping.json" `
|
|
37
|
+
-Token $env:AZURE_TOKEN
|
|
38
|
+
#>
|
|
39
|
+
[CmdletBinding(SupportsShouldProcess)]
|
|
40
|
+
param(
|
|
41
|
+
[Parameter(Mandatory)] [string] $OrgUrl,
|
|
42
|
+
[Parameter(Mandatory)] [string] $ProjectName,
|
|
43
|
+
[Parameter(Mandatory)] [int] $PlanId,
|
|
44
|
+
[int] $SuiteId = 0,
|
|
45
|
+
[Parameter(Mandatory)] [string] $MappingFile,
|
|
46
|
+
[string] $OutputFile = '',
|
|
47
|
+
[Parameter(Mandatory)] [string] $Token
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
Set-StrictMode -Version Latest
|
|
51
|
+
$ErrorActionPreference = 'Stop'
|
|
52
|
+
|
|
53
|
+
# Build auth header
|
|
54
|
+
$b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$Token"))
|
|
55
|
+
$headers = @{
|
|
56
|
+
'Authorization' = "Basic $b64"
|
|
57
|
+
'Content-Type' = 'application/json'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Load mapping
|
|
61
|
+
if (-not (Test-Path $MappingFile)) { Write-Error "Not found: $MappingFile"; exit 1 }
|
|
62
|
+
$mapping = Get-Content $MappingFile -Raw | ConvertFrom-Json
|
|
63
|
+
|
|
64
|
+
$apiBase = "$OrgUrl/$ProjectName/_apis"
|
|
65
|
+
$suiteId = if ($SuiteId -gt 0) { $SuiteId } else { $mapping.suiteId }
|
|
66
|
+
|
|
67
|
+
Write-Host "[create-testplan] Creating $($mapping.testCases.Count) test cases in Plan $PlanId / Suite $suiteId"
|
|
68
|
+
|
|
69
|
+
$results = @()
|
|
70
|
+
|
|
71
|
+
foreach ($tc in $mapping.testCases) {
|
|
72
|
+
$body = @{
|
|
73
|
+
op = 'add'
|
|
74
|
+
path = '/fields/System.Title'
|
|
75
|
+
value = "[$($tc.localId)] $($tc.title)"
|
|
76
|
+
} | ConvertTo-Json -AsArray
|
|
77
|
+
|
|
78
|
+
$createUrl = "$apiBase/wit/workitems/`$Test Case?api-version=7.1"
|
|
79
|
+
|
|
80
|
+
if ($PSCmdlet.ShouldProcess($tc.localId, "Create ADO Test Case")) {
|
|
81
|
+
try {
|
|
82
|
+
$wi = Invoke-RestMethod `
|
|
83
|
+
-Method Patch `
|
|
84
|
+
-Uri $createUrl `
|
|
85
|
+
-Headers ($headers + @{ 'Content-Type' = 'application/json-patch+json' }) `
|
|
86
|
+
-Body $body
|
|
87
|
+
|
|
88
|
+
$wiId = $wi.id
|
|
89
|
+
Write-Host " [CREATED] $($tc.localId) → WI #$wiId"
|
|
90
|
+
|
|
91
|
+
# Add to suite
|
|
92
|
+
$suiteUrl = "$apiBase/testplan/Plans/$PlanId/Suites/$suiteId/TestCase?api-version=7.1"
|
|
93
|
+
$suiteBody = @(@{ workItem = @{ id = $wiId } }) | ConvertTo-Json
|
|
94
|
+
Invoke-RestMethod -Method Post -Uri $suiteUrl -Headers $headers -Body $suiteBody | Out-Null
|
|
95
|
+
|
|
96
|
+
$results += [PSCustomObject]@{
|
|
97
|
+
localId = $tc.localId
|
|
98
|
+
adoWiId = $wiId
|
|
99
|
+
specFile = $tc.specFile
|
|
100
|
+
title = $tc.title
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
Write-Warning " [FAILED] $($tc.localId) — $($_.Exception.Message)"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Write output
|
|
109
|
+
$outFile = if ($OutputFile) { $OutputFile } else {
|
|
110
|
+
Join-Path (Split-Path $MappingFile) 'ado-ids.json'
|
|
111
|
+
}
|
|
112
|
+
$results | ConvertTo-Json -Depth 5 | Set-Content $outFile
|
|
113
|
+
Write-Host ""
|
|
114
|
+
Write-Host "[create-testplan] Done. $($results.Count) WIs created. Output: $outFile"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Prepends Azure DevOps Work Item IDs to Playwright spec file test titles.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Reads ado-ids.json (produced by create-testplan-from-mapping.ps1) and
|
|
7
|
+
updates every matching test() / test.skip() call in the spec files to
|
|
8
|
+
include the ADO WI ID as a prefix.
|
|
9
|
+
|
|
10
|
+
This operation is IDEMPOTENT: if the ID is already present, it is not
|
|
11
|
+
duplicated.
|
|
12
|
+
|
|
13
|
+
.PARAMETER SpecDir
|
|
14
|
+
Root directory containing Playwright spec files (.spec.ts).
|
|
15
|
+
Searched recursively.
|
|
16
|
+
|
|
17
|
+
.PARAMETER MappingFile
|
|
18
|
+
Path to ado-ids.json — array of { localId, adoWiId, specFile }.
|
|
19
|
+
|
|
20
|
+
.EXAMPLE
|
|
21
|
+
.\inject-ado-ids.ps1 `
|
|
22
|
+
-SpecDir "qa/07-automation/e2e" `
|
|
23
|
+
-MappingFile "qa/08-azure-integration/ado-ids.json"
|
|
24
|
+
|
|
25
|
+
.NOTES
|
|
26
|
+
Does NOT require a network connection or ADO token.
|
|
27
|
+
Commit the modified spec files after running.
|
|
28
|
+
#>
|
|
29
|
+
[CmdletBinding(SupportsShouldProcess)]
|
|
30
|
+
param(
|
|
31
|
+
[Parameter(Mandatory)]
|
|
32
|
+
[string] $SpecDir,
|
|
33
|
+
|
|
34
|
+
[Parameter(Mandatory)]
|
|
35
|
+
[string] $MappingFile
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
Set-StrictMode -Version Latest
|
|
39
|
+
$ErrorActionPreference = 'Stop'
|
|
40
|
+
|
|
41
|
+
# Load mapping
|
|
42
|
+
if (-not (Test-Path $MappingFile)) {
|
|
43
|
+
Write-Error "Mapping file not found: $MappingFile"
|
|
44
|
+
exit 1
|
|
45
|
+
}
|
|
46
|
+
$mapping = Get-Content $MappingFile -Raw | ConvertFrom-Json
|
|
47
|
+
|
|
48
|
+
Write-Host "[inject-ado-ids] Loaded $($mapping.Count) mapping entries from $MappingFile"
|
|
49
|
+
|
|
50
|
+
$updated = 0
|
|
51
|
+
$skipped = 0
|
|
52
|
+
|
|
53
|
+
foreach ($entry in $mapping) {
|
|
54
|
+
$adoId = $entry.adoWiId
|
|
55
|
+
$localId = $entry.localId
|
|
56
|
+
|
|
57
|
+
# Find spec files matching this entry
|
|
58
|
+
$pattern = if ($entry.specFile) { $entry.specFile } else { "**/*.spec.ts" }
|
|
59
|
+
$files = Get-ChildItem -Path $SpecDir -Recurse -Filter "*.spec.ts" |
|
|
60
|
+
Where-Object { $_.FullName -like "*$($entry.specFile)*" }
|
|
61
|
+
|
|
62
|
+
foreach ($file in $files) {
|
|
63
|
+
$content = Get-Content $file.FullName -Raw
|
|
64
|
+
|
|
65
|
+
# Match test titles containing the localId but NOT already prefixed with adoId
|
|
66
|
+
# Pattern: test('...[TC-x-x-NNN]...') where [NNN_ADOID] not yet present
|
|
67
|
+
$adoPrefix = "[$adoId]"
|
|
68
|
+
$escapedId = [regex]::Escape($localId)
|
|
69
|
+
|
|
70
|
+
# Check if already injected (idempotent guard)
|
|
71
|
+
if ($content -match [regex]::Escape($adoPrefix)) {
|
|
72
|
+
Write-Verbose "[$localId] ADO WI $adoId already present in $($file.Name) — skipping"
|
|
73
|
+
$skipped++
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Replace: test('ORIGINAL_TITLE') → test('[ADOID] ORIGINAL_TITLE')
|
|
78
|
+
# Matches both single and double quotes, test() and test.skip()
|
|
79
|
+
$newContent = $content -replace `
|
|
80
|
+
"(?<=(test(?:\.skip)?\s*\()(['""]))\s*(?=.*\Q$localId\E)", `
|
|
81
|
+
"$adoPrefix "
|
|
82
|
+
|
|
83
|
+
if ($newContent -ne $content) {
|
|
84
|
+
if ($PSCmdlet.ShouldProcess($file.FullName, "Inject ADO WI $adoId for $localId")) {
|
|
85
|
+
Set-Content -Path $file.FullName -Value $newContent -NoNewline
|
|
86
|
+
Write-Host " [UPDATED] $($file.Name) — injected [$adoId] for $localId"
|
|
87
|
+
$updated++
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
Write-Verbose " [NO MATCH] $localId not found by pattern in $($file.Name)"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Write-Host ""
|
|
96
|
+
Write-Host "[inject-ado-ids] Done. Updated: $updated | Skipped (already present): $skipped"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Syncs Playwright spec file test titles back to Azure DevOps Work Item titles.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
After test titles are updated in spec files (e.g., after a spec refactor),
|
|
7
|
+
this script reads ado-ids.json and updates ADO WI titles to match the
|
|
8
|
+
current spec file titles.
|
|
9
|
+
|
|
10
|
+
.PARAMETER OrgUrl
|
|
11
|
+
Azure DevOps organization URL.
|
|
12
|
+
|
|
13
|
+
.PARAMETER ProjectName
|
|
14
|
+
ADO project name.
|
|
15
|
+
|
|
16
|
+
.PARAMETER MappingFile
|
|
17
|
+
Path to ado-ids.json (output of create-testplan-from-mapping.ps1).
|
|
18
|
+
|
|
19
|
+
.PARAMETER SpecDir
|
|
20
|
+
Root directory of Playwright spec files (.spec.ts). Searched recursively.
|
|
21
|
+
|
|
22
|
+
.PARAMETER Token
|
|
23
|
+
ADO Personal Access Token. Prefer $env:AZURE_TOKEN.
|
|
24
|
+
|
|
25
|
+
.EXAMPLE
|
|
26
|
+
.\sync-ado-titles.ps1 `
|
|
27
|
+
-OrgUrl "https://dev.azure.com/your-org" `
|
|
28
|
+
-ProjectName "YourProject" `
|
|
29
|
+
-MappingFile "qa/08-azure-integration/ado-ids.json" `
|
|
30
|
+
-SpecDir "qa/07-automation/e2e" `
|
|
31
|
+
-Token $env:AZURE_TOKEN
|
|
32
|
+
#>
|
|
33
|
+
[CmdletBinding(SupportsShouldProcess)]
|
|
34
|
+
param(
|
|
35
|
+
[Parameter(Mandatory)] [string] $OrgUrl,
|
|
36
|
+
[Parameter(Mandatory)] [string] $ProjectName,
|
|
37
|
+
[Parameter(Mandatory)] [string] $MappingFile,
|
|
38
|
+
[Parameter(Mandatory)] [string] $SpecDir,
|
|
39
|
+
[Parameter(Mandatory)] [string] $Token
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
Set-StrictMode -Version Latest
|
|
43
|
+
$ErrorActionPreference = 'Stop'
|
|
44
|
+
|
|
45
|
+
$b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$Token"))
|
|
46
|
+
$headers = @{
|
|
47
|
+
'Authorization' = "Basic $b64"
|
|
48
|
+
'Content-Type' = 'application/json-patch+json'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$mapping = Get-Content $MappingFile -Raw | ConvertFrom-Json
|
|
52
|
+
$apiBase = "$OrgUrl/$ProjectName/_apis"
|
|
53
|
+
|
|
54
|
+
$synced = 0
|
|
55
|
+
$skipped = 0
|
|
56
|
+
|
|
57
|
+
foreach ($entry in $mapping) {
|
|
58
|
+
$adoId = $entry.adoWiId
|
|
59
|
+
$specFile = Get-ChildItem -Path $SpecDir -Recurse -Filter "*.spec.ts" |
|
|
60
|
+
Where-Object { $_.FullName -like "*$($entry.specFile)*" } |
|
|
61
|
+
Select-Object -First 1
|
|
62
|
+
|
|
63
|
+
if (-not $specFile) {
|
|
64
|
+
Write-Warning " [NOT FOUND] $($entry.specFile) — skipping WI $adoId"
|
|
65
|
+
$skipped++
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Extract title from test() line that contains the adoId prefix
|
|
70
|
+
$content = Get-Content $specFile.FullName -Raw
|
|
71
|
+
$match = [regex]::Match($content, "test(?:\.skip)?\s*\(\s*['""](\[$adoId\][^'""]+)['""]")
|
|
72
|
+
|
|
73
|
+
if (-not $match.Success) {
|
|
74
|
+
Write-Verbose " [NO MATCH] WI $adoId title not found in $($specFile.Name)"
|
|
75
|
+
$skipped++
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$newTitle = $match.Groups[1].Value.Trim()
|
|
80
|
+
$patchUrl = "$apiBase/wit/workitems/$($adoId)?api-version=7.1"
|
|
81
|
+
$body = @(
|
|
82
|
+
@{ op = 'replace'; path = '/fields/System.Title'; value = $newTitle }
|
|
83
|
+
) | ConvertTo-Json -AsArray
|
|
84
|
+
|
|
85
|
+
if ($PSCmdlet.ShouldProcess("WI #$adoId", "Update title to: $newTitle")) {
|
|
86
|
+
Invoke-RestMethod -Method Patch -Uri $patchUrl -Headers $headers -Body $body | Out-Null
|
|
87
|
+
Write-Host " [SYNCED] WI #$adoId → $newTitle"
|
|
88
|
+
$synced++
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Write-Host ""
|
|
93
|
+
Write-Host "[sync-ado-titles] Done. Synced: $synced | Skipped: $skipped"
|