@provartesting/provardx-cli 1.5.2 → 1.6.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/README.md +5 -37
- package/lib/commands/provar/automation/project/validate.js +5 -3
- package/lib/commands/provar/automation/project/validate.js.map +1 -1
- package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +269 -79
- package/lib/mcp/docs/VALIDATION_RULE_REGISTRY.md +225 -0
- package/lib/mcp/prompts/loopPrompts.js +4 -3
- package/lib/mcp/prompts/loopPrompts.js.map +1 -1
- package/lib/mcp/rules/comparisonTypeSets.d.ts +21 -0
- package/lib/mcp/rules/comparisonTypeSets.js +45 -0
- package/lib/mcp/rules/comparisonTypeSets.js.map +1 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +178 -8
- package/lib/mcp/rules/provar_layer1_rules.json +151 -0
- package/lib/mcp/rules/provar_test_step_schema.json +3005 -0
- package/lib/mcp/server.d.ts +15 -0
- package/lib/mcp/server.js +64 -1
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/automationTools.js +38 -1
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +1368 -10
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
- package/lib/mcp/tools/hierarchyValidate.d.ts +2 -1
- package/lib/mcp/tools/hierarchyValidate.js +7 -1
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +1 -2
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +23 -0
- package/lib/mcp/tools/sfSpawn.js +72 -1
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/testCaseGenerate.js +377 -23
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseValidate.d.ts +46 -0
- package/lib/mcp/tools/testCaseValidate.js +313 -43
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +3 -2
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +3 -2
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
- package/lib/mcp/tools/uiActionApiIds.d.ts +23 -0
- package/lib/mcp/tools/uiActionApiIds.js +46 -0
- package/lib/mcp/tools/uiActionApiIds.js.map +1 -0
- package/lib/mcp/utils/qualityThreshold.d.ts +8 -0
- package/lib/mcp/utils/qualityThreshold.js +42 -0
- package/lib/mcp/utils/qualityThreshold.js.map +1 -0
- package/lib/mcp/utils/testCaseId.d.ts +23 -0
- package/lib/mcp/utils/testCaseId.js +156 -0
- package/lib/mcp/utils/testCaseId.js.map +1 -0
- package/lib/services/projectValidation.js +2 -1
- package/lib/services/projectValidation.js.map +1 -1
- package/messages/sf.provar.automation.project.validate.md +1 -1
- package/messages/sf.provar.mcp.start.md +1 -0
- package/oclif.manifest.json +4 -4
- package/package.json +4 -2
|
@@ -12,8 +12,10 @@ import { z } from 'zod';
|
|
|
12
12
|
import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js';
|
|
13
13
|
import { makeError, makeRequestId } from '../schemas/common.js';
|
|
14
14
|
import { log } from '../logging/logger.js';
|
|
15
|
+
import { allocateTestCaseId, DEFAULT_TESTCASE_ID } from '../utils/testCaseId.js';
|
|
15
16
|
import { validateTestCase } from './testCaseValidate.js';
|
|
16
17
|
import { desc } from './descHelper.js';
|
|
18
|
+
import { UI_ACTION_API_IDS, UI_SCREEN_CONTAINER_API_IDS, UI_LOCATOR_BEARING_API_IDS } from './uiActionApiIds.js';
|
|
17
19
|
// ── Shorthand → fully-qualified API ID map ────────────────────────────────────
|
|
18
20
|
// Provar runtime requires fully-qualified IDs. Shorthand forms are accepted here
|
|
19
21
|
// and expanded automatically before writing XML.
|
|
@@ -22,8 +24,11 @@ const SHORTHAND_TO_FQID = {
|
|
|
22
24
|
UiDoAction: 'com.provar.plugins.forcedotcom.core.ui.UiDoAction',
|
|
23
25
|
UiWithScreen: 'com.provar.plugins.forcedotcom.core.ui.UiWithScreen',
|
|
24
26
|
UiAssert: 'com.provar.plugins.forcedotcom.core.ui.UiAssert',
|
|
27
|
+
UiRead: 'com.provar.plugins.forcedotcom.core.ui.UiRead',
|
|
28
|
+
UiFill: 'com.provar.plugins.forcedotcom.core.ui.UiFill',
|
|
25
29
|
UiNavigate: 'com.provar.plugins.forcedotcom.core.ui.UiNavigate',
|
|
26
30
|
UiWithRow: 'com.provar.plugins.forcedotcom.core.ui.UiWithRow',
|
|
31
|
+
UiHandleAlert: 'com.provar.plugins.forcedotcom.core.ui.UiHandleAlert',
|
|
27
32
|
UiScrollToElement: 'com.provar.plugins.forcedotcom.core.ui.UiScrollToElement',
|
|
28
33
|
ApexConnect: 'com.provar.plugins.forcedotcom.core.testapis.ApexConnect',
|
|
29
34
|
ApexSoqlQuery: 'com.provar.plugins.forcedotcom.core.testapis.ApexSoqlQuery',
|
|
@@ -37,10 +42,58 @@ const SHORTHAND_TO_FQID = {
|
|
|
37
42
|
Sleep: 'com.provar.plugins.bundled.apis.control.Sleep',
|
|
38
43
|
ForEach: 'com.provar.plugins.bundled.apis.control.ForEach',
|
|
39
44
|
CaseCall: 'com.provar.plugins.bundled.apis.control.CaseCall',
|
|
45
|
+
// NitroX MS variants (Microsoft Dynamics 365 + Power Platform — Provar 3.0.7+)
|
|
46
|
+
MSDynamics365Connect: 'com.provar.plugins.forcedotcom.core.ui.NitroXConnect:ms-dynamics365',
|
|
47
|
+
MSDataverseConnect: 'com.provar.plugins.forcedotcom.core.ui.NitroXConnect:ms-dataverse',
|
|
48
|
+
MSPowerAppConnect: 'com.provar.plugins.forcedotcom.core.ui.NitroXConnect:ms-powerapp',
|
|
49
|
+
MSPowerPageConnect: 'com.provar.plugins.forcedotcom.core.ui.NitroXConnect:ms-powerpage',
|
|
40
50
|
};
|
|
51
|
+
const NITROX_MS_SHORTHANDS = new Set([
|
|
52
|
+
'MSDynamics365Connect',
|
|
53
|
+
'MSDataverseConnect',
|
|
54
|
+
'MSPowerAppConnect',
|
|
55
|
+
'MSPowerPageConnect',
|
|
56
|
+
]);
|
|
41
57
|
function resolveApiId(apiId) {
|
|
42
58
|
return SHORTHAND_TO_FQID[apiId] ?? apiId;
|
|
43
59
|
}
|
|
60
|
+
// ── PDX-495 + PDX-497: UI-action grouping under UiWithScreen substeps clause ─
|
|
61
|
+
// The set of fully-qualified API IDs that authors expect to be nested inside a
|
|
62
|
+
// preceding UiWithScreen's <clauses><clause name="substeps"><steps>…</steps></clause>
|
|
63
|
+
// block. When the generator receives a flat list with these IDs trailing a
|
|
64
|
+
// UiWithScreen, the auto-grouping pass moves them inside the substeps clause so
|
|
65
|
+
// the Provar IDE renders the test case correctly. SetValues, ApexConnect, and
|
|
66
|
+
// other non-UI apiCalls stay at the root.
|
|
67
|
+
//
|
|
68
|
+
// PDX-497: API set imported from the shared `uiActionApiIds.ts` module so the
|
|
69
|
+
// generator and validator can never drift. The single-namespace alignment
|
|
70
|
+
// (`com.provar.plugins.forcedotcom.core.ui.*`) matches what `resolveApiId`
|
|
71
|
+
// produces from every shorthand AND what the validator enforces — the older
|
|
72
|
+
// `com.provar.plugins.ui.*` defensive entries in this file's local set were
|
|
73
|
+
// dead code (no test coverage, no production path emits them).
|
|
74
|
+
const FORCEDOTCOM_UI_WITH_SCREEN = 'com.provar.plugins.forcedotcom.core.ui.UiWithScreen';
|
|
75
|
+
function isUiAction(apiId) {
|
|
76
|
+
return UI_ACTION_API_IDS.has(resolveApiId(apiId));
|
|
77
|
+
}
|
|
78
|
+
function isUiWithScreen(apiId) {
|
|
79
|
+
return resolveApiId(apiId) === FORCEDOTCOM_UI_WITH_SCREEN;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* PDX-497: a UI action step whose own container clause satisfies the
|
|
83
|
+
* UI-NEST-STRUCT-001 rule for its descendants (UiWithScreen, UiWithRow).
|
|
84
|
+
* Mirrors the validator's `UI_SCREEN_CONTAINERS` set.
|
|
85
|
+
*/
|
|
86
|
+
function isScreenContainer(apiId) {
|
|
87
|
+
return UI_SCREEN_CONTAINER_API_IDS.has(resolveApiId(apiId));
|
|
88
|
+
}
|
|
89
|
+
// PDX-497: UiWithRow plays a dual role. As a UI action (in UI_ACTION_API_IDS)
|
|
90
|
+
// it must be nested under a UiWithScreen ancestor via a substeps clause — same
|
|
91
|
+
// rule QH's UI-NEST-STRUCT-001 enforces. As a screen container (in
|
|
92
|
+
// UI_SCREEN_CONTAINER_API_IDS) it owns its OWN substeps clause that satisfies
|
|
93
|
+
// the rule for its descendants. The auto-grouping algorithm in `collectGroup`
|
|
94
|
+
// (below) handles both roles, and `buildTestCaseXml` synthesizes a root
|
|
95
|
+
// UiWithScreen when the payload contains screen containers but no UiWithScreen
|
|
96
|
+
// — without that wrapper, a root-level UiWithRow would itself fail the rule.
|
|
44
97
|
// ── Per-step runtime warnings ─────────────────────────────────────────────────
|
|
45
98
|
function buildStepWarnings(steps) {
|
|
46
99
|
const warnings = [];
|
|
@@ -55,6 +108,13 @@ function buildStepWarnings(steps) {
|
|
|
55
108
|
'To assert SOQL results use either: (a) a ForEach loop over the result list with AssertValues inside, ' +
|
|
56
109
|
'or (b) a SetValues step to extract a specific field into a named variable, then assert that variable.');
|
|
57
110
|
}
|
|
111
|
+
const nitroxMsFqids = new Set(Array.from(NITROX_MS_SHORTHANDS, (s) => SHORTHAND_TO_FQID[s]).filter((id) => Boolean(id)));
|
|
112
|
+
if (resolvedIds.some((id) => nitroxMsFqids.has(id))) {
|
|
113
|
+
warnings.push('NitroX MS connect (Dynamics 365 / Dataverse / Power Apps / Power Pages): ' +
|
|
114
|
+
'variant-specific args (appName, powerAppName, environment, powerPageName) must either be supplied as ' +
|
|
115
|
+
'literals/variables in attributes OR declared as <generatedParameters> for data-driven tests. ' +
|
|
116
|
+
'Empty args with no parameter declaration cause runtime null binding.');
|
|
117
|
+
}
|
|
58
118
|
// D7: Cleanup steps placed after a potential failure point are skipped when stopOnError=false.
|
|
59
119
|
if (resolvedIds.includes(SHORTHAND_TO_FQID['ApexDeleteObject'] ?? '')) {
|
|
60
120
|
warnings.push('ApexDeleteObject detected (likely cleanup): with stopOnError=false Provar skips all steps after ' +
|
|
@@ -70,9 +130,11 @@ const StepSchema = z.object({
|
|
|
70
130
|
api_id: z
|
|
71
131
|
.string()
|
|
72
132
|
.describe('Provar step API ID. Shorthand forms are accepted and auto-expanded to fully-qualified IDs: ' +
|
|
73
|
-
'UiConnect, UiDoAction, UiWithScreen, UiAssert, UiNavigate, UiWithRow, ' +
|
|
133
|
+
'UiConnect, UiDoAction, UiWithScreen, UiAssert, UiRead, UiFill, UiNavigate, UiWithRow, UiHandleAlert, ' +
|
|
74
134
|
'ApexConnect, ApexSoqlQuery, ApexCreateObject, ApexReadObject, ApexUpdateObject, ApexDeleteObject, ' +
|
|
75
|
-
'SetValues, AssertValues, StepGroup, Sleep, ForEach, CaseCall
|
|
135
|
+
'SetValues, AssertValues, StepGroup, Sleep, ForEach, CaseCall, ' +
|
|
136
|
+
'MSDynamics365Connect, MSDataverseConnect, MSPowerAppConnect, MSPowerPageConnect ' +
|
|
137
|
+
'(NitroXConnect:ms-* family for Microsoft Dynamics 365 + Power Platform — Provar 3.0.7+). ' +
|
|
76
138
|
'Or pass the fully-qualified ID directly (com.provar.plugins.*).'),
|
|
77
139
|
name: z.string().describe('Human-readable step name'),
|
|
78
140
|
attributes: z
|
|
@@ -89,7 +151,7 @@ const StepSchema = z.object({
|
|
|
89
151
|
'(3) AssertValues: pass assertion arguments as flat key/value pairs; emitted as flat <argument> elements, NOT wrapped in valueList/namedValues. ' +
|
|
90
152
|
'(4) target argument (UiWithScreen / UiWithRow): pass the sf:ui:target or ui:pageobject:target URI; ' +
|
|
91
153
|
' emitted as class="uiTarget" uri="...". ' +
|
|
92
|
-
'(5) locator argument (UiDoAction / UiAssert): pass the locator URI; emitted as class="uiLocator" uri="...". ' +
|
|
154
|
+
'(5) locator argument (UiDoAction / UiAssert / UiRead / UiFill): pass the locator URI; emitted as class="uiLocator" uri="...". ' +
|
|
93
155
|
'All other string values use class="value" valueClass="string".'),
|
|
94
156
|
});
|
|
95
157
|
const TOOL_DESCRIPTION = [
|
|
@@ -104,11 +166,14 @@ const TOOL_DESCRIPTION = [
|
|
|
104
166
|
// ── Existing description (unchanged below) ───────────────────────────────────
|
|
105
167
|
'Generate a Provar XML test case skeleton with proper UUID v4 guids, sequential testItemId values, and <steps> structure.',
|
|
106
168
|
'Returns XML content. Writes to disk only when dry_run=false.',
|
|
107
|
-
'Generated structure: <?xml version="1.0" encoding="UTF-8" standalone="no"?> with <testCase guid="..." id="
|
|
169
|
+
'Generated structure: <?xml version="1.0" encoding="UTF-8" standalone="no"?> with <testCase guid="..." id="N" registryId="..."> (a numeric integer id), a <summary/> child, then <steps>. The unique identifier is the guid; id is a human-facing label. When writing into an existing project the id is auto-allocated as the highest in-use id + 1; otherwise it defaults to 1.',
|
|
108
170
|
'URI-aware generation: use target_uri to control the XML nesting structure.',
|
|
109
171
|
' - sf:ui:target (or omit target_uri) → flat Salesforce XML structure (existing behaviour).',
|
|
110
172
|
' - ui:pageobject:target?pageId=pageobjects.PageClass → wraps all steps in a UiWithScreen element targeting that non-SF page object.',
|
|
111
173
|
'API IDs: shorthand forms (e.g. UiConnect, ApexSoqlQuery) are automatically expanded to fully-qualified IDs required by the Provar runtime.',
|
|
174
|
+
'Microsoft Dynamics / Power Platform: MSDynamics365Connect, MSDataverseConnect, MSPowerAppConnect, MSPowerPageConnect ' +
|
|
175
|
+
'expand to NitroXConnect:ms-* variants. Variant-specific args (appName, powerAppName, environment, powerPageName) ' +
|
|
176
|
+
'may be passed as literals OR declared via <generatedParameters> for data-driven tests.',
|
|
112
177
|
'Step arguments: attributes are emitted as <arguments><argument id="..."><value .../></argument></arguments> — the only format the Provar runtime processes.',
|
|
113
178
|
'Shorthand XML attributes on <apiCall> are silently ignored at runtime; always supply arguments via the attributes map.',
|
|
114
179
|
'ApexSoqlQuery argument IDs: soqlQuery (the SOQL SELECT statement), resultListName (binds result list to a variable), apexConnectionName (named connection), resultScope (optional).',
|
|
@@ -124,11 +189,11 @@ const TOOL_DESCRIPTION = [
|
|
|
124
189
|
'If AssertValues uses namedValues-shaped content, validation reports warning ASSERT-001.',
|
|
125
190
|
'Variable references: pass values as "{VarName}" (braces); emitted as class="variable" <path element="VarName"/>.',
|
|
126
191
|
'target argument (UiWithScreen/UiWithRow): pass the URI value; emitted as class="uiTarget" uri="...".',
|
|
127
|
-
'locator argument (UiDoAction/UiAssert): pass the URI value; emitted as class="uiLocator" uri="...".',
|
|
192
|
+
'locator argument (UiDoAction/UiAssert/UiRead/UiFill): pass the URI value; emitted as class="uiLocator" uri="...".',
|
|
128
193
|
'valueClass auto-detection: argument values are typed automatically before XML emission. ' +
|
|
129
|
-
'ISO-8601 date "YYYY-MM-DD" → valueClass="date"; ISO-8601 datetime "YYYY-MM-DDTHH:MM:SS" (optional fractional seconds + timezone) → "
|
|
194
|
+
'ISO-8601 date "YYYY-MM-DD" → valueClass="date"; ISO-8601 datetime "YYYY-MM-DDTHH:MM:SS" (optional fractional seconds + timezone) → "dateTime"; ' +
|
|
130
195
|
'"true"/"false" → "boolean"; numeric string (e.g. "42", "-5", "3.14") → "decimal"; otherwise "string". ' +
|
|
131
|
-
'Pass dates
|
|
196
|
+
'Pass dates in ISO-8601 — the generator converts them to the epoch-millisecond value Provar requires (a date/dateTime field emitted as an ISO string, or as valueClass="string", fails to load in the IDE). A bare date is treated as UTC midnight; a datetime without a timezone is treated as UTC. ' +
|
|
132
197
|
'Note: numbers always emit valueClass="decimal" per the canonical Provar reference (there is no separate "integer" valueClass).',
|
|
133
198
|
'Edit page objects: action=Edit targets require a compiled page object for the SF object. ' +
|
|
134
199
|
'If none exists in the project page-objects directory, the locator binding will fail at runtime. ' +
|
|
@@ -196,6 +261,20 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
196
261
|
.string()
|
|
197
262
|
.optional()
|
|
198
263
|
.describe(desc('Caller-provided key echoed back for deduplication tracking', 'string, optional; deduplication key echoed in response')),
|
|
264
|
+
grouping_mode: z
|
|
265
|
+
.enum(['auto', 'flat', 'single-screen'])
|
|
266
|
+
.default('auto')
|
|
267
|
+
.describe(desc('Controls how UI action steps (UiDoAction, UiAssert, UiRead, UiFill, UiNavigate, UiWithRow, UiHandleAlert) ' +
|
|
268
|
+
'are nested under UiWithScreen wrappers. ' +
|
|
269
|
+
'"auto" (default): when the flat steps[] payload contains a UiWithScreen followed by UI action siblings, ' +
|
|
270
|
+
'those siblings are auto-grouped inside the UiWithScreen\'s <clause name="substeps"><steps> block ' +
|
|
271
|
+
'(the structure Provar IDE expects). Non-UI steps (SetValues, ApexConnect, …) stay at the root. ' +
|
|
272
|
+
'UiWithRow plays a dual role: when it follows a UiWithScreen it is pulled in as a child container; ' +
|
|
273
|
+
'when screen containers such as UiWithRow appear without an explicit preceding UiWithScreen, generation may ' +
|
|
274
|
+
'synthesize a root UiWithScreen wrapper so they are nested under that screen container rather than remaining at root. ' +
|
|
275
|
+
'"flat": legacy behaviour — emit every step as a root sibling, no nesting. ' +
|
|
276
|
+
'"single-screen": wrap all steps in a single synthetic UiWithScreen (matches target_uri=ui:pageobject:target semantics). ' +
|
|
277
|
+
'If target_uri is "ui:pageobject:target?…" the single-screen wrap takes precedence regardless of this flag.', 'enum, optional; default "auto" (group UI actions under UiWithScreen substeps)')),
|
|
199
278
|
},
|
|
200
279
|
}, (input) => {
|
|
201
280
|
const requestId = makeRequestId();
|
|
@@ -225,11 +304,18 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
225
304
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
|
|
226
305
|
}
|
|
227
306
|
try {
|
|
228
|
-
const xmlContent = buildTestCaseXml(input);
|
|
229
307
|
const filePath = input.output_path ? path.resolve(input.output_path) : undefined;
|
|
230
|
-
|
|
308
|
+
// Allocate the root testCase id from surrounding project context, but only
|
|
309
|
+
// when actually persisting (a write path that has cleared the path policy).
|
|
310
|
+
// Preview/dry-run runs have no project anchor, so they keep the default id.
|
|
311
|
+
let idAllocation = { id: DEFAULT_TESTCASE_ID, basis: 'default' };
|
|
231
312
|
if (filePath && !input.dry_run) {
|
|
232
313
|
assertPathAllowed(filePath, config.allowedPaths);
|
|
314
|
+
idAllocation = allocateTestCaseId(filePath, config.allowedPaths);
|
|
315
|
+
}
|
|
316
|
+
const xmlContent = buildTestCaseXml(input, idAllocation.id);
|
|
317
|
+
let written = false;
|
|
318
|
+
if (filePath && !input.dry_run) {
|
|
233
319
|
if (fs.existsSync(filePath) && !input.overwrite) {
|
|
234
320
|
const err = makeError('FILE_EXISTS', `File already exists: ${filePath}. Set overwrite=true to replace.`, requestId);
|
|
235
321
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
|
|
@@ -237,7 +323,12 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
237
323
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
238
324
|
fs.writeFileSync(filePath, xmlContent, 'utf-8');
|
|
239
325
|
written = true;
|
|
240
|
-
log('info', 'provar_testcase_generate: wrote file', {
|
|
326
|
+
log('info', 'provar_testcase_generate: wrote file', {
|
|
327
|
+
requestId,
|
|
328
|
+
filePath,
|
|
329
|
+
testCaseId: idAllocation.id,
|
|
330
|
+
idBasis: idAllocation.basis,
|
|
331
|
+
});
|
|
241
332
|
}
|
|
242
333
|
const warnings = buildStepWarnings(input.steps);
|
|
243
334
|
const runValidation = input.validate_after_edit !== false;
|
|
@@ -248,6 +339,7 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
248
339
|
written,
|
|
249
340
|
dry_run: input.dry_run,
|
|
250
341
|
step_count: input.steps.length,
|
|
342
|
+
test_case_id: idAllocation.id,
|
|
251
343
|
idempotency_key: input.idempotency_key,
|
|
252
344
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
253
345
|
};
|
|
@@ -363,20 +455,56 @@ function buildArgumentValue(key, val, indent, inNamedValues = false, apiId = '')
|
|
|
363
455
|
}
|
|
364
456
|
if (!inNamedValues) {
|
|
365
457
|
// D2: 'target' argument → class="uiTarget" (only for UiWithScreen / UiWithRow).
|
|
366
|
-
|
|
458
|
+
// PDX-497: dispatched via the shared canonical set rather than substring matching.
|
|
459
|
+
if (key === 'target' && UI_SCREEN_CONTAINER_API_IDS.has(apiId)) {
|
|
367
460
|
return `${indent}<value class="uiTarget" uri="${escapeXmlAttr(val)}"/>`;
|
|
368
461
|
}
|
|
369
|
-
// D2: 'locator' argument → class="uiLocator" (only for
|
|
370
|
-
|
|
462
|
+
// D2: 'locator' argument → class="uiLocator" (only for UI APIs that bear a locator).
|
|
463
|
+
// PDX-497: covers UiDoAction, UiAssert, UiRead, UiFill via the shared canonical set.
|
|
464
|
+
if (key === 'locator' && UI_LOCATOR_BEARING_API_IDS.has(apiId)) {
|
|
371
465
|
return `${indent}<value class="uiLocator" uri="${escapeXmlAttr(val)}"/>`;
|
|
372
466
|
}
|
|
467
|
+
// D2: 'interaction' argument → class="uiInteraction" (UiDoAction Action widget).
|
|
468
|
+
// PDX-506: the IDE step editor binds its Action only from a typed uiInteraction;
|
|
469
|
+
// a plain string runs green from the CLI but renders the Action field blank.
|
|
470
|
+
// Gated on the shared UI-action API set so generator + validator stay aligned.
|
|
471
|
+
if (key === 'interaction' && UI_ACTION_API_IDS.has(apiId)) {
|
|
472
|
+
return `${indent}<value class="uiInteraction" uri="${escapeXmlAttr(val)}"/>`;
|
|
473
|
+
}
|
|
373
474
|
}
|
|
374
475
|
// PDX-493 (H3): infer valueClass for date / datetime / boolean / decimal / string. The
|
|
375
476
|
// `fieldTypeHint` parameter on `inferSalesforceValueClass` is intentionally not threaded
|
|
376
477
|
// through here yet — it lands in PDX-492 (H2b) along with the `field_type_hints` tool input.
|
|
377
478
|
const inferred = inferSalesforceValueClass(key, val);
|
|
479
|
+
if (inferred === 'date' || inferred === 'datetime') {
|
|
480
|
+
// PDX-509: Provar stores date/dateTime as epoch MILLISECONDS, never an ISO string —
|
|
481
|
+
// an ISO string fails to load in the IDE (RENDER-DATE-VALUECLASS-001), and the real
|
|
482
|
+
// corpus uses epoch ms exclusively with camelCase `dateTime`. Convert here.
|
|
483
|
+
const ms = isoToEpochMs(val, inferred);
|
|
484
|
+
if (ms !== null) {
|
|
485
|
+
const valueClass = inferred === 'datetime' ? 'dateTime' : 'date';
|
|
486
|
+
return `${indent}<value class="value" valueClass="${valueClass}">${ms}</value>`;
|
|
487
|
+
}
|
|
488
|
+
// A value that matches the ISO shape but is not a real date (e.g. "2026-99-99")
|
|
489
|
+
// cannot be converted — fall back to a plain string rather than emit a
|
|
490
|
+
// load-breaking date/dateTime with a non-epoch value.
|
|
491
|
+
return `${indent}<value class="value" valueClass="string">${escapeXmlContent(val)}</value>`;
|
|
492
|
+
}
|
|
378
493
|
return `${indent}<value class="value" valueClass="${inferred}">${escapeXmlContent(val)}</value>`;
|
|
379
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Convert a validated ISO-8601 date / datetime string to epoch milliseconds.
|
|
497
|
+
* A date-only string ("YYYY-MM-DD") is parsed as UTC midnight per the ES spec. A
|
|
498
|
+
* datetime WITHOUT an explicit timezone would otherwise parse as machine-local
|
|
499
|
+
* time (non-deterministic), so we pin it to UTC by appending 'Z' — matching the
|
|
500
|
+
* Provar corpus, where date/dateTime values are stored as UTC epoch ms.
|
|
501
|
+
* Returns null if the value cannot be parsed (caller falls back to the raw text).
|
|
502
|
+
*/
|
|
503
|
+
function isoToEpochMs(val, kind) {
|
|
504
|
+
const needsUtc = kind === 'datetime' && !/(Z|[+-]\d{2}:?\d{2})$/.test(val);
|
|
505
|
+
const ms = Date.parse(needsUtc ? `${val}Z` : val);
|
|
506
|
+
return Number.isNaN(ms) ? null : ms;
|
|
507
|
+
}
|
|
380
508
|
function buildArgumentsXml(attributes, baseIndent = ' ', apiId = '') {
|
|
381
509
|
const entries = Object.entries(attributes);
|
|
382
510
|
if (entries.length === 0)
|
|
@@ -413,36 +541,262 @@ function buildSetValuesXml(attributes, baseIndent) {
|
|
|
413
541
|
`${i(0)}</arguments>\n` +
|
|
414
542
|
`${baseIndent.slice(0, -2)}`);
|
|
415
543
|
}
|
|
544
|
+
// PDX-507: derive the field name for a uiFieldAssertion from a locator URI's
|
|
545
|
+
// `name=` parameter (e.g. ui:locator?name=LastName&binding=… → "LastName").
|
|
546
|
+
function extractFieldName(uri) {
|
|
547
|
+
const m = /[?&]name=([^&]*)/.exec(uri);
|
|
548
|
+
if (!m)
|
|
549
|
+
return 'Field';
|
|
550
|
+
try {
|
|
551
|
+
return decodeURIComponent(m[1]) || 'Field';
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return m[1] || 'Field';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// PDX-507: UiAssert — assemble the nested fieldAssertions / uiFieldAssertion
|
|
558
|
+
// structure the Provar IDE Result Assertions tab binds from. The generator
|
|
559
|
+
// previously emitted the flat shape (top-level fieldLocator / attributeName /
|
|
560
|
+
// comparisonType / expectedValue arguments), which runs green from the CLI but
|
|
561
|
+
// renders the Result Assertions tab blank in the IDE. Shape confirmed against
|
|
562
|
+
// the real test corpus (AllPOCProjects): 3,743/3,778 UiAssert steps use this
|
|
563
|
+
// nested form, with a BARE <fieldLocator uri="…"/> element (never class="uiLocator"
|
|
564
|
+
// — see best-practice rule UI-ASSERT-FIELDLOCATOR-002) and no `assertionType`.
|
|
565
|
+
// Only a `fieldLocator` attribute triggers the nested form; a UiAssert that
|
|
566
|
+
// carries a plain `locator` argument keeps the documented locator→uiLocator
|
|
567
|
+
// contract via the flat fallback (buildArgumentValue dispatches it). When no
|
|
568
|
+
// fieldLocator is supplied the step is not a field assertion, so fall back to
|
|
569
|
+
// flat argument emission and leave other UiAssert shapes untouched.
|
|
570
|
+
function buildUiAssertXml(attributes, baseIndent, apiId) {
|
|
571
|
+
const a = attributes;
|
|
572
|
+
const fieldLocator = a['fieldLocator'] ?? '';
|
|
573
|
+
if (!fieldLocator)
|
|
574
|
+
return buildArgumentsXml(attributes, baseIndent, apiId);
|
|
575
|
+
const i = (n) => baseIndent + ' '.repeat(n);
|
|
576
|
+
const resultName = a['resultName'] ?? 'Values';
|
|
577
|
+
const resultScope = a['resultScope'] ?? 'Test';
|
|
578
|
+
const captureAfter = a['captureAfter'] ?? 'false';
|
|
579
|
+
const attributeName = a['attributeName'] ?? 'value';
|
|
580
|
+
const comparisonType = a['comparisonType'] ?? 'EqualTo';
|
|
581
|
+
const fieldName = extractFieldName(fieldLocator);
|
|
582
|
+
const expected = a['expectedValue'];
|
|
583
|
+
// resultName / resultScope / captureAfter are control-plane literals that the
|
|
584
|
+
// real corpus ALWAYS emits as valueClass="string" — including captureAfter
|
|
585
|
+
// ("true"/"false"), which is intentionally NOT valueClass="boolean" here.
|
|
586
|
+
// Routing these through inferSalesforceValueClass would diverge from the
|
|
587
|
+
// corpus (it would infer boolean), so they are emitted as string literals.
|
|
588
|
+
const strVal = (v) => `<value class="value" valueClass="string">${escapeXmlContent(v)}</value>`;
|
|
589
|
+
// uiAttributeAssertion — self-closing when there is no expected value. This is
|
|
590
|
+
// a real corpus form (e.g. ADP_POV "UI Assert - Date Empty" emits
|
|
591
|
+
// <uiAttributeAssertion attributeName="value" comparisonType="EqualTo"/>);
|
|
592
|
+
// otherwise it carries a typed <value> child built via buildArgumentValue so
|
|
593
|
+
// {Var} expands to class="variable".
|
|
594
|
+
const attrOpen = `${i(5)}<uiAttributeAssertion attributeName="${escapeXmlAttr(attributeName)}" comparisonType="${escapeXmlAttr(comparisonType)}"`;
|
|
595
|
+
const attrAssertion = expected != null && expected !== ''
|
|
596
|
+
? `${attrOpen}>\n${buildArgumentValue('expectedValue', expected, i(6), true)}\n${i(5)}</uiAttributeAssertion>`
|
|
597
|
+
: `${attrOpen}/>`;
|
|
598
|
+
const fieldAssertion = `${i(4)}<uiFieldAssertion resultName="${escapeXmlAttr(fieldName)}">\n` +
|
|
599
|
+
`${i(5)}<fieldLocator uri="${escapeXmlAttr(fieldLocator)}"/>\n` +
|
|
600
|
+
`${i(5)}<attributeAssertions>\n` +
|
|
601
|
+
`${attrAssertion}\n` +
|
|
602
|
+
`${i(5)}</attributeAssertions>\n` +
|
|
603
|
+
`${i(4)}</uiFieldAssertion>`;
|
|
604
|
+
return (`\n${i(0)}<arguments>\n` +
|
|
605
|
+
`${i(0)}<argument id="resultName">\n${i(1)}${strVal(resultName)}\n${i(0)}</argument>\n` +
|
|
606
|
+
`${i(0)}<argument id="resultScope">\n${i(1)}${strVal(resultScope)}\n${i(0)}</argument>\n` +
|
|
607
|
+
`${i(0)}<argument id="fieldAssertions">\n` +
|
|
608
|
+
`${i(1)}<value class="valueList" mutable="Mutable">\n` +
|
|
609
|
+
`${fieldAssertion}\n` +
|
|
610
|
+
`${i(1)}</value>\n` +
|
|
611
|
+
`${i(0)}</argument>\n` +
|
|
612
|
+
`${i(0)}<argument id="captureAfter">\n${i(1)}${strVal(captureAfter)}\n${i(0)}</argument>\n` +
|
|
613
|
+
`${i(0)}<argument id="columnAssertions">\n${i(1)}<value class="valueList" mutable="Mutable"/>\n${i(0)}</argument>\n` +
|
|
614
|
+
`${i(0)}<argument id="pageAssertions">\n${i(1)}<value class="valueList" mutable="Mutable"/>\n${i(0)}</argument>\n` +
|
|
615
|
+
`${i(0)}<argument id="beforeWait"/>\n` +
|
|
616
|
+
`${i(0)}<argument id="autoRetry"/>\n` +
|
|
617
|
+
`${i(0)}</arguments>\n` +
|
|
618
|
+
`${baseIndent.slice(0, -2)}`);
|
|
619
|
+
}
|
|
416
620
|
function buildFlatStepXml(step, testItemId, indent) {
|
|
621
|
+
return buildStepXmlWithChildren(step, testItemId, indent, '', undefined);
|
|
622
|
+
}
|
|
623
|
+
// PDX-495: build a single <apiCall> element, optionally with a <clauses>
|
|
624
|
+
// <clause name="substeps"><steps>…</steps></clause></clauses> block containing
|
|
625
|
+
// already-rendered child XML. This is what lets a UiWithScreen wrap its UI
|
|
626
|
+
// action siblings without breaking the existing flat emission path.
|
|
627
|
+
//
|
|
628
|
+
// `childrenXml` is the already-rendered, already-indented inner XML for the
|
|
629
|
+
// substeps clause (joined newline-separated, no leading/trailing whitespace).
|
|
630
|
+
// `substepsTestItemId` is the testItemId for the <clause name="substeps"> slot;
|
|
631
|
+
// omit (undefined) to skip the clauses block entirely (legacy flat behaviour).
|
|
632
|
+
function buildStepXmlWithChildren(step, testItemId, indent, childrenXml, substepsTestItemId) {
|
|
417
633
|
const guid = randomUUID();
|
|
418
634
|
const resolvedApiId = resolveApiId(step.api_id);
|
|
419
635
|
const baseIndent = indent + ' ';
|
|
420
636
|
// Use SetValues structure for any SetValues API (string-match mirrors the validator).
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
637
|
+
// PDX-507: UiAssert uses the nested fieldAssertions/uiFieldAssertion structure.
|
|
638
|
+
let argumentsXml;
|
|
639
|
+
if (resolvedApiId.includes('SetValues')) {
|
|
640
|
+
argumentsXml = buildSetValuesXml(step.attributes, baseIndent);
|
|
641
|
+
}
|
|
642
|
+
else if (resolvedApiId.endsWith('.UiAssert') || resolvedApiId === 'UiAssert') {
|
|
643
|
+
argumentsXml = buildUiAssertXml(step.attributes, baseIndent, resolvedApiId);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
argumentsXml = buildArgumentsXml(step.attributes, baseIndent, resolvedApiId);
|
|
647
|
+
}
|
|
648
|
+
const hasClauses = substepsTestItemId !== undefined;
|
|
649
|
+
const open = `${indent}<apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}" name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}">`;
|
|
650
|
+
const close = `${indent}</apiCall>`;
|
|
651
|
+
if (!hasClauses && !argumentsXml) {
|
|
425
652
|
return (`${indent}<apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}"` +
|
|
426
|
-
` name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}"
|
|
653
|
+
` name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}"/>`);
|
|
654
|
+
}
|
|
655
|
+
if (!hasClauses) {
|
|
656
|
+
// Legacy flat shape: keep the inline form so existing string assertions in
|
|
657
|
+
// call sites and tests (e.g. literal `</apiCall>` placement) still match.
|
|
658
|
+
return `${open}${argumentsXml}</apiCall>`;
|
|
427
659
|
}
|
|
428
|
-
|
|
429
|
-
|
|
660
|
+
// PDX-495 grouped shape: arguments + clauses block with substeps.
|
|
661
|
+
const clauseIndent = baseIndent;
|
|
662
|
+
const innerStepsIndent = baseIndent + ' ';
|
|
663
|
+
const stepsBlock = childrenXml
|
|
664
|
+
? `${innerStepsIndent}<steps>\n${childrenXml}\n${innerStepsIndent}</steps>`
|
|
665
|
+
: `${innerStepsIndent}<steps/>`;
|
|
666
|
+
const clausesBlock = `${baseIndent}<clauses>\n` +
|
|
667
|
+
`${clauseIndent}<clause name="substeps" testItemId="${substepsTestItemId}">\n` +
|
|
668
|
+
`${stepsBlock}\n` +
|
|
669
|
+
`${clauseIndent}</clause>\n` +
|
|
670
|
+
`${baseIndent}</clauses>`;
|
|
671
|
+
// argumentsXml when present already includes a leading "\n" and a trailing
|
|
672
|
+
// newline + parent-indent so the </apiCall> appears on its own line — match
|
|
673
|
+
// that shape for the grouped emission too.
|
|
674
|
+
if (argumentsXml) {
|
|
675
|
+
return `${open}${argumentsXml.replace(/\n[ \t]*$/, '')}\n${clausesBlock}\n${close}`;
|
|
676
|
+
}
|
|
677
|
+
return `${open}\n${clausesBlock}\n${close}`;
|
|
678
|
+
}
|
|
679
|
+
function groupStepsAuto(steps) {
|
|
680
|
+
const cursor = { i: 0 };
|
|
681
|
+
return collectGroup(steps, cursor, /* stopOnUiWithScreen */ false);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Walk a contiguous run of steps starting at `cursor.i`. Stops at end of list,
|
|
685
|
+
* or — when `stopOnUiWithScreen` is true — at the next UiWithScreen (which
|
|
686
|
+
* belongs to the caller).
|
|
687
|
+
*
|
|
688
|
+
* For each step:
|
|
689
|
+
* - UiWithScreen → consume one node, then recursively absorb a child run of
|
|
690
|
+
* UI-action siblings (with `stopOnUiWithScreen=true`).
|
|
691
|
+
* - UiWithRow → consume one node, then recursively absorb a child run of
|
|
692
|
+
* UI-action siblings (UiWithRow is itself a UI action that can also host
|
|
693
|
+
* substeps — same recursive behaviour as UiWithScreen for its children).
|
|
694
|
+
* - Any other UI action → consume one node, no children.
|
|
695
|
+
* - Non-UI step → consume one node, no children. When called from a child
|
|
696
|
+
* run this breaks the run (handled by the parent loop's `isUiAction` gate).
|
|
697
|
+
*/
|
|
698
|
+
function collectGroup(steps, cursor, stopOnUiWithScreen) {
|
|
699
|
+
const result = [];
|
|
700
|
+
while (cursor.i < steps.length) {
|
|
701
|
+
const step = steps[cursor.i];
|
|
702
|
+
if (stopOnUiWithScreen && isUiWithScreen(step.api_id))
|
|
703
|
+
break;
|
|
704
|
+
// Inside a parent's child run, a non-UI step ends the run; the parent's
|
|
705
|
+
// caller will see it next.
|
|
706
|
+
if (stopOnUiWithScreen && !isUiAction(step.api_id))
|
|
707
|
+
break;
|
|
708
|
+
cursor.i++;
|
|
709
|
+
const node = { ...step, children: [] };
|
|
710
|
+
result.push(node);
|
|
711
|
+
if (isScreenContainer(step.api_id)) {
|
|
712
|
+
// UiWithScreen always absorbs; UiWithRow absorbs when it is a child of
|
|
713
|
+
// a UiWithScreen (root-level UiWithRow is rewritten into a synthetic
|
|
714
|
+
// UiWithScreen by `buildTestCaseXml` before this walker runs). The child
|
|
715
|
+
// run itself stops at the next UiWithScreen so a later UiWithScreen is
|
|
716
|
+
// not pulled in as a grandchild.
|
|
717
|
+
node.children = collectGroup(steps, cursor, /* stopOnUiWithScreen */ true);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return result;
|
|
430
721
|
}
|
|
431
|
-
|
|
722
|
+
// PDX-495 + PDX-497: emit a list of grouped step nodes as XML. Assigns
|
|
723
|
+
// testItemIds depth-first: each node consumes one ID; if it has children it
|
|
724
|
+
// ALSO consumes one more ID for the <clause name="substeps"> slot, then its
|
|
725
|
+
// children consume their own IDs in order (recursing into grandchildren when
|
|
726
|
+
// a child node is itself a container, e.g. UiWithRow inside UiWithScreen).
|
|
727
|
+
// Mirrors the numbering convention used by Provar IDE (verified against the
|
|
728
|
+
// Contact_Lead1.testcase reference shape — see PDX-495).
|
|
729
|
+
function emitGroupedSteps(nodes, indent, startId) {
|
|
730
|
+
const lines = [];
|
|
731
|
+
let id = startId;
|
|
732
|
+
for (const node of nodes) {
|
|
733
|
+
const myId = id++;
|
|
734
|
+
if (node.children.length === 0) {
|
|
735
|
+
lines.push(buildFlatStepXml(node, myId, indent));
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const substepsId = id++;
|
|
739
|
+
const childIndent = indent + ' '; // matches buildUiWithScreenXml inner step indent
|
|
740
|
+
// PDX-497: recurse — children may themselves be containers (e.g. UiWithRow
|
|
741
|
+
// inside UiWithScreen). The recursive call assigns child + grandchild IDs
|
|
742
|
+
// and returns the XML for the full subtree.
|
|
743
|
+
const { xml: childrenXml, nextId } = emitGroupedSteps(node.children, childIndent, id);
|
|
744
|
+
id = nextId;
|
|
745
|
+
lines.push(buildStepXmlWithChildren(node, myId, indent, childrenXml, substepsId));
|
|
746
|
+
}
|
|
747
|
+
return { xml: lines.join('\n'), nextId: id };
|
|
748
|
+
}
|
|
749
|
+
function buildTestCaseXml(input, testCaseId = DEFAULT_TESTCASE_ID) {
|
|
432
750
|
const testCaseGuid = randomUUID();
|
|
433
751
|
const registryId = randomUUID();
|
|
752
|
+
const groupingMode = input.grouping_mode ?? 'auto';
|
|
434
753
|
let stepLines;
|
|
435
754
|
const isNonSf = !!input.target_uri && input.target_uri.startsWith('ui:');
|
|
436
755
|
if (isNonSf && input.target_uri) {
|
|
756
|
+
// target_uri=ui:pageobject:target?… always wraps all steps in a single
|
|
757
|
+
// synthetic UiWithScreen — this predates PDX-495 and takes precedence
|
|
758
|
+
// regardless of grouping_mode (matches "single-screen" semantics).
|
|
437
759
|
stepLines = buildUiWithScreenXml(input.steps, input.target_uri);
|
|
438
760
|
}
|
|
761
|
+
else if (groupingMode === 'single-screen' && input.steps.length > 0) {
|
|
762
|
+
// Explicit single-screen request without a non-SF target_uri: wrap with the
|
|
763
|
+
// caller-supplied target_uri when provided (e.g. `sf:ui:target?object=Lead&action=New`),
|
|
764
|
+
// falling back to the bare `sf:ui:target` default so the synthetic wrapper
|
|
765
|
+
// is well-formed even when target_uri is omitted.
|
|
766
|
+
stepLines = buildUiWithScreenXml(input.steps, input.target_uri ?? 'sf:ui:target');
|
|
767
|
+
}
|
|
768
|
+
else if (groupingMode === 'auto' && input.steps.some((s) => isScreenContainer(s.api_id))) {
|
|
769
|
+
// PDX-495 + PDX-497 auto-grouping: nest UI actions inside their preceding
|
|
770
|
+
// screen container (UiWithScreen). When the payload contains a screen
|
|
771
|
+
// container but no UiWithScreen at root (e.g. starts with UiWithRow), QH's
|
|
772
|
+
// UI-NEST-STRUCT-001 still requires UiWithRow itself to descend from a
|
|
773
|
+
// UiWithScreen via a substeps clause. Synthesize a root UiWithScreen so the
|
|
774
|
+
// round-trip generate -> validate stays clean. Mirrors the single-screen
|
|
775
|
+
// path's synthetic wrapper (target_uri ?? 'sf:ui:target').
|
|
776
|
+
const stepsForGrouping = input.steps.some((s) => isUiWithScreen(s.api_id))
|
|
777
|
+
? input.steps
|
|
778
|
+
: [
|
|
779
|
+
{
|
|
780
|
+
api_id: 'UiWithScreen',
|
|
781
|
+
name: 'With page',
|
|
782
|
+
attributes: { target: input.target_uri ?? 'sf:ui:target' },
|
|
783
|
+
},
|
|
784
|
+
...input.steps,
|
|
785
|
+
];
|
|
786
|
+
const tree = groupStepsAuto(stepsForGrouping);
|
|
787
|
+
stepLines = emitGroupedSteps(tree, ' ', 1).xml;
|
|
788
|
+
}
|
|
439
789
|
else {
|
|
790
|
+
// Legacy flat behaviour: every step is a root sibling. Preserved by
|
|
791
|
+
// grouping_mode="flat" and by payloads with no UiWithScreen present.
|
|
440
792
|
const lines = input.steps.map((step, i) => buildFlatStepXml(step, i + 1, ' ')).join('\n');
|
|
441
793
|
stepLines = lines || ' <!-- TODO: Add test steps here -->';
|
|
442
794
|
}
|
|
443
|
-
// Provar requires: standalone="no",
|
|
795
|
+
// Provar requires: standalone="no", a numeric integer id, no name attr, <summary/> before <steps>.
|
|
796
|
+
// The id is a human-facing label, not a uniqueness key (guid is) — see allocateTestCaseId:
|
|
797
|
+
// when writing into an existing project we pass highest-in-use + 1; otherwise it defaults to 1.
|
|
444
798
|
return ('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' +
|
|
445
|
-
`<testCase guid="${testCaseGuid}" id="
|
|
799
|
+
`<testCase guid="${testCaseGuid}" id="${testCaseId}" registryId="${registryId}">\n` +
|
|
446
800
|
' <summary/>\n' +
|
|
447
801
|
' <steps>\n' +
|
|
448
802
|
stepLines +
|