@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 +57 -0
- package/README.md +1 -1
- package/integrations/ado-powershell/README.md +47 -13
- package/integrations/ado-powershell/scripts/create-testplan-from-mapping.ps1 +54 -58
- package/integrations/ado-powershell/scripts/inject-ado-ids.ps1 +70 -55
- package/integrations/ado-powershell/scripts/md-to-tc-mapping.ps1 +196 -0
- package/package.json +1 -1
- package/scripts/init.js +34 -0
- package/templates/automation-scaffold/package.json +1 -1
- package/templates/temp-script.ps1 +74 -0
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
|
@@ -7,27 +7,37 @@ 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
|
|
|
15
16
|
## Prerequisites
|
|
16
17
|
|
|
17
18
|
- PowerShell 7+ (recommended) or Windows PowerShell 5.1
|
|
18
|
-
-
|
|
19
|
-
- PAT
|
|
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
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
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
|
-
{
|
|
63
|
-
|
|
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
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
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
|
|
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
|
-
#
|
|
54
|
-
|
|
55
|
-
$
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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 $
|
|
72
|
-
$
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
$
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keber/qa-framework",
|
|
3
|
-
"version": "1.
|
|
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
|
// -----------------------------------------------------------------------
|
|
@@ -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
|
+
}
|