@keber/qa-framework 1.8.0 → 1.9.1

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,63 @@ has been removed or parameterized. See `docs/comparison-matrix.md` and `docs/gen
33
33
 
34
34
  ---
35
35
 
36
+ ## [1.9.1] - 2026-05-15
37
+
38
+ ### Added
39
+
40
+ - **`integrations/ado-powershell/scripts/md-to-tc-mapping.ps1`** — new script that parses a
41
+ Plan-de-Pruebas markdown file and generates `tc-mapping.json`. Reads the Tabla de Pruebas
42
+ table, detects columns dynamically, skips Bloqueado rows (PENDING-CODE), splits steps by
43
+ `<br>`, and strips leading numbering. Supports a `-TableSection` parameter for plans with
44
+ non-standard headings. This was previously missing from the framework, forcing agents to
45
+ implement ad-hoc parsers in each project.
46
+
47
+ ### Fixed
48
+
49
+ - **`create-testplan-from-mapping.ps1`** — removed Unicode characters that caused
50
+ `MissingArrayIndexExpression` parser errors in PowerShell 5.1.
51
+ - **`create-testplan-from-mapping.ps1`** — added auto-load of `ado-powershell` skill when
52
+ `New-AdoTestCase` is not yet in scope; script no longer requires manual dot-sourcing before
53
+ invocation.
54
+ - **`create-testplan-from-mapping.ps1`** — added fallback to `$mapping.items` when
55
+ `$mapping.testCases` is absent, for compatibility with mappings generated by older sessions.
56
+ - **`inject-ado-ids.ps1`** — fixed broken regex (`\Q...\E` is not valid PowerShell); replaced
57
+ with `[regex]::Escape()`. Added `-AdonIdsDir` parameter to process all `ado-ids-*.json`
58
+ files in a directory in one pass. Added `-DryRun` switch. Fixed UTF-8 encoding on write.
59
+
60
+ ---
61
+
62
+ ## [1.9.0] - 2026-05-15
63
+
64
+ ### Changed
65
+
66
+ - **`create-testplan-from-mapping.ps1` refactored** — removed all ADO-specific logic (auth headers,
67
+ `Invoke-RestMethod` calls, XML step construction). The script now delegates to `New-AdoTestCase`
68
+ and `Add-AdoTestCaseToSuite` from `@keber/ado-powershell`, keeping `qa-framework` free of
69
+ ADO implementation details. Requires `@keber/ado-powershell` ≥ 1.3.0.
70
+ - **Script parameters simplified** — `-OrgUrl`, `-ProjectName`, and `-Token` removed from
71
+ `create-testplan-from-mapping.ps1`. Caller is responsible for loading the ADO session via
72
+ `. .github/skills/ado-powershell/load.ps1` before invoking the script.
73
+ - **`tc-mapping.json` schema extended** — added optional `steps` (string array) and
74
+ `expectedResult` (string) fields per test case. When present, steps are sent to ADO as
75
+ `Microsoft.VSTS.TCM.Steps`; all steps except the last are `ActionStep`, the last is
76
+ `ValidateStep` with `expectedResult`.
77
+ - **`qa-ado-integration` SKILL.md updated** — Step 3 now specifies exactly how to parse the
78
+ Tabla de Pruebas columns (`Steps` split on `<br>`, strip numbering prefix) to produce
79
+ `tc-mapping.json` with steps.
80
+
81
+ ### Added
82
+
83
+ - **Guard in `create-testplan-from-mapping.ps1`** — fails fast with a clear message if
84
+ `New-AdoTestCase` is not available (ADO session not loaded).
85
+
86
+ ### Requires
87
+
88
+ - `@keber/ado-powershell` ≥ **1.3.0** — adds `$ExpectedResult` parameter to `New-AdoTestCase`
89
+ and `ValidateStep` support in the generated TCM steps XML.
90
+
91
+ ---
92
+
36
93
  ## [1.8.0] - 2026-05-07
37
94
 
38
95
  ### 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.8.0**
4
+ > Current version: **v1.9.0**
5
5
 
6
6
  ---
7
7
 
@@ -7,27 +7,37 @@ 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
 
15
16
  ## Prerequisites
16
17
 
17
18
  - 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
19
+ - `@keber/ado-powershell` skill installed and loaded (provides `New-AdoTestCase`, `Add-AdoTestCaseToSuite`, etc.)
20
+ - Azure DevOps Personal Access Token (PAT) with `Work Items: Read & Write` and `Test Management: Read & Write` scopes
21
+ - PAT and org/project set as env vars: `ADO_PAT`, `ADO_ORG`, `ADO_PROJECT`
20
22
 
21
23
  ## Quick start
22
24
 
23
25
  ```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
26
+ # 0. Load the ADO skill (initialises session from env vars)
27
+ . .github/skills/ado-powershell/load.ps1
28
+
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"
31
41
 
32
42
  # 2. Inject ADO IDs into spec files (idempotent)
33
43
  .\scripts\inject-ado-ids.ps1 `
@@ -59,12 +69,36 @@ npx playwright test
59
69
  "planId": 22304,
60
70
  "suiteId": 22305,
61
71
  "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" }
72
+ {
73
+ "localId": "TC-SUP-CR-001",
74
+ "title": "Create supplier @P0",
75
+ "specFile": "suppliers/create.spec.ts",
76
+ "steps": [
77
+ "Navigate to /suppliers/new",
78
+ "Fill in required fields: Name, RUT, Email",
79
+ "Click 'Save'"
80
+ ],
81
+ "expectedResult": "Supplier is created and appears in the list with status Active"
82
+ },
83
+ {
84
+ "localId": "TC-SUP-CR-002",
85
+ "title": "Create supplier - required fields @P0",
86
+ "specFile": "suppliers/create.spec.ts",
87
+ "steps": [
88
+ "Navigate to /suppliers/new",
89
+ "Leave required fields empty",
90
+ "Click 'Save'"
91
+ ],
92
+ "expectedResult": "Validation errors appear for each required field; record is not saved"
93
+ }
64
94
  ]
65
95
  }
66
96
  ```
67
97
 
98
+ > **`steps`** (optional): Array of step strings. Each entry becomes one ADO step. All steps except the last are `ActionStep`; the last is `ValidateStep` and receives `expectedResult`.
99
+ > **`expectedResult`** (optional): Overall expected result, applied to the last step. If omitted, the last step has an empty expected result.
100
+ > If `steps` is absent or empty, the Test Case is created with title only (no steps).
101
+
68
102
  ## File: ado-ids.json (output after create-testplan)
69
103
 
70
104
  ```json
@@ -7,17 +7,16 @@
7
7
  corresponding Test Case Work Items in a specified ADO Test Plan/Suite.
8
8
  Outputs ado-ids.json with the resulting WI IDs for use by inject-ado-ids.ps1.
9
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.
10
+ Requires an active ADO session. Call:
11
+ . .github/skills/ado-powershell/load.ps1
12
+ before running this script. The session is initialised automatically by
13
+ load.ps1 when ADO_ORG, ADO_PROJECT and ADO_PAT env vars are set.
15
14
 
16
15
  .PARAMETER PlanId
17
16
  ADO Test Plan ID.
18
17
 
19
18
  .PARAMETER SuiteId
20
- ADO Test Suite ID. If omitted, uses the plan's root suite.
19
+ ADO Test Suite ID. If omitted, uses the suiteId from tc-mapping.json.
21
20
 
22
21
  .PARAMETER MappingFile
23
22
  Path to tc-mapping.json input file.
@@ -25,83 +24,80 @@
25
24
  .PARAMETER OutputFile
26
25
  Path to write ado-ids.json output. Default: ado-ids.json in same dir as MappingFile.
27
26
 
28
- .PARAMETER Token
29
- ADO Personal Access Token. Prefer passing via $env:AZURE_TOKEN.
30
-
31
27
  .EXAMPLE
28
+ . .github/skills/ado-powershell/load.ps1
32
29
  .\create-testplan-from-mapping.ps1 `
33
- -OrgUrl "https://dev.azure.com/your-org" `
34
- -ProjectName "YourProject" `
35
30
  -PlanId 22304 `
36
- -MappingFile "qa/08-azure-integration/tc-mapping.json" `
37
- -Token $env:AZURE_TOKEN
31
+ -MappingFile "qa/08-azure-integration/tc-mapping.json"
38
32
  #>
39
33
  [CmdletBinding(SupportsShouldProcess)]
40
34
  param(
41
- [Parameter(Mandatory)] [string] $OrgUrl,
42
- [Parameter(Mandatory)] [string] $ProjectName,
43
35
  [Parameter(Mandatory)] [int] $PlanId,
44
36
  [int] $SuiteId = 0,
45
37
  [Parameter(Mandatory)] [string] $MappingFile,
46
- [string] $OutputFile = '',
47
- [Parameter(Mandatory)] [string] $Token
38
+ [string] $OutputFile = ''
48
39
  )
49
40
 
50
41
  Set-StrictMode -Version Latest
51
42
  $ErrorActionPreference = 'Stop'
52
43
 
53
- # Build auth header
54
- $b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$Token"))
55
- $headers = @{
56
- 'Authorization' = "Basic $b64"
57
- 'Content-Type' = 'application/json'
44
+ # Auto-load ADO session if not already active
45
+ if (-not (Get-Command 'New-AdoTestCase' -ErrorAction SilentlyContinue)) {
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)."
57
+ exit 1
58
58
  }
59
59
 
60
60
  # Load mapping
61
61
  if (-not (Test-Path $MappingFile)) { Write-Error "Not found: $MappingFile"; exit 1 }
62
62
  $mapping = Get-Content $MappingFile -Raw | ConvertFrom-Json
63
63
 
64
- $apiBase = "$OrgUrl/$ProjectName/_apis"
65
64
  $suiteId = if ($SuiteId -gt 0) { $SuiteId } else { $mapping.suiteId }
66
65
 
67
- 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"
68
70
 
69
71
  $results = @()
70
72
 
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)"
73
+ foreach ($tc in $tcList) {
74
+ $steps = if ($tc.PSObject.Properties['steps']) { $tc.steps } else { @() }
75
+ $expectedResult = if ($tc.PSObject.Properties['expectedResult']) { $tc.expectedResult } else { '' }
76
+
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
104
98
  }
99
+ } catch {
100
+ Write-Warning " [FAILED] $($tc.localId) -- $($_.Exception.Message)"
105
101
  }
106
102
  }
107
103
 
@@ -109,6 +105,6 @@ foreach ($tc in $mapping.testCases) {
109
105
  $outFile = if ($OutputFile) { $OutputFile } else {
110
106
  Join-Path (Split-Path $MappingFile) 'ado-ids.json'
111
107
  }
112
- $results | ConvertTo-Json -Depth 5 | Set-Content $outFile
108
+ $results | ConvertTo-Json -Depth 5 | Set-Content $outFile -Encoding UTF8
113
109
  Write-Host ""
114
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keber/qa-framework",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
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",
package/scripts/init.js CHANGED
@@ -341,6 +341,40 @@ cd qa/07-automation/e2e && npm install && npx playwright install chromium
341
341
  const nextStepsPath = path.join(qaRoot, 'AGENT-NEXT-STEPS.md');
342
342
  writeIfMissing(nextStepsPath, nextStepsContent);
343
343
 
344
+ // --- Print post-install summary ---
345
+ const playwrightInstalled = fs.existsSync(path.join(cwd, 'node_modules', '@playwright', 'test'));
346
+ const adoQaInstalledFinal = fs.existsSync(path.join(cwd, 'node_modules', '@keber', 'ado-qa'));
347
+ const azureReporterInstalled = fs.existsSync(path.join(cwd, 'node_modules', '@alex_neo', 'playwright-azure-reporter'));
348
+
349
+ console.log('');
350
+ console.log(' @keber/qa-framework — scaffold complete');
351
+ console.log(' ----------------------------------------');
352
+ console.log(' Installed:');
353
+ console.log(' qa/ QA directory structure + spec templates');
354
+ console.log(' .github/skills/ QA agent skills (8 stages)');
355
+ console.log(' .github/instructions/ qa-framework.instructions.md');
356
+ console.log('');
357
+ console.log(' Optional integrations:');
358
+ if (playwrightInstalled) {
359
+ console.log(' [ok] Playwright @playwright/test');
360
+ } else {
361
+ console.log(' [ ] Playwright npm install @playwright/test');
362
+ console.log(' cd qa/07-automation/e2e && npm install && npx playwright install chromium');
363
+ }
364
+ if (adoQaInstalledFinal) {
365
+ console.log(' [ok] ADO integration @keber/ado-qa');
366
+ } else {
367
+ console.log(' [ ] ADO integration npm install @keber/ado-qa');
368
+ }
369
+ if (azureReporterInstalled) {
370
+ console.log(' [ok] ADO Playwright reporter (installed)');
371
+ } else {
372
+ console.log(' [ ] ADO Playwright reporter npm install @alex_neo/playwright-azure-reporter');
373
+ }
374
+ console.log('');
375
+ console.log(' Next: edit qa/qa-framework.config.json, then read qa/AGENT-NEXT-STEPS.md');
376
+ console.log('');
377
+
344
378
  // -----------------------------------------------------------------------
345
379
  // Helpers
346
380
  // -----------------------------------------------------------------------
@@ -19,6 +19,6 @@
19
19
  "dotenv": "^16.0.0"
20
20
  },
21
21
  "optionalDependencies": {
22
- "@alex_neo/playwright-azure-reporter": "^2.0.0"
22
+ "@alex_neo/playwright-azure-reporter": "^1.20.0"
23
23
  }
24
24
  }
@@ -0,0 +1,74 @@
1
+ function New-AdoTestCase {
2
+ <#
3
+ .SYNOPSIS
4
+ Creates a Test Case work item with optional steps.
5
+ Wraps New-AdoWorkItem -Type 'Test Case' and handles TCM-specific fields.
6
+
7
+ .PARAMETER Title Title of the Test Case (mandatory).
8
+ .PARAMETER Steps Ordered array of plain-text action strings.
9
+ Encoded as the XML format required by Microsoft.VSTS.TCM.Steps.
10
+ .PARAMETER Priority 1 (Critical) | 2 (High) | 3 (Medium) | 4 (Low).
11
+ .PARAMETER ExtraFields Hashtable of additional TCM or custom fields.
12
+
13
+ .EXAMPLE
14
+ New-AdoTestCase -Title 'Verify login with valid credentials' `
15
+ -Steps @('Navigate to /login', 'Enter username and password', 'Click Sign In') `
16
+ -Priority '2' -AssignedTo 'qa@contoso.com' -Tags 'login; smoke'
17
+ #>
18
+ [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Low')]
19
+ param(
20
+ [Parameter(Mandatory)][string]$Title,
21
+ [string]$Description,
22
+ [string]$AssignedTo,
23
+ [string]$AreaPath,
24
+ [string]$IterationPath,
25
+ [string]$Tags,
26
+ [ValidateSet('Design','Ready','Closed')]
27
+ [string]$State,
28
+ [string[]]$Steps,
29
+ [ValidateSet('1','2','3','4')]
30
+ [string]$Priority,
31
+ [hashtable]$ExtraFields = @{},
32
+ [string]$Org = $script:AdoSession.Org,
33
+ [string]$Project = $script:AdoSession.Project,
34
+ [string]$ApiV = $script:AdoSession.ApiV,
35
+ [hashtable]$Headers = $script:AdoSession.Headers
36
+ )
37
+
38
+ $extra = $ExtraFields.Clone()
39
+
40
+ if ($Priority) {
41
+ $extra['Microsoft.VSTS.Common.Priority'] = $Priority
42
+ }
43
+
44
+ if ($Steps -and $Steps.Count -gt 0) {
45
+ $stepsXml = '<steps id="0" last="{0}">' -f $Steps.Count
46
+ $stepIndex = 1
47
+ foreach ($action in $Steps) {
48
+ $escaped = [System.Security.SecurityElement]::Escape($action)
49
+ $stepsXml += '<step id="{0}" type="ActionStep"><parameterizedString isformatted="true">{1}</parameterizedString><parameterizedString isformatted="true"/></step>' -f $stepIndex, $escaped
50
+ $stepIndex++
51
+ }
52
+ $stepsXml += '</steps>'
53
+ $extra['Microsoft.VSTS.TCM.Steps'] = $stepsXml
54
+ }
55
+
56
+ $splatArgs = @{
57
+ Type = 'Test Case'
58
+ Title = $Title
59
+ ExtraFields = $extra
60
+ Org = $Org
61
+ Project = $Project
62
+ ApiV = $ApiV
63
+ Headers = $Headers
64
+ }
65
+ if ($Description) { $splatArgs.Description = $Description }
66
+ if ($AssignedTo) { $splatArgs.AssignedTo = $AssignedTo }
67
+ if ($AreaPath) { $splatArgs.AreaPath = $AreaPath }
68
+ if ($IterationPath) { $splatArgs.IterationPath = $IterationPath }
69
+ if ($Tags) { $splatArgs.Tags = $Tags }
70
+ if ($State) { $splatArgs.State = $State }
71
+
72
+ # WhatIf / Confirm propagate automatically via $WhatIfPreference / $ConfirmPreference
73
+ return New-AdoWorkItem @splatArgs
74
+ }