@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +233 -0
  3. package/agent-instructions/00-module-analysis.md +263 -0
  4. package/agent-instructions/01-spec-generation.md +278 -0
  5. package/agent-instructions/02-test-plan-generation.md +202 -0
  6. package/agent-instructions/03-test-case-generation.md +147 -0
  7. package/agent-instructions/04-automation-generation.md +310 -0
  8. package/agent-instructions/04b-test-stabilization.md +306 -0
  9. package/agent-instructions/05-ado-integration.md +244 -0
  10. package/agent-instructions/06-maintenance.md +125 -0
  11. package/docs/architecture.md +227 -0
  12. package/docs/comparison-matrix.md +131 -0
  13. package/docs/final-report.md +279 -0
  14. package/docs/folder-structure-guide.md +291 -0
  15. package/docs/generalization-decisions.md +203 -0
  16. package/docs/installation.md +239 -0
  17. package/docs/spec-driven-philosophy.md +170 -0
  18. package/docs/usage-with-agent.md +203 -0
  19. package/examples/module-example/README.md +34 -0
  20. package/examples/module-example/suppliers/00-inventory.md +56 -0
  21. package/examples/module-example/suppliers/suppliers-create.spec.ts +148 -0
  22. package/integrations/ado-powershell/README.md +75 -0
  23. package/integrations/ado-powershell/pipelines/azure-pipeline-qa.yml +133 -0
  24. package/integrations/ado-powershell/scripts/create-testplan-from-mapping.ps1 +114 -0
  25. package/integrations/ado-powershell/scripts/inject-ado-ids.ps1 +96 -0
  26. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +93 -0
  27. package/integrations/playwright/README.md +68 -0
  28. package/integrations/playwright-azure-reporter/README.md +88 -0
  29. package/package.json +57 -0
  30. package/qa-framework.config.json +87 -0
  31. package/scripts/cli.js +74 -0
  32. package/scripts/generate.js +92 -0
  33. package/scripts/init.js +322 -0
  34. package/scripts/validate.js +184 -0
  35. package/templates/automation-scaffold/.env.example +56 -0
  36. package/templates/automation-scaffold/fixtures/auth.ts +77 -0
  37. package/templates/automation-scaffold/fixtures/test-helpers.ts +85 -0
  38. package/templates/automation-scaffold/global-setup.ts +106 -0
  39. package/templates/automation-scaffold/package.json +24 -0
  40. package/templates/automation-scaffold/playwright.config.ts +85 -0
  41. package/templates/defect-report.md +101 -0
  42. package/templates/execution-report.md +116 -0
  43. package/templates/session-summary.md +73 -0
  44. package/templates/specification/00-inventory.md +81 -0
  45. package/templates/specification/01-business-rules.md +90 -0
  46. package/templates/specification/02-workflows.md +114 -0
  47. package/templates/specification/03-roles-permissions.md +49 -0
  48. package/templates/specification/04-test-data.md +104 -0
  49. package/templates/specification/05-test-scenarios.md +226 -0
  50. package/templates/test-case.md +81 -0
  51. 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"