@keber/qa-framework 1.9.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -33,6 +33,74 @@ has been removed or parameterized. See `docs/comparison-matrix.md` and `docs/gen
33
33
 
34
34
  ---
35
35
 
36
+ ## [1.10.0] - 2026-05-19
37
+
38
+ ### Added
39
+
40
+ - **`integrations/ado-powershell/pipelines/azure-pipeline-qa.yml`** — pipeline template
41
+ significantly expanded:
42
+ - New **Stage 3: API Integration Tests** — separate stage for `qa/07-automation/integration`,
43
+ with its own `PublishTestResults` and `PublishPipelineArtifact` tasks.
44
+ - **Cross-project repository checkout** via `resources.repositories` block; each stage
45
+ now performs an explicit `checkout: source`.
46
+ - **Smoke test step** in the E2E stage — validates `QA_BASE_URL` is reachable via `curl`
47
+ before running the suite.
48
+ - **Commented placeholder** for `AzureCLI@2` login task (Playwright Workspaces / Entra ID)
49
+ in both E2E and API stages, with `PLAYWRIGHT_SERVICE_URL` env var placeholder.
50
+ - Optional nightly schedule block (commented out).
51
+ - `PublishPipelineArtifact` tasks for HTML report and trace artifacts in both stages.
52
+ - **`integrations/playwright-azure-reporter/README.md`** — fully rewritten:
53
+ - Variables split into local (PAT) vs CI/CD (`SYSTEM_ACCESSTOKEN`) sections.
54
+ - Config snippet updated to use `ADO_ORG`, `ADO_PROJECT`, `orgUrl` constructed from
55
+ `ADO_ORG`, and `SYSTEM_ACCESSTOKEN ?? AZURE_TOKEN` token fallback.
56
+ - New **Playwright Workspaces reporter** section (`@azure/playwright` +
57
+ `playwright.service.config.ts` + Entra ID authentication notes).
58
+ - Troubleshooting table expanded with pipeline-specific `401` scenario.
59
+
60
+ ### Changed
61
+
62
+ - **`azure-pipeline-qa.yml`** — variable names aligned with current conventions:
63
+ `AZURE_TOKEN` / `ADO_ORG_URL` / `ADO_PROJECT_NAME` replaced by `ADO_ORG` / `ADO_PROJECT`;
64
+ `ADO_PLAN_ID` split into `ADO_E2E_PLAN_ID` / `ADO_API_PLAN_ID`; `SYSTEM_ACCESSTOKEN`
65
+ replaces PAT for ADO reporter authentication.
66
+ - **`azure-pipeline-qa.yml`** — E2E job `timeoutInMinutes` increased from 60 to 90.
67
+ - **`azure-pipeline-qa.yml`** — trigger reduced to `main` only (removed `qa` and
68
+ `release/*` branches); trigger rationale documented in comments.
69
+ - **`azure-pipeline-qa.yml`** — E2E test run consolidated from two `@grep`-tag steps
70
+ (P0 + P1/P2) into a single `--grep-invert "_inspect|_diag"` run with `continueOnError: true`.
71
+ - **`azure-pipeline-qa.yml`** — cache target changed from `~/.cache/ms-playwright` to
72
+ `node_modules` per workspace; `cacheHitVar` added to both cache tasks.
73
+ - **`azure-pipeline-qa.yml`** — `pool` block moved above `variables` for readability;
74
+ `ADO_SYNC_DISABLED` variable removed.
75
+
76
+ ---
77
+
78
+ ## [1.9.1] - 2026-05-15
79
+
80
+ ### Added
81
+
82
+ - **`integrations/ado-powershell/scripts/md-to-tc-mapping.ps1`** — new script that parses a
83
+ Plan-de-Pruebas markdown file and generates `tc-mapping.json`. Reads the Tabla de Pruebas
84
+ table, detects columns dynamically, skips Bloqueado rows (PENDING-CODE), splits steps by
85
+ `<br>`, and strips leading numbering. Supports a `-TableSection` parameter for plans with
86
+ non-standard headings. This was previously missing from the framework, forcing agents to
87
+ implement ad-hoc parsers in each project.
88
+
89
+ ### Fixed
90
+
91
+ - **`create-testplan-from-mapping.ps1`** — removed Unicode characters that caused
92
+ `MissingArrayIndexExpression` parser errors in PowerShell 5.1.
93
+ - **`create-testplan-from-mapping.ps1`** — added auto-load of `ado-powershell` skill when
94
+ `New-AdoTestCase` is not yet in scope; script no longer requires manual dot-sourcing before
95
+ invocation.
96
+ - **`create-testplan-from-mapping.ps1`** — added fallback to `$mapping.items` when
97
+ `$mapping.testCases` is absent, for compatibility with mappings generated by older sessions.
98
+ - **`inject-ado-ids.ps1`** — fixed broken regex (`\Q...\E` is not valid PowerShell); replaced
99
+ with `[regex]::Escape()`. Added `-AdonIdsDir` parameter to process all `ado-ids-*.json`
100
+ files in a directory in one pass. Added `-DryRun` switch. Fixed UTF-8 encoding on write.
101
+
102
+ ---
103
+
36
104
  ## [1.9.0] - 2026-05-15
37
105
 
38
106
  ### Changed
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # qa-framework
2
2
 
3
3
  > Reusable, installable, agent-oriented QA framework for spec-driven automated testing.
4
- > Current version: **v1.9.0**
4
+ > Current version: **v1.9.1**
5
5
 
6
6
  ---
7
7
 
@@ -7,8 +7,9 @@ All scripts are parameterized and project-agnostic.
7
7
 
8
8
  | Script | Purpose |
9
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 |
10
+ | `scripts/md-to-tc-mapping.ps1` | Parse a Plan-de-Pruebas markdown and generate `tc-mapping.json` |
11
+ | `scripts/create-testplan-from-mapping.ps1` | Create ADO Test Cases from `tc-mapping.json` |
12
+ | `scripts/inject-ado-ids.ps1` | Inject ADO WI IDs into Playwright spec file test titles |
12
13
  | `scripts/sync-ado-titles.ps1` | Sync spec file test titles back to ADO WI titles |
13
14
  | `pipelines/azure-pipeline-qa.yml` | CI pipeline template for running Playwright + ADO sync |
14
15
 
@@ -25,10 +26,18 @@ All scripts are parameterized and project-agnostic.
25
26
  # 0. Load the ADO skill (initialises session from env vars)
26
27
  . .github/skills/ado-powershell/load.ps1
27
28
 
28
- # 1. Create Test Cases in ADO from tc-mapping.json
29
- .\scripts\create-testplan-from-mapping.ps1 `
30
- -PlanId 22304 `
31
- -MappingFile "qa/08-azure-integration/tc-mapping.json"
29
+ # 1. Parse Plan de Pruebas markdown -> tc-mapping.json
30
+ .\node_modules\@keber\qa-framework\integrations\ado-powershell\scripts\md-to-tc-mapping.ps1 `
31
+ -PlanFile "qa/02-test-plans/sprints/Sprint-N/Plan-de-Pruebas-MyProject-Sprint-N-MOD-SUB.md" `
32
+ -SpecFile "mod/sub.spec.ts" `
33
+ -OutputFile "qa/08-azure-integration/tc-mapping-e2e-mod-sub.json" `
34
+ -PlanId <PLAN_ID> `
35
+ -SuiteId <SUITE_ID>
36
+
37
+ # 2. Create Test Cases in ADO from tc-mapping.json (session already loaded above)
38
+ .\node_modules\@keber\qa-framework\integrations\ado-powershell\scripts\create-testplan-from-mapping.ps1 `
39
+ -PlanId <PLAN_ID> `
40
+ -MappingFile "qa/08-azure-integration/tc-mapping-e2e-mod-sub.json"
32
41
 
33
42
  # 2. Inject ADO IDs into spec files (idempotent)
34
43
  .\scripts\inject-ado-ids.ps1 `
@@ -1,133 +1,278 @@
1
1
  # -----------------------------------------------------------------------
2
2
  # keber/qa-framework — Azure Pipeline QA Template
3
3
  # -----------------------------------------------------------------------
4
- # Parameterized CI pipeline for running Playwright E2E tests and
5
- # optionally syncing results to Azure DevOps Test Plans.
4
+ # Parameterized CI pipeline for running Playwright E2E and API tests and
5
+ # syncing results to Azure DevOps Test Plans via SYSTEM_ACCESSTOKEN.
6
+ #
7
+ # Cross-project repository: the test repo lives in a different ADO project.
8
+ # Update 'resources.repositories.name' to match your org/project/repo.
6
9
  #
7
10
  # 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
11
+ # Create this group in ADO → Pipelines → Library and add the following:
12
+ #
13
+ # QA_BASE_URL — Target application URL (e.g. https://app-qa.example.com)
14
+ # QA_USER_EMAIL — Test user email / RUT
15
+ # QA_USER_PASSWORD — Test user password [secret]
16
+ # ADO_ORG — ADO organization slug (e.g. garcesfruit-desarrollo)
17
+ # ADO_PROJECT — ADO project name (e.g. gf-portalproveedores)
18
+ # ADO_E2E_PLAN_ID — Test Plan ID for the E2E suite (e.g. 27044)
19
+ # ADO_API_PLAN_ID — Test Plan ID for the API/integration suite (e.g. 27046)
20
+ #
21
+ # NOTE on multiple Plan IDs: ADO variable groups store plain strings — there
22
+ # is no array type. Use one variable per suite (ADO_E2E_PLAN_ID / ADO_API_PLAN_ID).
23
+ # If the project has only one suite, a single ADO_PLAN_ID variable is enough.
24
+ # Results are reported to ADO via System.AccessToken (OAuth); no PAT required.
15
25
  # -----------------------------------------------------------------------
16
26
 
27
+ # Cross-project repository — update <ADO_SOURCE_PROJECT>/<REPO_NAME> as needed
28
+ resources:
29
+ repositories:
30
+ - repository: source
31
+ type: git
32
+ name: <ADO_SOURCE_PROJECT>/<REPO_NAME> # e.g. gf-gestion-qa/gf-portalproveedores
33
+ ref: refs/heads/main
34
+
35
+ # Trigger: push and PR to main.
36
+ # The test repo is decoupled from the app repo, so branch triggers are minimal.
17
37
  trigger:
18
38
  branches:
19
39
  include:
20
40
  - main
21
- - qa
22
- - release/*
23
41
 
24
42
  pr:
25
43
  branches:
26
44
  include:
27
45
  - main
28
46
 
47
+ # Optional scheduled run — uncomment and adjust if a nightly regression is needed:
48
+ # schedules:
49
+ # - cron: '0 6 * * 1-5' # 06:00 UTC Mon–Fri (03:00 CLT / 02:00 CLST)
50
+ # displayName: Nightly regression
51
+ # branches:
52
+ # include:
53
+ # - main
54
+ # always: true
55
+
56
+ pool:
57
+ vmImage: 'ubuntu-latest'
58
+
29
59
  variables:
30
60
  - group: QA-Variables
31
61
  - name: NODE_VERSION
32
62
  value: '20.x'
33
- - name: QA_DIR
63
+ - name: E2E_DIR
34
64
  value: 'qa/07-automation/e2e'
35
- - name: ADO_SYNC_DISABLED
36
- value: 'false'
37
-
38
- pool:
39
- vmImage: 'ubuntu-latest'
65
+ - name: API_DIR
66
+ value: 'qa/07-automation/integration'
40
67
 
41
68
  stages:
42
69
  # -----------------------------------------------------------------------
43
- # Stage 1: Install & Setup
70
+ # Stage 1: Install & cache dependencies
44
71
  # -----------------------------------------------------------------------
45
72
  - stage: Setup
46
73
  displayName: 'Install Dependencies'
47
74
  jobs:
48
75
  - job: Install
49
- displayName: 'npm install + Playwright browsers'
76
+ displayName: 'npm ci + Playwright browsers'
50
77
  steps:
78
+ - checkout: source
79
+ displayName: 'Checkout source repo'
80
+
51
81
  - task: NodeTool@0
52
82
  inputs:
53
83
  versionSpec: $(NODE_VERSION)
54
84
  displayName: 'Use Node.js $(NODE_VERSION)'
55
85
 
56
86
  - script: |
57
- cd $(QA_DIR)
87
+ cd $(E2E_DIR)
58
88
  npm ci
59
89
  npx playwright install chromium --with-deps
60
- displayName: 'Install npm deps and Playwright Chromium'
90
+ displayName: 'Install E2E deps and Playwright Chromium'
61
91
 
62
92
  - task: CacheBeta@1
63
93
  inputs:
64
- key: 'playwright | "$(Agent.OS)" | $(QA_DIR)/package-lock.json'
65
- path: '~/.cache/ms-playwright'
66
- displayName: 'Cache Playwright browsers'
94
+ key: 'e2e | "$(Agent.OS)" | $(E2E_DIR)/package-lock.json'
95
+ path: '$(E2E_DIR)/node_modules'
96
+ cacheHitVar: 'CACHE_RESTORED_E2E'
97
+ displayName: 'Cache E2E node_modules'
98
+
99
+ - script: |
100
+ cd $(API_DIR)
101
+ npm ci
102
+ displayName: 'Install API deps'
103
+
104
+ - task: CacheBeta@1
105
+ inputs:
106
+ key: 'api | "$(Agent.OS)" | $(API_DIR)/package-lock.json'
107
+ path: '$(API_DIR)/node_modules'
108
+ cacheHitVar: 'CACHE_RESTORED_API'
109
+ displayName: 'Cache API node_modules'
67
110
 
68
111
  # -----------------------------------------------------------------------
69
- # Stage 2: E2E Tests
112
+ # Stage 2: E2E Tests (Playwright/Chromium)
70
113
  # -----------------------------------------------------------------------
71
114
  - stage: E2E
72
- displayName: 'E2E Tests'
115
+ displayName: 'E2E Tests (Playwright/Chromium)'
73
116
  dependsOn: Setup
74
117
  jobs:
75
- - job: PlaywrightTests
76
- displayName: 'Run Playwright suite'
77
- timeoutInMinutes: 60
118
+ - job: PlaywrightE2E
119
+ displayName: 'Run E2E suite'
120
+ timeoutInMinutes: 90
78
121
  steps:
122
+ - checkout: source
123
+ displayName: 'Checkout source repo'
124
+
79
125
  - task: NodeTool@0
80
126
  inputs:
81
127
  versionSpec: $(NODE_VERSION)
128
+ displayName: 'Use Node.js $(NODE_VERSION)'
82
129
 
83
130
  - script: |
84
- cd $(QA_DIR)
131
+ cd $(E2E_DIR)
85
132
  npm ci
86
133
  npx playwright install chromium --with-deps
87
- displayName: 'Restore dependencies'
134
+ displayName: 'Restore E2E dependencies'
135
+
136
+ - script: |
137
+ echo "QA_BASE_URL = $(QA_BASE_URL)"
138
+ HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time 30 "$(QA_BASE_URL)")
139
+ echo "HTTP response: $HTTP_CODE"
140
+ if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 400 ]; then
141
+ echo "##[error]Smoke test failed — $(QA_BASE_URL) returned HTTP $HTTP_CODE"
142
+ exit 1
143
+ fi
144
+ echo "Smoke test passed (HTTP $HTTP_CODE)"
145
+ displayName: 'Smoke test — verify QA_BASE_URL is reachable'
146
+
147
+ # ---------------------------------------------------------------
148
+ # Playwright Workspaces (Azure App Testing)
149
+ # Enable once the ADO service connection is configured.
150
+ # Prerequisite: the service connection must have the Contributor
151
+ # role on the workspace in Azure Portal (Access control IAM).
152
+ # ---------------------------------------------------------------
153
+ # - task: AzureCLI@2
154
+ # displayName: 'Login to Azure (Playwright Workspaces)'
155
+ # inputs:
156
+ # azureSubscription: '<AZURE_SERVICE_CONNECTION_NAME>'
157
+ # scriptType: bash
158
+ # scriptLocation: inlineScript
159
+ # inlineScript: echo "Authenticated via Entra ID"
88
160
 
89
161
  - script: |
90
- cd $(QA_DIR)
91
- npx playwright test --grep @P0
92
- displayName: 'Run P0 smoke suite'
162
+ cd $(E2E_DIR)
163
+ npx playwright test --project=chromium --ignore-snapshots \
164
+ --grep-invert "_inspect|_diag"
165
+ displayName: 'Run E2E tests'
166
+ continueOnError: true
93
167
  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'
168
+ QA_BASE_URL: $(QA_BASE_URL)
169
+ QA_USER_EMAIL: $(QA_USER_EMAIL)
170
+ QA_USER_PASSWORD: $(QA_USER_PASSWORD)
171
+ ADO_ORG: $(ADO_ORG)
172
+ ADO_PROJECT: $(ADO_PROJECT)
173
+ ADO_PLAN_ID: $(ADO_E2E_PLAN_ID)
174
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
175
+ CI: 'true'
176
+ # PLAYWRIGHT_SERVICE_URL: $(PLAYWRIGHT_SERVICE_URL) # Playwright Workspaces endpoint — enable together with the login task
177
+
178
+ - task: PublishTestResults@2
179
+ condition: always()
180
+ inputs:
181
+ testResultsFormat: 'JUnit'
182
+ testResultsFiles: '**/test-results/**/*.xml'
183
+ searchFolder: $(E2E_DIR)
184
+ mergeTestResults: true
185
+ failTaskOnFailedTests: true
186
+ testRunTitle: '[Auto E2E] $(Build.BuildNumber)'
187
+ displayName: 'Publish E2E test results (JUnit)'
188
+
189
+ - task: PublishPipelineArtifact@1
190
+ condition: always()
191
+ inputs:
192
+ targetPath: '$(E2E_DIR)/playwright-report'
193
+ artifact: 'e2e-playwright-report'
194
+ displayName: 'Publish E2E HTML report'
195
+
196
+ - task: PublishPipelineArtifact@1
197
+ condition: always()
198
+ inputs:
199
+ targetPath: '$(E2E_DIR)/test-results'
200
+ artifact: 'e2e-test-results'
201
+ displayName: 'Publish E2E trace artifacts'
202
+
203
+ # -----------------------------------------------------------------------
204
+ # Stage 3: API Integration Tests
205
+ # -----------------------------------------------------------------------
206
+ - stage: API
207
+ displayName: 'API Integration Tests'
208
+ dependsOn: E2E
209
+ jobs:
210
+ - job: PlaywrightAPI
211
+ displayName: 'Run API suite'
212
+ timeoutInMinutes: 30
213
+ steps:
214
+ - checkout: source
215
+ displayName: 'Checkout source repo'
216
+
217
+ - task: NodeTool@0
218
+ inputs:
219
+ versionSpec: $(NODE_VERSION)
220
+ displayName: 'Use Node.js $(NODE_VERSION)'
221
+
222
+ - script: |
223
+ cd $(API_DIR)
224
+ npm ci
225
+ displayName: 'Restore API dependencies'
226
+
227
+ # ---------------------------------------------------------------
228
+ # Playwright Workspaces (Azure App Testing)
229
+ # Enable once the ADO service connection is configured.
230
+ # ---------------------------------------------------------------
231
+ # - task: AzureCLI@2
232
+ # displayName: 'Login to Azure (Playwright Workspaces)'
233
+ # inputs:
234
+ # azureSubscription: '<AZURE_SERVICE_CONNECTION_NAME>'
235
+ # scriptType: bash
236
+ # scriptLocation: inlineScript
237
+ # inlineScript: echo "Authenticated via Entra ID"
103
238
 
104
239
  - script: |
105
- cd $(QA_DIR)
106
- npx playwright test --grep "@P1|@P2"
107
- displayName: 'Run P1/P2 regression suite'
108
- condition: succeededOrFailed()
240
+ cd $(API_DIR)
241
+ npx playwright test --grep-invert "_inspect|_diag"
242
+ displayName: 'Run API integration tests'
243
+ continueOnError: true
109
244
  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'
245
+ QA_BASE_URL: $(QA_BASE_URL)
246
+ QA_USER_EMAIL: $(QA_USER_EMAIL)
247
+ QA_USER_PASSWORD: $(QA_USER_PASSWORD)
248
+ ADO_ORG: $(ADO_ORG)
249
+ ADO_PROJECT: $(ADO_PROJECT)
250
+ ADO_PLAN_ID: $(ADO_API_PLAN_ID)
251
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
252
+ CI: 'true'
253
+ # PLAYWRIGHT_SERVICE_URL: $(PLAYWRIGHT_SERVICE_URL) # Playwright Workspaces endpoint — enable together with the login task
118
254
 
119
255
  - task: PublishTestResults@2
120
256
  condition: always()
121
257
  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'
258
+ testResultsFormat: 'JUnit'
259
+ testResultsFiles: '**/test-results/**/*.xml'
260
+ searchFolder: $(API_DIR)
261
+ mergeTestResults: true
262
+ failTaskOnFailedTests: true
263
+ testRunTitle: '[Auto API] $(Build.BuildNumber)'
264
+ displayName: 'Publish API test results (JUnit)'
265
+
266
+ - task: PublishPipelineArtifact@1
267
+ condition: always()
268
+ inputs:
269
+ targetPath: '$(API_DIR)/playwright-report'
270
+ artifact: 'api-playwright-report'
271
+ displayName: 'Publish API HTML report'
127
272
 
128
- - task: PublishBuildArtifacts@1
273
+ - task: PublishPipelineArtifact@1
129
274
  condition: always()
130
275
  inputs:
131
- pathToPublish: '$(QA_DIR)/playwright-report'
132
- artifactName: 'playwright-report'
133
- displayName: 'Publish Playwright HTML report'
276
+ targetPath: '$(API_DIR)/test-results'
277
+ artifact: 'api-test-results'
278
+ displayName: 'Publish API trace artifacts'
@@ -41,9 +41,19 @@ param(
41
41
  Set-StrictMode -Version Latest
42
42
  $ErrorActionPreference = 'Stop'
43
43
 
44
- # Verify ADO session is available
44
+ # Auto-load ADO session if not already active
45
45
  if (-not (Get-Command 'New-AdoTestCase' -ErrorAction SilentlyContinue)) {
46
- Write-Error "ADO session not loaded. Run: . .github/skills/ado-powershell/load.ps1"
46
+ $loadScript = Join-Path $PSScriptRoot '..\..\..\node_modules\@keber\ado-powershell\.github\skills\ado-powershell\load.ps1'
47
+ if (Test-Path $loadScript) {
48
+ . $loadScript
49
+ } else {
50
+ Write-Error "ADO session not loaded and load.ps1 not found. Run: . node_modules\@keber\ado-powershell\.github\skills\ado-powershell\load.ps1"
51
+ exit 1
52
+ }
53
+ }
54
+
55
+ if (-not (Get-Command 'New-AdoTestCase' -ErrorAction SilentlyContinue)) {
56
+ Write-Error "New-AdoTestCase not available after loading ado-powershell. Check the skill version (requires >= 1.3.0)."
47
57
  exit 1
48
58
  }
49
59
 
@@ -53,40 +63,41 @@ $mapping = Get-Content $MappingFile -Raw | ConvertFrom-Json
53
63
 
54
64
  $suiteId = if ($SuiteId -gt 0) { $SuiteId } else { $mapping.suiteId }
55
65
 
56
- Write-Host "[create-testplan] Creating $($mapping.testCases.Count) test cases in Plan $PlanId / Suite $suiteId"
66
+ # Support both 'testCases' (canonical) and 'items' (legacy)
67
+ $tcList = if ($mapping.PSObject.Properties['testCases']) { $mapping.testCases } else { $mapping.items }
68
+
69
+ Write-Host "[create-testplan] Creating $($tcList.Count) test cases in Plan $PlanId / Suite $suiteId"
57
70
 
58
71
  $results = @()
59
72
 
60
- foreach ($tc in $mapping.testCases) {
73
+ foreach ($tc in $tcList) {
61
74
  $steps = if ($tc.PSObject.Properties['steps']) { $tc.steps } else { @() }
62
75
  $expectedResult = if ($tc.PSObject.Properties['expectedResult']) { $tc.expectedResult } else { '' }
63
76
 
64
- if ($PSCmdlet.ShouldProcess($tc.localId, "Create ADO Test Case")) {
65
- try {
66
- $splatArgs = @{
67
- Title = "[$($tc.localId)] $($tc.title)"
68
- Confirm = $false
69
- }
70
- if ($steps.Count -gt 0) {
71
- $splatArgs.Steps = $steps
72
- $splatArgs.ExpectedResult = $expectedResult
73
- }
74
-
75
- $wi = New-AdoTestCase @splatArgs
76
- $wiId = $wi.id
77
- Write-Host " [CREATED] $($tc.localId) → WI #$wiId"
78
-
79
- Add-AdoTestCaseToSuite -PlanId $PlanId -SuiteId $suiteId -TestCaseIds @($wiId) -Confirm:$false
80
-
81
- $results += [PSCustomObject]@{
82
- localId = $tc.localId
83
- adoWiId = $wiId
84
- specFile = $tc.specFile
85
- title = $tc.title
86
- }
87
- } catch {
88
- Write-Warning " [FAILED] $($tc.localId) — $($_.Exception.Message)"
77
+ try {
78
+ $splatArgs = @{
79
+ Title = "[$($tc.localId)] $($tc.title)"
80
+ Confirm = $false
81
+ }
82
+ if ($steps.Count -gt 0) {
83
+ $splatArgs.Steps = $steps
84
+ $splatArgs.ExpectedResult = $expectedResult
85
+ }
86
+
87
+ $wi = New-AdoTestCase @splatArgs
88
+ $wiId = $wi.id
89
+ Write-Host " [CREATED] $($tc.localId) -> WI #$wiId"
90
+
91
+ Add-AdoTestCaseToSuite -PlanId $PlanId -SuiteId $suiteId -TestCaseIds @($wiId) -Confirm:$false
92
+
93
+ $results += [PSCustomObject]@{
94
+ localId = $tc.localId
95
+ adoWiId = $wiId
96
+ specFile = $tc.specFile
97
+ title = $tc.title
89
98
  }
99
+ } catch {
100
+ Write-Warning " [FAILED] $($tc.localId) -- $($_.Exception.Message)"
90
101
  }
91
102
  }
92
103
 
@@ -94,6 +105,6 @@ foreach ($tc in $mapping.testCases) {
94
105
  $outFile = if ($OutputFile) { $OutputFile } else {
95
106
  Join-Path (Split-Path $MappingFile) 'ado-ids.json'
96
107
  }
97
- $results | ConvertTo-Json -Depth 5 | Set-Content $outFile
108
+ $results | ConvertTo-Json -Depth 5 | Set-Content $outFile -Encoding UTF8
98
109
  Write-Host ""
99
110
  Write-Host "[create-testplan] Done. $($results.Count) WIs created. Output: $outFile"
@@ -3,94 +3,109 @@
3
3
  Prepends Azure DevOps Work Item IDs to Playwright spec file test titles.
4
4
 
5
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.
6
+ Reads one or more ado-ids-*.json files and updates every matching test()
7
+ call in the spec files to include the ADO WI ID as a [NNNNN] prefix.
8
+ This operation is IDEMPOTENT: if the ID is already present, it is skipped.
9
9
 
10
- This operation is IDEMPOTENT: if the ID is already present, it is not
11
- duplicated.
10
+ Two modes:
11
+ -MappingFile Process a single ado-ids.json file (backward-compatible).
12
+ -AdonIdsDir Process all ado-ids-*.json files found in a directory.
12
13
 
13
14
  .PARAMETER SpecDir
14
- Root directory containing Playwright spec files (.spec.ts).
15
- Searched recursively.
15
+ Root directory containing Playwright spec files (.spec.ts). Searched recursively.
16
16
 
17
17
  .PARAMETER MappingFile
18
- Path to ado-ids.json — array of { localId, adoWiId, specFile }.
18
+ Path to a single ado-ids.json file. Mutually exclusive with -AdonIdsDir.
19
+
20
+ .PARAMETER AdonIdsDir
21
+ Directory containing ado-ids-*.json files. All matching files are processed.
22
+ Mutually exclusive with -MappingFile.
23
+
24
+ .PARAMETER DryRun
25
+ Shows what would be changed without modifying any files.
19
26
 
20
27
  .EXAMPLE
21
- .\inject-ado-ids.ps1 `
22
- -SpecDir "qa/07-automation/e2e" `
23
- -MappingFile "qa/08-azure-integration/ado-ids.json"
28
+ # Single file
29
+ .\inject-ado-ids.ps1 -SpecDir "qa/07-automation/e2e/tests" -MappingFile "qa/08-azure-integration/ado-ids.json"
30
+
31
+ .EXAMPLE
32
+ # All ado-ids-*.json files in a directory
33
+ .\inject-ado-ids.ps1 -SpecDir "qa/07-automation/e2e/tests" -AdonIdsDir "qa/08-azure-integration"
24
34
 
25
35
  .NOTES
26
36
  Does NOT require a network connection or ADO token.
27
37
  Commit the modified spec files after running.
28
38
  #>
29
- [CmdletBinding(SupportsShouldProcess)]
39
+ [CmdletBinding()]
30
40
  param(
31
41
  [Parameter(Mandatory)]
32
42
  [string] $SpecDir,
33
43
 
34
- [Parameter(Mandatory)]
35
- [string] $MappingFile
44
+ [Parameter(ParameterSetName='SingleFile', Mandatory)]
45
+ [string] $MappingFile,
46
+
47
+ [Parameter(ParameterSetName='MultiFile', Mandatory)]
48
+ [string] $AdonIdsDir,
49
+
50
+ [switch] $DryRun
36
51
  )
37
52
 
38
53
  Set-StrictMode -Version Latest
39
54
  $ErrorActionPreference = 'Stop'
40
55
 
41
- # Load mapping
42
- if (-not (Test-Path $MappingFile)) {
43
- Write-Error "Mapping file not found: $MappingFile"
44
- exit 1
56
+ # Collect input files based on mode
57
+ $adoIdFiles = if ($PSCmdlet.ParameterSetName -eq 'SingleFile') {
58
+ if (-not (Test-Path $MappingFile)) { Write-Error "Not found: $MappingFile"; exit 1 }
59
+ @(Get-Item $MappingFile)
60
+ } else {
61
+ @(Get-ChildItem -Path $AdonIdsDir -Filter 'ado-ids-*.json')
45
62
  }
46
- $mapping = Get-Content $MappingFile -Raw | ConvertFrom-Json
47
63
 
48
- Write-Host "[inject-ado-ids] Loaded $($mapping.Count) mapping entries from $MappingFile"
64
+ Write-Host "[inject-ado-ids] Processing $($adoIdFiles.Count) mapping file(s)"
49
65
 
50
- $updated = 0
51
- $skipped = 0
66
+ $totalUpdated = 0
67
+ $totalSkipped = 0
52
68
 
53
- foreach ($entry in $mapping) {
54
- $adoId = $entry.adoWiId
55
- $localId = $entry.localId
69
+ foreach ($f in $adoIdFiles) {
70
+ $entries = Get-Content $f.FullName -Raw | ConvertFrom-Json
71
+ if ($entries -isnot [array]) { $entries = @($entries) }
72
+ Write-Host " $($f.Name) ($($entries.Count) entries)"
56
73
 
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)*" }
74
+ foreach ($entry in $entries) {
75
+ $localId = $entry.localId
76
+ $wiId = $entry.adoWiId
77
+ $specHint = $entry.specFile
61
78
 
62
- foreach ($file in $files) {
63
- $content = Get-Content $file.FullName -Raw
79
+ $specHintNorm = $specHint -replace '/', '\'
80
+ $specFiles = Get-ChildItem -Path $SpecDir -Recurse -Filter '*.spec.ts' |
81
+ Where-Object { $_.FullName -like "*$specHintNorm*" }
64
82
 
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)
83
+ foreach ($sf in $specFiles) {
84
+ $content = Get-Content $sf.FullName -Raw
85
+ $wiPrefix = "[$wiId]"
69
86
 
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
- }
87
+ # Idempotent: skip if this wiId is already associated with this localId
88
+ if ($content -match "$([regex]::Escape($wiPrefix)).*$([regex]::Escape($localId))") {
89
+ $totalSkipped++
90
+ continue
91
+ }
76
92
 
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++
93
+ # Inject [wiId] before the title of any test containing this localId
94
+ $escaped = [regex]::Escape($localId)
95
+ $pattern = "(test(?:\.(?:skip|fail|fixme))?\s*\(\s*['""])(?!\[$wiId\])([^'""]*$escaped)"
96
+ $replacement = "`${1}$wiPrefix `${2}"
97
+ $newContent = [regex]::Replace($content, $pattern, $replacement)
98
+
99
+ if ($newContent -ne $content) {
100
+ if (-not $DryRun) {
101
+ [System.IO.File]::WriteAllText($sf.FullName, $newContent, [System.Text.Encoding]::UTF8)
102
+ }
103
+ Write-Host " [UPDATED] $($sf.Name) -- injected $wiPrefix for $localId$(if($DryRun){' (dry run)'})"
104
+ $totalUpdated++
88
105
  }
89
- } else {
90
- Write-Verbose " [NO MATCH] $localId not found by pattern in $($file.Name)"
91
106
  }
92
107
  }
93
108
  }
94
109
 
95
110
  Write-Host ""
96
- Write-Host "[inject-ado-ids] Done. Updated: $updated | Skipped (already present): $skipped"
111
+ Write-Host "[inject-ado-ids] Done. Updated=$totalUpdated Skipped=$totalSkipped$(if($DryRun){' (DRY RUN)'})"
@@ -0,0 +1,196 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Generates a tc-mapping.json from a Plan-de-Pruebas markdown file.
4
+
5
+ .DESCRIPTION
6
+ Reads the "Tabla de Pruebas" table in a Plan-de-Pruebas markdown file,
7
+ skips rows with Tipo = "Bloqueado" (PENDING-CODE), splits Steps by <br>,
8
+ and outputs a tc-mapping.json compatible with create-testplan-from-mapping.ps1.
9
+
10
+ Paths are resolved relative to the current working directory (project root).
11
+ Always run this script from the project root.
12
+
13
+ .PARAMETER PlanFile
14
+ Path to the Plan-de-Pruebas-*.md file to parse (relative to project root or absolute).
15
+
16
+ .PARAMETER SpecFile
17
+ Relative path to the Playwright spec file (e.g. "reg/apl.spec.ts").
18
+ Stored as metadata in the output; not validated by this script.
19
+
20
+ .PARAMETER OutputFile
21
+ Path to write the tc-mapping.json output (relative to project root or absolute).
22
+
23
+ .PARAMETER PlanId
24
+ ADO Test Plan ID to embed in the JSON. Fill in after creating the plan (default: 0).
25
+
26
+ .PARAMETER SuiteId
27
+ ADO Test Suite ID to embed in the JSON. Fill in after creating the suite (default: 0).
28
+
29
+ .PARAMETER TableSection
30
+ Heading text of the table section to parse. Defaults to "Tabla de Pruebas".
31
+ Use the exact heading text if your plan uses a different section name
32
+ (e.g. "6. Casos de prueba").
33
+
34
+ .EXAMPLE
35
+ .\integrations\ado-powershell\scripts\md-to-tc-mapping.ps1 `
36
+ -PlanFile "qa/02-test-plans/sprints/Sprint-2/Plan-de-Pruebas-MyProject-Sprint-2-REG-APL.md" `
37
+ -SpecFile "reg/apl.spec.ts" `
38
+ -OutputFile "qa/08-azure-integration/tc-mapping-e2e-reg-apl.json"
39
+
40
+ .EXAMPLE
41
+ # For plans where the table is under a different heading:
42
+ .\integrations\ado-powershell\scripts\md-to-tc-mapping.ps1 `
43
+ -PlanFile "qa/02-test-plans/sprints/Sprint-10/Plan-de-Pruebas-MyProject-Sprint-10-MOD-API-Neg.md" `
44
+ -SpecFile "mod.api.negative.spec.ts" `
45
+ -OutputFile "qa/08-azure-integration/tc-mapping-api-mod-neg.json" `
46
+ -TableSection "6. Casos de prueba"
47
+ #>
48
+ param(
49
+ [Parameter(Mandatory)][string]$PlanFile,
50
+ [Parameter(Mandatory)][string]$SpecFile,
51
+ [Parameter(Mandatory)][string]$OutputFile,
52
+ [int]$PlanId = 0,
53
+ [int]$SuiteId = 0,
54
+ [string]$TableSection = 'Tabla de Pruebas'
55
+ )
56
+
57
+ Set-StrictMode -Version Latest
58
+ $ErrorActionPreference = 'Stop'
59
+
60
+ function Trim-Cell([string]$cell) { return $cell.Trim() }
61
+
62
+ # Resolve paths relative to current working directory (project root)
63
+ $WorkspaceRoot = (Get-Location).Path
64
+ if (-not [System.IO.Path]::IsPathRooted($PlanFile)) {
65
+ $PlanFile = Join-Path $WorkspaceRoot $PlanFile
66
+ }
67
+ if (-not [System.IO.Path]::IsPathRooted($OutputFile)) {
68
+ $OutputFile = Join-Path $WorkspaceRoot $OutputFile
69
+ }
70
+
71
+ if (-not (Test-Path $PlanFile)) {
72
+ throw "Plan file not found: $PlanFile"
73
+ }
74
+
75
+ $lines = Get-Content $PlanFile -Encoding UTF8
76
+
77
+ # Find the table section (configurable heading)
78
+ $tableStart = -1
79
+ for ($i = 0; $i -lt $lines.Count; $i++) {
80
+ if ($lines[$i] -match "##\s+$([regex]::Escape($TableSection))") {
81
+ $tableStart = $i + 1
82
+ break
83
+ }
84
+ }
85
+ if ($tableStart -lt 0) {
86
+ throw "Section '## $TableSection' not found in: $PlanFile"
87
+ }
88
+
89
+ # Find the header row (first line starting with |)
90
+ $headerIdx = -1
91
+ for ($i = $tableStart; $i -lt $lines.Count; $i++) {
92
+ if ($lines[$i].TrimStart().StartsWith('|')) {
93
+ $headerIdx = $i
94
+ break
95
+ }
96
+ }
97
+ if ($headerIdx -lt 0) {
98
+ throw "Table header row not found after '## $TableSection'"
99
+ }
100
+
101
+ # Parse header to find column indices dynamically
102
+ $headerCells = $lines[$headerIdx] -split '\|' | Where-Object { $_ -ne '' }
103
+ $colMap = @{}
104
+ for ($c = 0; $c -lt $headerCells.Count; $c++) {
105
+ $name = (Trim-Cell $headerCells[$c]).ToLower()
106
+ $colMap[$name] = $c
107
+ }
108
+
109
+ # Determine column indices (handle both E2E and API table formats)
110
+ $idxTcId = if ($colMap.ContainsKey('tc-id')) { $colMap['tc-id'] } else { 1 }
111
+ $idxTitulo = if ($colMap.ContainsKey('titulo')) { $colMap['titulo'] } else { 3 }
112
+ $idxSteps = if ($colMap.ContainsKey('steps')) { $colMap['steps'] } `
113
+ elseif ($colMap.ContainsKey('pasos')) { $colMap['pasos'] } else { 5 }
114
+ $idxResult = if ($colMap.ContainsKey('resultado esperado')) { $colMap['resultado esperado'] } else { 6 }
115
+ $idxTipo = if ($colMap.ContainsKey('tipo')) { $colMap['tipo'] } else { -1 }
116
+
117
+ # Data rows start after header + separator
118
+ $dataStart = $headerIdx + 2
119
+
120
+ $tcList = [System.Collections.Generic.List[hashtable]]::new()
121
+
122
+ for ($i = $dataStart; $i -lt $lines.Count; $i++) {
123
+ $line = $lines[$i].TrimStart()
124
+
125
+ # Stop at blank line or next section heading
126
+ if ($line -eq '' -or $line.StartsWith('#')) { break }
127
+ if (-not $line.StartsWith('|')) { continue }
128
+
129
+ # Split row into cells, strip leading empty element
130
+ $cells = $lines[$i] -split '\|'
131
+ if ($cells[0] -eq '' -or $cells[0] -match '^\s*$') {
132
+ $cells = $cells[1..($cells.Count - 1)]
133
+ }
134
+ if ($cells.Count -lt 3) { continue }
135
+
136
+ function Get-Cell([string[]]$arr, [int]$idx) {
137
+ if ($idx -ge 0 -and $idx -lt $arr.Count) { return (Trim-Cell $arr[$idx]) }
138
+ return ''
139
+ }
140
+
141
+ $tipo = Get-Cell $cells $idxTipo
142
+ $tcId = Get-Cell $cells $idxTcId
143
+ $titulo = Get-Cell $cells $idxTitulo
144
+ $steps = Get-Cell $cells $idxSteps
145
+ $result = Get-Cell $cells $idxResult
146
+
147
+ # Skip sub-header rows or empty rows
148
+ if ($tcId -eq '' -or $tcId -eq 'TC-ID') { continue }
149
+
150
+ # Skip blocked TCs (PENDING-CODE)
151
+ if ($tipo -ieq 'Bloqueado') {
152
+ Write-Host " [skip] $tcId - Bloqueado (PENDING-CODE)" -ForegroundColor DarkYellow
153
+ continue
154
+ }
155
+
156
+ # Fallback: use Descripcion column as title if Titulo is absent
157
+ if ($titulo -eq '' -or $titulo -eq 'N/A') {
158
+ $idxDesc = if ($colMap.ContainsKey('descripcion')) { $colMap['descripcion'] } else { 1 }
159
+ $titulo = Get-Cell $cells $idxDesc
160
+ }
161
+
162
+ # Parse steps: split on <br>, strip leading numbering (e.g. "1. ", "2) ")
163
+ $stepList = @()
164
+ if ($steps -ne '' -and $steps -ne 'N/A') {
165
+ $rawSteps = $steps -split '(?i)<br\s*/?>'
166
+ foreach ($s in $rawSteps) {
167
+ $cleaned = $s.Trim() -replace '^\d+[\.\)]\s*', ''
168
+ if ($cleaned -ne '') { $stepList += $cleaned }
169
+ }
170
+ }
171
+
172
+ $tcList.Add(@{
173
+ localId = $tcId
174
+ title = $titulo
175
+ specFile = $SpecFile
176
+ steps = $stepList
177
+ expectedResult = $result
178
+ })
179
+ }
180
+
181
+ $output = @{
182
+ planId = $PlanId
183
+ suiteId = $SuiteId
184
+ testCases = @($tcList)
185
+ }
186
+
187
+ # Ensure output directory exists
188
+ $outputDir = Split-Path $OutputFile
189
+ if (-not (Test-Path $outputDir)) {
190
+ New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
191
+ }
192
+
193
+ $json = $output | ConvertTo-Json -Depth 10
194
+ [System.IO.File]::WriteAllText($OutputFile, $json, [System.Text.Encoding]::UTF8)
195
+
196
+ Write-Host "(ok) Generated $($tcList.Count) TC mappings -> $OutputFile" -ForegroundColor Green
@@ -1,4 +1,4 @@
1
- # Integration — playwright-azure-reporter
1
+ # Integration - playwright-azure-reporter
2
2
 
3
3
  Connects Playwright test results to Azure DevOps Test Plans automatically.
4
4
 
@@ -8,42 +8,98 @@ Connects Playwright test results to Azure DevOps Test Plans automatically.
8
8
  npm install --save-dev @alex_neo/playwright-azure-reporter
9
9
  ```
10
10
 
11
- ## Required environment variables
11
+ ## Environment variables
12
+
13
+ ### Local development (`.env`)
12
14
 
13
15
  ```
14
- ADO_ORG_URL=https://dev.azure.com/your-org
15
- ADO_PROJECT_NAME=YourProject
16
+ ADO_ORG=<org-slug>
17
+ ADO_PROJECT=<project-name>
16
18
  ADO_PLAN_ID=<plan-id>
17
- AZURE_TOKEN=<personal-access-token>
19
+ AZURE_TOKEN=<personal-access-token> # PAT with Test Management: Read & Write
20
+ ```
21
+
22
+ > NEVER commit `.env` files to source control.
23
+
24
+ ### CI/CD pipeline (`QA-Variables` Library Variable Group)
25
+
26
+ In the pipeline, `SYSTEM_ACCESSTOKEN` (OAuth) is used instead of a PAT -
27
+ no `AZURE_TOKEN` is needed. The reporter accepts both authentication methods.
28
+
29
+ ```
30
+ ADO_ORG=<org-slug>
31
+ ADO_PROJECT=<project-name>
32
+ ADO_E2E_PLAN_ID=<plan-id>
33
+ ADO_API_PLAN_ID=<plan-id>
18
34
  ```
19
35
 
20
- > These variables MUST be injected via CI secrets (Azure Pipeline Library Variable Group).
21
- > NEVER store tokens in `.env` files committed to source control.
36
+ `SYSTEM_ACCESSTOKEN` is injected automatically by the pipeline via `$(System.AccessToken)`.
22
37
 
23
38
  ## playwright.config.ts snippet
24
39
 
25
40
  ```typescript
26
- import type { PlaywrightTestConfig } from '@playwright/test';
41
+ import { defineConfig } from '@playwright/test';
27
42
 
28
- const config: PlaywrightTestConfig = {
43
+ export default defineConfig({
29
44
  reporter: [
30
45
  ['list'],
31
46
  ['html', { open: 'never' }],
32
47
  ['@alex_neo/playwright-azure-reporter', {
33
- token: process.env.AZURE_TOKEN!,
34
- planId: Number(process.env.ADO_PLAN_ID),
35
- projectName: process.env.ADO_PROJECT_NAME!,
36
- orgUrl: process.env.ADO_ORG_URL!,
37
- testRunTitle: `[Automated] ${new Date().toISOString().slice(0, 10)}`,
48
+ token: process.env.SYSTEM_ACCESSTOKEN ?? process.env.AZURE_TOKEN!,
49
+ planId: Number(process.env.ADO_PLAN_ID),
50
+ projectName: process.env.ADO_PROJECT!,
51
+ orgUrl: `https://dev.azure.com/${process.env.ADO_ORG}`,
52
+ testRunTitle: `[Automated] ${new Date().toISOString().slice(0, 10)}`,
38
53
  publishResultsOnFailure: true,
39
- isDisabled: process.env.ADO_SYNC_DISABLED === 'true',
40
54
  }],
41
55
  ],
42
- };
56
+ });
57
+ ```
58
+
59
+ > `token` falls back to `AZURE_TOKEN` when `SYSTEM_ACCESSTOKEN` is not present (local runs).
60
+
61
+ ## Playwright Workspaces reporter (optional)
62
+
63
+ To also publish results to the Azure Playwright Workspaces dashboard, add
64
+ `@azure/playwright` alongside `@alex_neo/playwright-azure-reporter`.
43
65
 
44
- export default config;
66
+ ```bash
67
+ npm install --save-dev @azure/playwright
45
68
  ```
46
69
 
70
+ Create a `playwright.service.config.ts` file next to `playwright.config.ts`:
71
+
72
+ ```typescript
73
+ import { defineConfig } from '@playwright/test';
74
+ import { createAzurePlaywrightConfig } from '@azure/playwright';
75
+ import { DefaultAzureCredential } from '@azure/identity';
76
+ import config from './playwright.config';
77
+
78
+ export default defineConfig(
79
+ config,
80
+ createAzurePlaywrightConfig(config, {
81
+ credential: new DefaultAzureCredential(),
82
+ })
83
+ );
84
+ ```
85
+
86
+ Run using the service config in CI:
87
+
88
+ ```bash
89
+ npx playwright test -c playwright.service.config.ts
90
+ ```
91
+
92
+ Required variable (add to `QA-Variables` Library Variable Group):
93
+
94
+ ```
95
+ PLAYWRIGHT_SERVICE_URL=<workspace-endpoint-url> # Azure Portal -> Playwright workspace -> Get Started
96
+ ```
97
+
98
+ Authentication uses Microsoft Entra ID via the pipeline's Azure service connection
99
+ (`AzureCLI@2` login task). No `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` is needed when
100
+ Entra ID is configured. See `pipelines/azure-pipeline-qa.yml` for the commented
101
+ placeholder steps.
102
+
47
103
  ## Test case title format (required for ADO sync)
48
104
 
49
105
  The reporter matches test titles to ADO Work Items by the numeric prefix.
@@ -73,16 +129,11 @@ file/describe block to its ADO Test Suite. Example:
73
129
  }
74
130
  ```
75
131
 
76
- ## Disabling ADO sync locally
77
-
78
- Set `ADO_SYNC_DISABLED=true` in your `.env` to skip reporting while running
79
- tests locally.
80
-
81
132
  ## Troubleshooting
82
133
 
83
134
  | Symptom | Cause | Fix |
84
135
  |---------|-------|-----|
85
- | `401 Unauthorized` | Expired PAT | Regenerate PAT in ADO → User Settings |
136
+ | `401 Unauthorized` (local) | Expired PAT | Regenerate PAT in ADO -> User Settings |
137
+ | `401 Unauthorized` (pipeline) | `SYSTEM_ACCESSTOKEN` not passed | Verify `env: SYSTEM_ACCESSTOKEN: $(System.AccessToken)` in the pipeline step |
86
138
  | `Test case not found` | WI ID mismatch | Re-run `inject-ado-ids.ps1` |
87
- | Results not published | `isDisabled: true` | Remove or set to `false` |
88
139
  | Duplicate test runs | Reporter called twice | Check for duplicate reporter arrays |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keber/qa-framework",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Reusable spec-driven QA framework for IDE-agent-assisted automated testing. Installable as an npm package. Provides structure, templates, agent instructions, and optional integrations for Playwright and Azure DevOps.",
5
5
  "keywords": [
6
6
  "qa",