@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 +68 -0
- package/README.md +1 -1
- package/integrations/ado-powershell/README.md +15 -6
- package/integrations/ado-powershell/pipelines/azure-pipeline-qa.yml +209 -64
- package/integrations/ado-powershell/scripts/create-testplan-from-mapping.ps1 +41 -30
- package/integrations/ado-powershell/scripts/inject-ado-ids.ps1 +70 -55
- package/integrations/ado-powershell/scripts/md-to-tc-mapping.ps1 +196 -0
- package/integrations/playwright-azure-reporter/README.md +75 -24
- package/package.json +1 -1
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
|
@@ -7,8 +7,9 @@ All scripts are parameterized and project-agnostic.
|
|
|
7
7
|
|
|
8
8
|
| Script | Purpose |
|
|
9
9
|
|--------|---------|
|
|
10
|
-
| `scripts/
|
|
11
|
-
| `scripts/create-testplan-from-mapping.ps1` | Create ADO Test Cases from
|
|
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.
|
|
29
|
-
.\scripts\
|
|
30
|
-
-
|
|
31
|
-
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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:
|
|
63
|
+
- name: E2E_DIR
|
|
34
64
|
value: 'qa/07-automation/e2e'
|
|
35
|
-
- name:
|
|
36
|
-
value: '
|
|
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 &
|
|
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
|
|
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 $(
|
|
87
|
+
cd $(E2E_DIR)
|
|
58
88
|
npm ci
|
|
59
89
|
npx playwright install chromium --with-deps
|
|
60
|
-
displayName: 'Install
|
|
90
|
+
displayName: 'Install E2E deps and Playwright Chromium'
|
|
61
91
|
|
|
62
92
|
- task: CacheBeta@1
|
|
63
93
|
inputs:
|
|
64
|
-
key: '
|
|
65
|
-
path: '
|
|
66
|
-
|
|
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:
|
|
76
|
-
displayName: 'Run
|
|
77
|
-
timeoutInMinutes:
|
|
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 $(
|
|
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 $(
|
|
91
|
-
npx playwright test --
|
|
92
|
-
|
|
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:
|
|
95
|
-
QA_USER_EMAIL:
|
|
96
|
-
QA_USER_PASSWORD:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 $(
|
|
106
|
-
npx playwright test --grep "
|
|
107
|
-
displayName: 'Run
|
|
108
|
-
|
|
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:
|
|
111
|
-
QA_USER_EMAIL:
|
|
112
|
-
QA_USER_PASSWORD:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
CI:
|
|
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:
|
|
123
|
-
testResultsFiles:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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:
|
|
273
|
+
- task: PublishPipelineArtifact@1
|
|
129
274
|
condition: always()
|
|
130
275
|
inputs:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
displayName: 'Publish
|
|
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
|
-
#
|
|
44
|
+
# Auto-load ADO session if not already active
|
|
45
45
|
if (-not (Get-Command 'New-AdoTestCase' -ErrorAction SilentlyContinue)) {
|
|
46
|
-
|
|
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
|
-
|
|
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 $
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
$
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
$
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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(
|
|
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
|
-
#
|
|
42
|
-
if (
|
|
43
|
-
Write-Error "
|
|
44
|
-
|
|
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]
|
|
64
|
+
Write-Host "[inject-ado-ids] Processing $($adoIdFiles.Count) mapping file(s)"
|
|
49
65
|
|
|
50
|
-
$
|
|
51
|
-
$
|
|
66
|
+
$totalUpdated = 0
|
|
67
|
+
$totalSkipped = 0
|
|
52
68
|
|
|
53
|
-
foreach ($
|
|
54
|
-
$
|
|
55
|
-
$
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
foreach ($entry in $entries) {
|
|
75
|
+
$localId = $entry.localId
|
|
76
|
+
$wiId = $entry.adoWiId
|
|
77
|
+
$specHint = $entry.specFile
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
$
|
|
79
|
+
$specHintNorm = $specHint -replace '/', '\'
|
|
80
|
+
$specFiles = Get-ChildItem -Path $SpecDir -Recurse -Filter '*.spec.ts' |
|
|
81
|
+
Where-Object { $_.FullName -like "*$specHintNorm*" }
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
$escapedId = [regex]::Escape($localId)
|
|
83
|
+
foreach ($sf in $specFiles) {
|
|
84
|
+
$content = Get-Content $sf.FullName -Raw
|
|
85
|
+
$wiPrefix = "[$wiId]"
|
|
69
86
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
$
|
|
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
|
|
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
|
|
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
|
-
##
|
|
11
|
+
## Environment variables
|
|
12
|
+
|
|
13
|
+
### Local development (`.env`)
|
|
12
14
|
|
|
13
15
|
```
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
41
|
+
import { defineConfig } from '@playwright/test';
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
export default defineConfig({
|
|
29
44
|
reporter: [
|
|
30
45
|
['list'],
|
|
31
46
|
['html', { open: 'never' }],
|
|
32
47
|
['@alex_neo/playwright-azure-reporter', {
|
|
33
|
-
token:
|
|
34
|
-
planId:
|
|
35
|
-
projectName:
|
|
36
|
-
orgUrl:
|
|
37
|
-
testRunTitle:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|