@provartesting/provardx-cli 1.5.3 → 1.6.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/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 +18 -6
- package/lib/mcp/docs/VALIDATION_RULE_REGISTRY.md +225 -0
- package/lib/mcp/prompts/loopPrompts.js +2 -2
- 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 +119 -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 +31 -7
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +1069 -8
- 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/qualityHubTools.js +1 -13
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +23 -0
- package/lib/mcp/tools/sfSpawn.js +170 -15
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/spawnErrors.d.ts +13 -0
- package/lib/mcp/tools/spawnErrors.js +51 -0
- package/lib/mcp/tools/spawnErrors.js.map +1 -0
- package/lib/mcp/tools/testCaseGenerate.js +146 -12
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseValidate.d.ts +46 -0
- package/lib/mcp/tools/testCaseValidate.js +307 -41
- 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/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 +3 -2
|
@@ -56,7 +56,6 @@ const VALID_API_IDS = new Set([
|
|
|
56
56
|
'com.provar.plugins.bundled.apis.control.ActualResult',
|
|
57
57
|
'com.provar.plugins.bundled.apis.control.DesignStep',
|
|
58
58
|
'com.provar.plugins.bundled.apis.control.CallTest',
|
|
59
|
-
'com.provar.plugins.bundled.apis.control.TryCatchFinally',
|
|
60
59
|
// Bundled — database
|
|
61
60
|
'com.provar.plugins.bundled.apis.db.DbConnect',
|
|
62
61
|
'com.provar.plugins.bundled.apis.db.DbDelete',
|
|
@@ -171,7 +170,7 @@ function toArr(v) {
|
|
|
171
170
|
}
|
|
172
171
|
/**
|
|
173
172
|
* Recursively collect every <apiCall> element in the parsed tree.
|
|
174
|
-
* Works for flat and deeply nested structures (StepGroup, BDD,
|
|
173
|
+
* Works for flat and deeply nested structures (StepGroup, BDD, Finally…).
|
|
175
174
|
*/
|
|
176
175
|
function getAllApiCalls(node) {
|
|
177
176
|
const results = [];
|
|
@@ -538,7 +537,9 @@ function validateDetectDuplicatesLiterals(tc, rule) {
|
|
|
538
537
|
continue;
|
|
539
538
|
if (valElem['@_class'] !== 'value')
|
|
540
539
|
continue; // skip variables/compounds
|
|
541
|
-
|
|
540
|
+
// Read through nodeText: fast-xml-parser yields a NUMBER for a numeric tag
|
|
541
|
+
// value (e.g. <value>123</value>), so a bare `.trim()` would throw.
|
|
542
|
+
const text = nodeText(valElem);
|
|
542
543
|
if (!text || text.length <= 3)
|
|
543
544
|
continue;
|
|
544
545
|
if (DEFAULT_LITERAL_VALUES.has(text))
|
|
@@ -850,11 +851,7 @@ function validateUiActionNestingStructure(tc, rule) {
|
|
|
850
851
|
const tid = call['@_testItemId'];
|
|
851
852
|
const shortApi = apiId.split('.').pop() ?? apiId;
|
|
852
853
|
const tidSuffix = tid ? ` (testItemId=${tid})` : '';
|
|
853
|
-
const requiredContainer = verdict.includes('UiWithRow')
|
|
854
|
-
? 'UiWithRow'
|
|
855
|
-
: verdict.includes('UiWithScreen')
|
|
856
|
-
? 'UiWithScreen'
|
|
857
|
-
: 'UiWithScreen';
|
|
854
|
+
const requiredContainer = verdict.includes('UiWithRow') ? 'UiWithRow' : 'UiWithScreen';
|
|
858
855
|
const message = `${shortApi} '${title}' is ${verdict} - must be nested inside a parent ` +
|
|
859
856
|
`${requiredContainer}'s <clauses><clause name="substeps"><steps> block${tidSuffix}`;
|
|
860
857
|
violations.push(makeViolation(rule, message));
|
|
@@ -979,6 +976,1041 @@ function validateNitroxVariantArgRequired(tc, rule) {
|
|
|
979
976
|
}
|
|
980
977
|
return violations;
|
|
981
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Value `class` attributes that, when present on a condition/expression argument,
|
|
981
|
+
* are themselves meaningful content — comparison and boolean-logic operators that
|
|
982
|
+
* Provar emits for `If`/`DoWhile`/`WaitFor` conditions (e.g. `{Count(Rows) > 0}`
|
|
983
|
+
* is stored as `<value class="gt">`). Mirrors the backend operator allow-list.
|
|
984
|
+
*/
|
|
985
|
+
const MEANINGFUL_VALUE_OPERATOR_CLASSES = new Set([
|
|
986
|
+
'gt',
|
|
987
|
+
'lt',
|
|
988
|
+
'eq',
|
|
989
|
+
'ne',
|
|
990
|
+
'ge',
|
|
991
|
+
'le',
|
|
992
|
+
'and',
|
|
993
|
+
'or',
|
|
994
|
+
'not',
|
|
995
|
+
]);
|
|
996
|
+
/**
|
|
997
|
+
* True when an `<argument>` carries a *meaningful* value, mirroring the Quality
|
|
998
|
+
* Hub `MustContainArgumentValidator` content checks exactly. A `variable` value
|
|
999
|
+
* counts only if it references a `<path>` (or has non-empty text) — a bare
|
|
1000
|
+
* `<value class="variable"/>` is effectively empty; `funcCall` and the comparison
|
|
1001
|
+
* / logic operator classes always count; `compound` counts only if `<parts>` has
|
|
1002
|
+
* children; any other (simple) value counts only if it has non-empty text. An
|
|
1003
|
+
* `<argument>` with no `<value>` child, or an empty `<value/>`, is NOT meaningful
|
|
1004
|
+
* and is treated as a missing required argument.
|
|
1005
|
+
*/
|
|
1006
|
+
function argumentHasMeaningfulValue(arg) {
|
|
1007
|
+
for (const value of toArr(arg['value'])) {
|
|
1008
|
+
if (value == null)
|
|
1009
|
+
continue;
|
|
1010
|
+
if (typeof value === 'string') {
|
|
1011
|
+
if (value.trim().length > 0)
|
|
1012
|
+
return true;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (typeof value !== 'object')
|
|
1016
|
+
continue;
|
|
1017
|
+
const v = value;
|
|
1018
|
+
const vClass = v['@_class'] ?? '';
|
|
1019
|
+
// nodeText coerces a numeric #text to string first — a bare `.trim()` throws on it.
|
|
1020
|
+
const text = nodeText(v);
|
|
1021
|
+
if (vClass === 'variable') {
|
|
1022
|
+
if (v['path'] != null || text.length > 0)
|
|
1023
|
+
return true;
|
|
1024
|
+
continue; // bare <value class="variable"/> — not meaningful
|
|
1025
|
+
}
|
|
1026
|
+
if (vClass === 'funcCall' || MEANINGFUL_VALUE_OPERATOR_CLASSES.has(vClass))
|
|
1027
|
+
return true;
|
|
1028
|
+
if (vClass === 'compound') {
|
|
1029
|
+
const parts = v['parts'];
|
|
1030
|
+
if (parts && typeof parts === 'object' && Object.keys(parts).some((k) => !k.startsWith('@_')))
|
|
1031
|
+
return true;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (text.length > 0)
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
/** Find an `<argument id=…>` for a call, tolerating both the `<arguments>` wrapper and direct children. */
|
|
1040
|
+
function findArgumentById(call, argId) {
|
|
1041
|
+
return getCallArguments(call).find((a) => a['@_id'] === argId) ?? getArguments(call).find((a) => a['@_id'] === argId);
|
|
1042
|
+
}
|
|
1043
|
+
/** Human-readable step label for a violation message: `'<title|name>' (testItemId=N)`. */
|
|
1044
|
+
function stepLabel(call) {
|
|
1045
|
+
const label = call['@_title'] ?? call['@_name'] ?? '(unnamed)';
|
|
1046
|
+
const tid = call['@_testItemId'];
|
|
1047
|
+
return `'${label}'${tid ? ` (testItemId=${tid})` : ''}`;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* True when `call` satisfies a `mustContainArgument` requirement — either the
|
|
1051
|
+
* argument is present with a meaningful value, or (for `If`/`DoWhile` conditions)
|
|
1052
|
+
* the legacy condition-in-title format is used.
|
|
1053
|
+
*/
|
|
1054
|
+
function callSatisfiesRequiredArg(call, requiredArg, conditionInTitleAllowed) {
|
|
1055
|
+
const arg = findArgumentById(call, requiredArg);
|
|
1056
|
+
if (arg && argumentHasMeaningfulValue(arg))
|
|
1057
|
+
return true;
|
|
1058
|
+
if (!arg && conditionInTitleAllowed) {
|
|
1059
|
+
const title = call['@_title'] ?? '';
|
|
1060
|
+
if (title.includes('{') && title.includes('}'))
|
|
1061
|
+
return true; // legacy condition-in-title format
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* mustContainArgument — every apiCall whose apiId equals `check.apiId` must carry a
|
|
1067
|
+
* populated `<argument id="check.argument">`. Faithful TypeScript port of the
|
|
1068
|
+
* Quality Hub `MustContainArgumentValidator`, so the local (offline) result and
|
|
1069
|
+
* the back-end agree: present-AND-non-empty semantics via
|
|
1070
|
+
* {@link argumentHasMeaningfulValue} (an absent argument OR an empty
|
|
1071
|
+
* `<argument/>`/`<value/>` is a violation); exact apiId match (no substring /
|
|
1072
|
+
* variant widening); the legacy exception where `If`/`DoWhile` may carry the
|
|
1073
|
+
* condition in the step `title` (`If: {expr}`) instead of a `condition` argument;
|
|
1074
|
+
* disabled steps are NOT skipped (a missing required argument is load/exec
|
|
1075
|
+
* blocking regardless of the disabled flag); and one violation per rule (the
|
|
1076
|
+
* back-end returns the first offender), so the weighted-deduction score stays in
|
|
1077
|
+
* parity with the Lambda. The message still names every offending step without
|
|
1078
|
+
* inflating `count`.
|
|
1079
|
+
*/
|
|
1080
|
+
function validateMustContainArgument(tc, rule) {
|
|
1081
|
+
const targetApiId = rule.check['apiId'] ?? '';
|
|
1082
|
+
const requiredArg = rule.check['argument'] ?? '';
|
|
1083
|
+
if (!targetApiId || !requiredArg)
|
|
1084
|
+
return null;
|
|
1085
|
+
const conditionInTitleAllowed = requiredArg === 'condition' &&
|
|
1086
|
+
(targetApiId === 'com.provar.plugins.bundled.apis.If' ||
|
|
1087
|
+
targetApiId === 'com.provar.plugins.bundled.apis.control.DoWhile');
|
|
1088
|
+
const offending = [];
|
|
1089
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1090
|
+
if (call['@_apiId'] !== targetApiId)
|
|
1091
|
+
continue;
|
|
1092
|
+
if (callSatisfiesRequiredArg(call, requiredArg, conditionInTitleAllowed))
|
|
1093
|
+
continue;
|
|
1094
|
+
offending.push(stepLabel(call));
|
|
1095
|
+
}
|
|
1096
|
+
if (!offending.length)
|
|
1097
|
+
return null;
|
|
1098
|
+
const apiName = targetApiId.split('.').pop() ?? targetApiId;
|
|
1099
|
+
let msg = `${apiName} step missing required '${requiredArg}' argument: ${offending.slice(0, 2).join(', ')}`;
|
|
1100
|
+
if (offending.length > 2)
|
|
1101
|
+
msg += ` (and ${offending.length - 2} more)`;
|
|
1102
|
+
// Intentionally no `count`: the back-end reports a single violation per rule, so
|
|
1103
|
+
// omitting count keeps the weighted-deduction score in parity with the Lambda.
|
|
1104
|
+
return makeViolation(rule, msg);
|
|
1105
|
+
}
|
|
1106
|
+
// ── Render / load-blocking validators (Tier 2) ───────────────────────────────
|
|
1107
|
+
// Faithful ports of the Quality Hub XMLRendering / InvalidValueClass /
|
|
1108
|
+
// DateValueClassFormat / ApexConnect* / SetValuesInvalidElements validators.
|
|
1109
|
+
// These check types map 1:1 to load-blocking rules (mostly `critical`) that
|
|
1110
|
+
// stop a test case rendering or loading in the Provar IDE. Each returns a
|
|
1111
|
+
// single BPViolation per rule (the back-end reports the first offender and sets
|
|
1112
|
+
// `count = len(offenders)` only when > 1), so the weighted-deduction score stays
|
|
1113
|
+
// in parity with the Lambda.
|
|
1114
|
+
const APEX_CONNECT_API_ID = 'com.provar.plugins.forcedotcom.core.testapis.ApexConnect';
|
|
1115
|
+
const SETVALUES_API_ID = 'com.provar.plugins.bundled.apis.control.SetValues';
|
|
1116
|
+
/**
|
|
1117
|
+
* Recursively collect every element that appears under `tag` anywhere in the
|
|
1118
|
+
* subtree (the fast-xml-parser equivalent of ElementTree's `.//tag`). Returns
|
|
1119
|
+
* the raw values (object, string, or — for repeated tags — each array member);
|
|
1120
|
+
* callers filter to objects when they need attributes. Mirrors the back-end's
|
|
1121
|
+
* descendant search so nested-step double-counting matches exactly.
|
|
1122
|
+
*/
|
|
1123
|
+
function collectElementsByTag(node, tag) {
|
|
1124
|
+
const out = [];
|
|
1125
|
+
function walk(n) {
|
|
1126
|
+
if (!n || typeof n !== 'object')
|
|
1127
|
+
return;
|
|
1128
|
+
if (Array.isArray(n)) {
|
|
1129
|
+
for (const item of n)
|
|
1130
|
+
walk(item);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
for (const [k, v] of Object.entries(n)) {
|
|
1134
|
+
if (k.startsWith('@_') || k === '#text')
|
|
1135
|
+
continue;
|
|
1136
|
+
if (k === tag)
|
|
1137
|
+
for (const item of toArr(v))
|
|
1138
|
+
out.push(item);
|
|
1139
|
+
walk(v);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
walk(node);
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
/** Object-form `<value>` descendants of a node (string-only text values are skipped). */
|
|
1146
|
+
function collectValueElements(node) {
|
|
1147
|
+
return collectElementsByTag(node, 'value').filter((v) => v != null && typeof v === 'object');
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Trimmed text of an element's `#text`, coercing to string first. fast-xml-parser
|
|
1151
|
+
* parses numeric tag text to a `number` (e.g. an epoch-millis date), so a raw
|
|
1152
|
+
* `.trim()` on `#text` would throw — always read element text through here.
|
|
1153
|
+
*/
|
|
1154
|
+
function nodeText(node) {
|
|
1155
|
+
const t = node['#text'];
|
|
1156
|
+
return t == null ? '' : String(t).trim();
|
|
1157
|
+
}
|
|
1158
|
+
/** Step context used in load-blocking violation messages, mirroring the back-end defaults. */
|
|
1159
|
+
function stepContext(call) {
|
|
1160
|
+
const apiId = call['@_apiId'] ?? '';
|
|
1161
|
+
const apiName = apiId ? apiId.split('.').pop() ?? apiId : 'Unknown';
|
|
1162
|
+
const title = call['@_title'] ?? apiName;
|
|
1163
|
+
const tid = call['@_testItemId'] ?? 'N/A';
|
|
1164
|
+
return { apiName, title, tid };
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Every `<value>` element within an apiCall, tagged with its parent `<argument id>`.
|
|
1168
|
+
* Replicates the back-end's "find the first enclosing argument" lookup so violation
|
|
1169
|
+
* messages name the right argument. Values not inside any argument get `unknown`.
|
|
1170
|
+
*/
|
|
1171
|
+
function getStepValueElements(call) {
|
|
1172
|
+
const argOf = new Map();
|
|
1173
|
+
for (const arg of collectElementsByTag(call, 'argument')) {
|
|
1174
|
+
if (!arg || typeof arg !== 'object')
|
|
1175
|
+
continue;
|
|
1176
|
+
const id = arg['@_id'] ?? 'unknown';
|
|
1177
|
+
for (const v of collectValueElements(arg))
|
|
1178
|
+
if (!argOf.has(v))
|
|
1179
|
+
argOf.set(v, id);
|
|
1180
|
+
}
|
|
1181
|
+
return collectValueElements(call).map((value) => ({ value, argId: argOf.get(value) ?? 'unknown' }));
|
|
1182
|
+
}
|
|
1183
|
+
/** Trimmed text of an argument's direct `<value>` child (handles string and object forms). */
|
|
1184
|
+
function directValueText(arg) {
|
|
1185
|
+
const v = Array.isArray(arg['value']) ? arg['value'][0] : arg['value'];
|
|
1186
|
+
if (v == null)
|
|
1187
|
+
return '';
|
|
1188
|
+
if (typeof v === 'string')
|
|
1189
|
+
return v.trim();
|
|
1190
|
+
if (typeof v !== 'object')
|
|
1191
|
+
return String(v).trim();
|
|
1192
|
+
return nodeText(v);
|
|
1193
|
+
}
|
|
1194
|
+
// RENDER-CASE-001 — the valueClass values that actually exist. This validator only
|
|
1195
|
+
// inspects the `valueClass` attribute, and a full-corpus scan (AllPOCProjects) shows
|
|
1196
|
+
// exactly SIX distinct valueClass values: string, boolean, decimal, id, date, dateTime
|
|
1197
|
+
// — matching the back-end's VALID_VALUE_CLASSES. (The earlier list also carried
|
|
1198
|
+
// `class="..."` tokens — variable/compound/funcCall/value/valueList/operators — and
|
|
1199
|
+
// `integer`; none of those ever appear as a valueClass, so they were dead entries, and
|
|
1200
|
+
// `id` — a real corpus valueClass — was missing. Coordinated with the QH back-end.)
|
|
1201
|
+
const VALUE_CLASS_CASING_VALID = new Set([
|
|
1202
|
+
'string',
|
|
1203
|
+
'boolean',
|
|
1204
|
+
'decimal',
|
|
1205
|
+
'id',
|
|
1206
|
+
'date',
|
|
1207
|
+
'datetime',
|
|
1208
|
+
]);
|
|
1209
|
+
// Canonical Provar spelling for valueClasses whose correct form is NOT all-lowercase.
|
|
1210
|
+
// The corpus uses camelCase `dateTime` exclusively (lowercase `datetime` never appears),
|
|
1211
|
+
// so the casing check must expect `dateTime`; every other valueClass is all-lowercase.
|
|
1212
|
+
const VALUE_CLASS_CANONICAL_CASE = { datetime: 'dateTime' };
|
|
1213
|
+
/** RENDER-CASE-001 — a known valueClass spelled with wrong case (e.g. `Boolean` → `boolean`). */
|
|
1214
|
+
function validateValueClassCasing(tc, rule) {
|
|
1215
|
+
const offenders = [];
|
|
1216
|
+
for (const v of collectValueElements(tc)) {
|
|
1217
|
+
const vc = v['@_valueClass'];
|
|
1218
|
+
if (!vc)
|
|
1219
|
+
continue;
|
|
1220
|
+
const lower = vc.toLowerCase();
|
|
1221
|
+
if (!VALUE_CLASS_CASING_VALID.has(lower))
|
|
1222
|
+
continue;
|
|
1223
|
+
const expected = VALUE_CLASS_CANONICAL_CASE[lower] ?? lower;
|
|
1224
|
+
if (vc !== expected)
|
|
1225
|
+
offenders.push({ valueClass: vc, expected });
|
|
1226
|
+
}
|
|
1227
|
+
if (!offenders.length)
|
|
1228
|
+
return null;
|
|
1229
|
+
const MAX = 5;
|
|
1230
|
+
const reported = Math.min(offenders.length, MAX);
|
|
1231
|
+
let msg = offenders
|
|
1232
|
+
.slice(0, 3)
|
|
1233
|
+
.map((o) => `valueClass='${o.valueClass}' should be '${o.expected}'`)
|
|
1234
|
+
.join('; ');
|
|
1235
|
+
if (reported > 3)
|
|
1236
|
+
msg += ` (+${reported - 3} more)`;
|
|
1237
|
+
if (offenders.length > MAX)
|
|
1238
|
+
msg += ` (total: ${offenders.length})`;
|
|
1239
|
+
return makeViolation(rule, msg, reported);
|
|
1240
|
+
}
|
|
1241
|
+
const BOOLEAN_CASING_BAD = new Set(['True', 'False', 'TRUE', 'FALSE']);
|
|
1242
|
+
/** RENDER-BOOL-001 — `<value valueClass="boolean">` text must be lowercase `true`/`false`. */
|
|
1243
|
+
function validateBooleanCasing(tc, rule) {
|
|
1244
|
+
const offenders = [];
|
|
1245
|
+
for (const v of collectValueElements(tc)) {
|
|
1246
|
+
if (v['@_valueClass'] !== 'boolean')
|
|
1247
|
+
continue;
|
|
1248
|
+
const text = nodeText(v);
|
|
1249
|
+
if (BOOLEAN_CASING_BAD.has(text))
|
|
1250
|
+
offenders.push(text);
|
|
1251
|
+
}
|
|
1252
|
+
if (!offenders.length)
|
|
1253
|
+
return null;
|
|
1254
|
+
const MAX = 5;
|
|
1255
|
+
const reported = Math.min(offenders.length, MAX);
|
|
1256
|
+
let msg = `Boolean values must be lowercase: ${offenders
|
|
1257
|
+
.slice(0, 3)
|
|
1258
|
+
.map((t) => `'${t}' should be '${t.toLowerCase()}'`)
|
|
1259
|
+
.join('; ')}`;
|
|
1260
|
+
if (reported > 3)
|
|
1261
|
+
msg += ` (+${reported - 3} more)`;
|
|
1262
|
+
if (offenders.length > MAX)
|
|
1263
|
+
msg += ` (total: ${offenders.length})`;
|
|
1264
|
+
return makeViolation(rule, msg, reported);
|
|
1265
|
+
}
|
|
1266
|
+
// VALUE-CLASS-001 — the back-end HARDCODES these sets and ignores the rule JSON's
|
|
1267
|
+
// validClasses list, so we mirror the hardcoded sets (note `invalid` and
|
|
1268
|
+
// `namedValues` are accepted, and `dateTime` is camelCase) to stay score-exact.
|
|
1269
|
+
const VALID_VALUE_ELEMENT_CLASSES = new Set([
|
|
1270
|
+
'value',
|
|
1271
|
+
'variable',
|
|
1272
|
+
'compound',
|
|
1273
|
+
'funcCall',
|
|
1274
|
+
'valueList',
|
|
1275
|
+
'namedValues',
|
|
1276
|
+
'uiWait',
|
|
1277
|
+
'uiLocator',
|
|
1278
|
+
'uiTarget',
|
|
1279
|
+
'uiInteraction',
|
|
1280
|
+
'restTarget',
|
|
1281
|
+
'excelTarget',
|
|
1282
|
+
'csvTarget',
|
|
1283
|
+
'url',
|
|
1284
|
+
'template',
|
|
1285
|
+
'add',
|
|
1286
|
+
'sub',
|
|
1287
|
+
'mult',
|
|
1288
|
+
'div',
|
|
1289
|
+
'eq',
|
|
1290
|
+
'ne',
|
|
1291
|
+
'gt',
|
|
1292
|
+
'lt',
|
|
1293
|
+
'ge',
|
|
1294
|
+
'le',
|
|
1295
|
+
'and',
|
|
1296
|
+
'or',
|
|
1297
|
+
'match',
|
|
1298
|
+
'invalid',
|
|
1299
|
+
]);
|
|
1300
|
+
const VALID_VALUE_CLASS_TYPES = new Set([
|
|
1301
|
+
'string',
|
|
1302
|
+
'boolean',
|
|
1303
|
+
'decimal',
|
|
1304
|
+
'id',
|
|
1305
|
+
'date',
|
|
1306
|
+
'dateTime',
|
|
1307
|
+
]);
|
|
1308
|
+
/** Classify one `<value>` for VALUE-CLASS-001, or `null` when it is valid / unattributed. */
|
|
1309
|
+
function classifyInvalidValueClass(v) {
|
|
1310
|
+
const cls = v['@_class'] ?? '';
|
|
1311
|
+
if (!cls)
|
|
1312
|
+
return null;
|
|
1313
|
+
if (!VALID_VALUE_ELEMENT_CLASSES.has(cls))
|
|
1314
|
+
return { kind: 'class', bad: cls };
|
|
1315
|
+
const vc = v['@_valueClass'] ?? '';
|
|
1316
|
+
if (cls === 'value' && vc && !VALID_VALUE_CLASS_TYPES.has(vc))
|
|
1317
|
+
return { kind: 'valueClass', bad: vc };
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
/** VALUE-CLASS-001 — `<value class="…">` must use a valid class (and valueClass when class="value"). */
|
|
1321
|
+
function validateInvalidValueClass(tc, rule) {
|
|
1322
|
+
const offenders = [];
|
|
1323
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1324
|
+
const ctx = stepContext(call);
|
|
1325
|
+
for (const { value, argId } of getStepValueElements(call)) {
|
|
1326
|
+
const c = classifyInvalidValueClass(value);
|
|
1327
|
+
if (c)
|
|
1328
|
+
offenders.push({ argId, ctx, kind: c.kind, bad: c.bad });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!offenders.length)
|
|
1332
|
+
return null;
|
|
1333
|
+
const f = offenders[0];
|
|
1334
|
+
const message = f.kind === 'class'
|
|
1335
|
+
? `Step '${f.ctx.title}' has invalid class="${f.bad}" in argument '${f.argId}'. Valid class values: value, ` +
|
|
1336
|
+
'variable, compound, funcCall, valueList, uiWait, uiLocator, uiTarget, etc. For empty arguments, omit ' +
|
|
1337
|
+
`<value> entirely: <argument id="${f.argId}"/> (testItemId=${f.ctx.tid})`
|
|
1338
|
+
: `Step '${f.ctx.title}' has invalid valueClass="${f.bad}" in argument '${f.argId}'. Valid valueClass values: ` +
|
|
1339
|
+
`string, boolean, decimal, id, date, dateTime. (testItemId=${f.ctx.tid})`;
|
|
1340
|
+
return makeViolation(rule, message, offenders.length);
|
|
1341
|
+
}
|
|
1342
|
+
/** RENDER-DATE-VALUECLASS-001 — `valueClass="date"|"dateTime"` text must be an epoch-millis integer. */
|
|
1343
|
+
function validateDateValueClassFormat(tc, rule) {
|
|
1344
|
+
const offenders = [];
|
|
1345
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1346
|
+
const ctx = stepContext(call);
|
|
1347
|
+
for (const { value, argId } of getStepValueElements(call)) {
|
|
1348
|
+
if (value['@_class'] !== 'value')
|
|
1349
|
+
continue;
|
|
1350
|
+
const vc = value['@_valueClass'] ?? '';
|
|
1351
|
+
if (vc !== 'date' && vc !== 'dateTime')
|
|
1352
|
+
continue;
|
|
1353
|
+
const text = nodeText(value);
|
|
1354
|
+
if (!text || /^\d+$/.test(text))
|
|
1355
|
+
continue;
|
|
1356
|
+
offenders.push({ argId, ctx, vc, text: text.slice(0, 50) });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!offenders.length)
|
|
1360
|
+
return null;
|
|
1361
|
+
const f = offenders[0];
|
|
1362
|
+
const message = `Step '${f.ctx.title}' uses valueClass='${f.vc}' with invalid string value '${f.text}' in argument ` +
|
|
1363
|
+
`'${f.argId}'. valueClass='${f.vc}' requires an epoch timestamp (milliseconds), not a date string. This ` +
|
|
1364
|
+
`causes test case loading failures in Provar. (testItemId=${f.ctx.tid})`;
|
|
1365
|
+
return makeViolation(rule, message, offenders.length);
|
|
1366
|
+
}
|
|
1367
|
+
/** Return the ApexConnect calls in a test case (exact apiId match, nested-aware). */
|
|
1368
|
+
function getApexConnectCalls(tc) {
|
|
1369
|
+
return getAllApiCalls(tc).filter((c) => c['@_apiId'] === APEX_CONNECT_API_ID);
|
|
1370
|
+
}
|
|
1371
|
+
/** APEX-REUSE-CONN-001 — ApexConnect `reuseConnectionName` must be blank. */
|
|
1372
|
+
function validateApexConnectReuseConnection(tc, rule) {
|
|
1373
|
+
const offenders = [];
|
|
1374
|
+
for (const call of getApexConnectCalls(tc)) {
|
|
1375
|
+
const arg = getCallArguments(call).find((a) => a['@_id'] === 'reuseConnectionName');
|
|
1376
|
+
if (!arg)
|
|
1377
|
+
continue;
|
|
1378
|
+
const text = directValueText(arg);
|
|
1379
|
+
if (text)
|
|
1380
|
+
offenders.push({ ctx: stepContext(call), value: text });
|
|
1381
|
+
}
|
|
1382
|
+
if (!offenders.length)
|
|
1383
|
+
return null;
|
|
1384
|
+
const f = offenders[0];
|
|
1385
|
+
const message = `ApexConnect step '${f.ctx.title}' has non-empty reuseConnectionName value '${f.value}'. The ` +
|
|
1386
|
+
`reuseConnectionName argument should be left blank: <argument id="reuseConnectionName"/> (testItemId=${f.ctx.tid})`;
|
|
1387
|
+
return makeViolation(rule, message, offenders.length);
|
|
1388
|
+
}
|
|
1389
|
+
const APEX_CONNECT_VALID_ARGS_DEFAULT = [
|
|
1390
|
+
'connectionName',
|
|
1391
|
+
'resultName',
|
|
1392
|
+
'resultScope',
|
|
1393
|
+
'uiApplicationName',
|
|
1394
|
+
'quickUiLogin',
|
|
1395
|
+
'closeAllPrimaryTabs',
|
|
1396
|
+
'reuseConnectionName',
|
|
1397
|
+
'alreadyOpenBehaviour',
|
|
1398
|
+
'autoCleanup',
|
|
1399
|
+
'cleanupConnectionName',
|
|
1400
|
+
'logFileLocation',
|
|
1401
|
+
'connectionId',
|
|
1402
|
+
'enableObjectIdLogging',
|
|
1403
|
+
'privateBrowsingMode',
|
|
1404
|
+
'lightningMode',
|
|
1405
|
+
'username',
|
|
1406
|
+
'password',
|
|
1407
|
+
'securityToken',
|
|
1408
|
+
'environment',
|
|
1409
|
+
'webBrowser',
|
|
1410
|
+
];
|
|
1411
|
+
/** APEX-CONNECT-ARGS-001 — every ApexConnect `<argument id>` must be in the valid whitelist. */
|
|
1412
|
+
function validateApexConnectValidArguments(tc, rule) {
|
|
1413
|
+
const validIds = new Set(rule.check['validArgumentIds'] ?? APEX_CONNECT_VALID_ARGS_DEFAULT);
|
|
1414
|
+
const offenders = [];
|
|
1415
|
+
for (const call of getApexConnectCalls(tc)) {
|
|
1416
|
+
if (!call['arguments'] || typeof call['arguments'] !== 'object')
|
|
1417
|
+
continue;
|
|
1418
|
+
for (const arg of getCallArguments(call)) {
|
|
1419
|
+
const id = arg['@_id'];
|
|
1420
|
+
if (id && !validIds.has(id))
|
|
1421
|
+
offenders.push({ ctx: stepContext(call), id });
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (!offenders.length)
|
|
1425
|
+
return null;
|
|
1426
|
+
const f = offenders[0];
|
|
1427
|
+
const message = `ApexConnect step '${f.ctx.title}' uses invalid argument ID(s): ${offenders.map((o) => o.id).join(', ')}. ` +
|
|
1428
|
+
'Only the documented ApexConnect argument IDs are valid (connectionName, resultName, resultScope, …, ' +
|
|
1429
|
+
'webBrowser). Leave unused arguments empty (e.g. <argument id="username"/>) rather than inventing IDs. ' +
|
|
1430
|
+
`(testItemId=${f.ctx.tid})`;
|
|
1431
|
+
return makeViolation(rule, message, offenders.length);
|
|
1432
|
+
}
|
|
1433
|
+
/** APEX-CONNECT-CONNID-001 — `connectionId` value must use `valueClass="id"`, not string/other. */
|
|
1434
|
+
function validateApexConnectConnectionIdValueClass(tc, rule) {
|
|
1435
|
+
const offenders = [];
|
|
1436
|
+
for (const call of getApexConnectCalls(tc)) {
|
|
1437
|
+
const arg = getCallArguments(call).find((a) => a['@_id'] === 'connectionId');
|
|
1438
|
+
if (!arg)
|
|
1439
|
+
continue;
|
|
1440
|
+
const ve = Array.isArray(arg['value']) ? arg['value'][0] : arg['value'];
|
|
1441
|
+
if (!ve || typeof ve !== 'object')
|
|
1442
|
+
continue;
|
|
1443
|
+
const v = ve;
|
|
1444
|
+
const vc = v['@_valueClass'] ?? '';
|
|
1445
|
+
if (v['@_class'] !== 'value' || !vc || vc === 'id')
|
|
1446
|
+
continue;
|
|
1447
|
+
offenders.push({ ctx: stepContext(call), wrong: vc, value: nodeText(v) });
|
|
1448
|
+
}
|
|
1449
|
+
if (!offenders.length)
|
|
1450
|
+
return null;
|
|
1451
|
+
const f = offenders[0];
|
|
1452
|
+
const message = `ApexConnect step '${f.ctx.title}' uses incorrect valueClass='${f.wrong}' for connectionId argument. The ` +
|
|
1453
|
+
"connectionId must use valueClass='id' with a GUID value, NOT valueClass='string'. Current value: " +
|
|
1454
|
+
`'${f.value}'. If you have no specific connection GUID, leave it empty: <argument id="connectionId"/> ` +
|
|
1455
|
+
`(testItemId=${f.ctx.tid})`;
|
|
1456
|
+
return makeViolation(rule, message, offenders.length);
|
|
1457
|
+
}
|
|
1458
|
+
/** Tags found directly under a `<namedValues>` container that are not the allowed `namedValue`. */
|
|
1459
|
+
function namedValuesInvalidChildren(nv) {
|
|
1460
|
+
const bad = [];
|
|
1461
|
+
for (const [k, v] of Object.entries(nv)) {
|
|
1462
|
+
if (k.startsWith('@_') || k === '#text' || k === 'namedValue')
|
|
1463
|
+
continue;
|
|
1464
|
+
const instances = toArr(v).length; // one entry per element instance (mirrors the back-end count)
|
|
1465
|
+
for (let i = 0; i < instances; i++)
|
|
1466
|
+
bad.push(k);
|
|
1467
|
+
}
|
|
1468
|
+
return bad;
|
|
1469
|
+
}
|
|
1470
|
+
/** SETVALUES-INVALID-ELEMENT-001 — reject hallucinated `<namedValueSet>`/`<name>` and bad namedValues children. */
|
|
1471
|
+
function validateSetValuesInvalidElements(tc, rule) {
|
|
1472
|
+
const invalidElements = rule.check['invalidElements'] ?? ['namedValueSet', 'name'];
|
|
1473
|
+
let count = 0;
|
|
1474
|
+
let first = null;
|
|
1475
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1476
|
+
if (call['@_apiId'] !== SETVALUES_API_ID)
|
|
1477
|
+
continue;
|
|
1478
|
+
const ctx = stepContext(call);
|
|
1479
|
+
for (const elem of invalidElements) {
|
|
1480
|
+
if (collectElementsByTag(call, elem).length) {
|
|
1481
|
+
count++;
|
|
1482
|
+
first ??= { ctx, elem };
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
for (const nv of collectElementsByTag(call, 'namedValues')) {
|
|
1486
|
+
if (!nv || typeof nv !== 'object')
|
|
1487
|
+
continue;
|
|
1488
|
+
for (const childTag of namedValuesInvalidChildren(nv)) {
|
|
1489
|
+
count++;
|
|
1490
|
+
first ??= { ctx, elem: childTag, context: 'inside <namedValues>' };
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (!first)
|
|
1495
|
+
return null;
|
|
1496
|
+
const ctxStr = first.context ? ` ${first.context}` : '';
|
|
1497
|
+
const message = `SetValues step '${first.ctx.title}' contains invalid element <${first.elem}>${ctxStr}. SetValues must use ` +
|
|
1498
|
+
'<namedValues mutable="Mutable"> with <namedValue name="valuePath|value|valueScope"> children. Do not use ' +
|
|
1499
|
+
`<namedValueSet> or <name> elements. (testItemId=${first.ctx.tid})`;
|
|
1500
|
+
return makeViolation(rule, message, count);
|
|
1501
|
+
}
|
|
1502
|
+
// ── Back-end-only rules (Tier 4) ─────────────────────────────────────────────
|
|
1503
|
+
// Ports of seven Quality Hub validators that existed only in the back-end rule
|
|
1504
|
+
// set. All seven rules are severity=major / weight=5. Score parity: six emit a
|
|
1505
|
+
// single violation (the back-end returns the first offender; `count` is set only
|
|
1506
|
+
// for the two UI-locator checks, and only when >1 offender); `varStringLiteral`
|
|
1507
|
+
// emits ONE violation per offending value (the back-end returns a list, scored
|
|
1508
|
+
// linearly) — do not collapse it.
|
|
1509
|
+
const ASSERT_VALUES_API_ID = 'com.provar.plugins.bundled.apis.AssertValues';
|
|
1510
|
+
const DB_CONNECT_API_ID = 'com.provar.plugins.bundled.apis.db.DbConnect';
|
|
1511
|
+
const UI_DO_ACTION_API_ID = 'com.provar.plugins.forcedotcom.core.ui.UiDoAction';
|
|
1512
|
+
// DB operation steps whose dbConnectionName must reference a DbConnect resultName
|
|
1513
|
+
// (both the modern `db.*` and legacy `data.*` namespaces, mirroring the back-end).
|
|
1514
|
+
const DB_OPERATION_API_IDS = new Set([
|
|
1515
|
+
'com.provar.plugins.bundled.apis.db.SqlQuery',
|
|
1516
|
+
'com.provar.plugins.bundled.apis.db.DbRead',
|
|
1517
|
+
'com.provar.plugins.bundled.apis.db.DbInsert',
|
|
1518
|
+
'com.provar.plugins.bundled.apis.db.DbUpdate',
|
|
1519
|
+
'com.provar.plugins.bundled.apis.db.DbDelete',
|
|
1520
|
+
'com.provar.plugins.bundled.apis.data.SqlQuery',
|
|
1521
|
+
'com.provar.plugins.bundled.apis.data.DbRead',
|
|
1522
|
+
'com.provar.plugins.bundled.apis.data.DbInsert',
|
|
1523
|
+
'com.provar.plugins.bundled.apis.data.DbUpdate',
|
|
1524
|
+
'com.provar.plugins.bundled.apis.data.DbDelete',
|
|
1525
|
+
]);
|
|
1526
|
+
/** Escape a literal for safe embedding in a RegExp (mirrors Python's re.escape). */
|
|
1527
|
+
function escapeRegExp(s) {
|
|
1528
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Resolve an argument's text value, mirroring the back-end `get_argument_value`:
|
|
1532
|
+
* `variable` → first `<path>` element name, `compound` → concatenated `<parts>`
|
|
1533
|
+
* text, otherwise the element text. Wrapper-aware (handles both the `<arguments>`
|
|
1534
|
+
* wrapper and direct `<argument>` children) via {@link findArgumentById}.
|
|
1535
|
+
*/
|
|
1536
|
+
function resolvedArgText(call, argId) {
|
|
1537
|
+
const arg = findArgumentById(call, argId);
|
|
1538
|
+
if (!arg)
|
|
1539
|
+
return '';
|
|
1540
|
+
const raw = arg['value'];
|
|
1541
|
+
const ve = Array.isArray(raw) ? raw[0] : raw;
|
|
1542
|
+
if (ve == null)
|
|
1543
|
+
return '';
|
|
1544
|
+
if (typeof ve === 'string')
|
|
1545
|
+
return ve.trim();
|
|
1546
|
+
if (typeof ve !== 'object')
|
|
1547
|
+
return String(ve).trim();
|
|
1548
|
+
const v = ve;
|
|
1549
|
+
const cls = v['@_class'];
|
|
1550
|
+
if (cls === 'variable') {
|
|
1551
|
+
const firstPath = toArr(v['path'])[0];
|
|
1552
|
+
return (firstPath?.['@_element'] ?? '').trim();
|
|
1553
|
+
}
|
|
1554
|
+
if (cls === 'compound') {
|
|
1555
|
+
const partsNode = v['parts']?.['value'];
|
|
1556
|
+
return toArr(partsNode)
|
|
1557
|
+
.filter((p) => p && typeof p === 'object')
|
|
1558
|
+
.map((p) => nodeText(p))
|
|
1559
|
+
.join('');
|
|
1560
|
+
}
|
|
1561
|
+
return nodeText(v);
|
|
1562
|
+
}
|
|
1563
|
+
// Matches a bare variable token only: `{Name}` or `{Obj.Field}`. The character
|
|
1564
|
+
// class `[\w.]` excludes `:`, so binding-style expressions such as
|
|
1565
|
+
// `{targetUrl:object}` never match — they are inherently safe and need no
|
|
1566
|
+
// argument-name exemption.
|
|
1567
|
+
const VAR_LITERAL_PATTERN = /^\{[\w.]+\}$/;
|
|
1568
|
+
/**
|
|
1569
|
+
* VAR-STRING-LITERAL-001 — a `{Var}`/`{Obj.Field}` token stored as
|
|
1570
|
+
* `class="value" valueClass="string"` instead of `class="variable"`. Provar does
|
|
1571
|
+
* not resolve it at runtime, so the API silently receives the literal text. Emits
|
|
1572
|
+
* ONE violation per offending value (the back-end returns a list).
|
|
1573
|
+
*
|
|
1574
|
+
* Local correction (PDX-508): the back-end exempts `sfUiTargetObjectId` /
|
|
1575
|
+
* `sfUiTargetResultName` from this check, but field evidence shows a bare
|
|
1576
|
+
* `{Variable}` in those UI-target args is NOT interpolated — it lands in the URL
|
|
1577
|
+
* as `%7B…%7D` and the step hard-fails (a load/exec stopper, not a warning). We
|
|
1578
|
+
* therefore do NOT exempt those args. Binding-style `{ns:key}` expressions stay
|
|
1579
|
+
* safe because {@link VAR_LITERAL_PATTERN} already excludes them (the colon). A
|
|
1580
|
+
* matching change is queued for the Quality Hub back-end so the score parity is
|
|
1581
|
+
* restored once both ship.
|
|
1582
|
+
*/
|
|
1583
|
+
function validateVarStringLiteral(tc, rule) {
|
|
1584
|
+
const violations = [];
|
|
1585
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1586
|
+
const ctx = stepContext(call);
|
|
1587
|
+
for (const { value } of getStepValueElements(call)) {
|
|
1588
|
+
if (value['@_class'] !== 'value' || value['@_valueClass'] !== 'string')
|
|
1589
|
+
continue;
|
|
1590
|
+
const text = nodeText(value);
|
|
1591
|
+
if (!VAR_LITERAL_PATTERN.test(text))
|
|
1592
|
+
continue;
|
|
1593
|
+
violations.push(makeViolation(rule, `Argument value "${text}" looks like a variable reference but is stored as a plain string — Provar ` +
|
|
1594
|
+
'will not resolve it at runtime. Use <value class="variable"> instead ' +
|
|
1595
|
+
`(step '${ctx.title}', testItemId=${ctx.tid})`));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return violations;
|
|
1599
|
+
}
|
|
1600
|
+
/** CONN-DB-002 — every DB operation's dbConnectionName must match a DbConnect resultName in the test. */
|
|
1601
|
+
function validateDbConnectResultNameMismatch(tc, rule) {
|
|
1602
|
+
const calls = getAllApiCalls(tc);
|
|
1603
|
+
const resultNames = new Set();
|
|
1604
|
+
for (const call of calls) {
|
|
1605
|
+
if (call['@_apiId'] !== DB_CONNECT_API_ID)
|
|
1606
|
+
continue;
|
|
1607
|
+
const rn = resolvedArgText(call, 'resultName');
|
|
1608
|
+
if (rn)
|
|
1609
|
+
resultNames.add(rn);
|
|
1610
|
+
}
|
|
1611
|
+
if (!resultNames.size)
|
|
1612
|
+
return null; // no DbConnect resultName — CONN-DB-001 covers a missing DbConnect
|
|
1613
|
+
const offenders = [];
|
|
1614
|
+
for (const call of calls) {
|
|
1615
|
+
if (!DB_OPERATION_API_IDS.has(call['@_apiId']))
|
|
1616
|
+
continue;
|
|
1617
|
+
const ref = resolvedArgText(call, 'dbConnectionName');
|
|
1618
|
+
if (!ref || resultNames.has(ref))
|
|
1619
|
+
continue;
|
|
1620
|
+
offenders.push(`'${stepContext(call).title.slice(0, 40)}' uses dbConnectionName='${ref}'`);
|
|
1621
|
+
}
|
|
1622
|
+
if (!offenders.length)
|
|
1623
|
+
return null;
|
|
1624
|
+
const names = [...resultNames].sort().join(', ');
|
|
1625
|
+
return makeViolation(rule, `DbConnect resultName does not match dbConnectionName in ${offenders.length} DB operation(s): ` +
|
|
1626
|
+
`${offenders[0]} but DbConnect resultName(s) are: ${names}`);
|
|
1627
|
+
}
|
|
1628
|
+
/** The `<value>` children of a SetValues `<namedValue name="value">` (the assigned-value slot). */
|
|
1629
|
+
function getSetValuesValueElements(call) {
|
|
1630
|
+
const out = [];
|
|
1631
|
+
for (const nv of collectElementsByTag(call, 'namedValue')) {
|
|
1632
|
+
if (!nv || typeof nv !== 'object' || nv['@_name'] !== 'value')
|
|
1633
|
+
continue;
|
|
1634
|
+
for (const v of toArr(nv['value'])) {
|
|
1635
|
+
if (v && typeof v === 'object')
|
|
1636
|
+
out.push(v);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return out;
|
|
1640
|
+
}
|
|
1641
|
+
const SETVALUES_FUNC_EXPR = /\{[A-Za-z][A-Za-z0-9]*\s*\([^)]*\)\s*\}/;
|
|
1642
|
+
const SETVALUES_ZERO_IDX = /\{[^}]*\[0\][^}]*\}/;
|
|
1643
|
+
/** First SetValues `valueClass="string"` value whose text matches `re` (string-template anti-patterns). */
|
|
1644
|
+
function firstSetValuesStringMatch(tc, re) {
|
|
1645
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1646
|
+
if (call['@_apiId'] !== SETVALUES_API_ID)
|
|
1647
|
+
continue;
|
|
1648
|
+
for (const ve of getSetValuesValueElements(call)) {
|
|
1649
|
+
if (ve['@_class'] !== 'value' || ve['@_valueClass'] !== 'string')
|
|
1650
|
+
continue;
|
|
1651
|
+
const text = nodeText(ve);
|
|
1652
|
+
if (re.test(text))
|
|
1653
|
+
return { ctx: stepContext(call), text };
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
/** SETVALUES-FUNC-STR-001 — SetValues uses `{Func(args)}` string interpolation instead of a `funcCall` value. */
|
|
1659
|
+
function validateSetValuesFuncCallString(tc, rule) {
|
|
1660
|
+
const hit = firstSetValuesStringMatch(tc, SETVALUES_FUNC_EXPR);
|
|
1661
|
+
if (!hit)
|
|
1662
|
+
return null;
|
|
1663
|
+
return makeViolation(rule, 'SetValues uses string interpolation for a function call — the value will not be evaluated: ' +
|
|
1664
|
+
`'${hit.ctx.title.slice(0, 50)}' value='${hit.text.slice(0, 60)}' (testItemId=${hit.ctx.tid})`);
|
|
1665
|
+
}
|
|
1666
|
+
/** SETVALUES-ZERO-IDX-001 — SetValues string template uses a 0 index (templates are 1-indexed). */
|
|
1667
|
+
function validateSetValuesZeroIndexString(tc, rule) {
|
|
1668
|
+
const hit = firstSetValuesStringMatch(tc, SETVALUES_ZERO_IDX);
|
|
1669
|
+
if (!hit)
|
|
1670
|
+
return null;
|
|
1671
|
+
return makeViolation(rule, 'SetValues string expression uses a [0] index — Provar string templates are 1-indexed, causing an ' +
|
|
1672
|
+
`out-of-bounds error at runtime: '${hit.ctx.title.slice(0, 50)}' value='${hit.text.slice(0, 60)}' — use ` +
|
|
1673
|
+
`[1] for the first item (testItemId=${hit.ctx.tid})`);
|
|
1674
|
+
}
|
|
1675
|
+
const ASSERT_WHOLE_EXPR = /^\s*\{[^{}]+\}\s*$/;
|
|
1676
|
+
/** The direct `<value>` element child of an argument (wrapper-aware), or undefined. */
|
|
1677
|
+
function directArgValueElement(call, argId) {
|
|
1678
|
+
const arg = findArgumentById(call, argId);
|
|
1679
|
+
if (!arg)
|
|
1680
|
+
return undefined;
|
|
1681
|
+
const raw = arg['value'];
|
|
1682
|
+
const ve = Array.isArray(raw) ? raw[0] : raw;
|
|
1683
|
+
return ve && typeof ve === 'object' ? ve : undefined;
|
|
1684
|
+
}
|
|
1685
|
+
/** ASSERT-STR-VAR-001 — AssertValues references a variable via a `{Var}` string literal instead of `class="variable"`. */
|
|
1686
|
+
function validateAssertValuesStringExpr(tc, rule) {
|
|
1687
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1688
|
+
if (call['@_apiId'] !== ASSERT_VALUES_API_ID)
|
|
1689
|
+
continue;
|
|
1690
|
+
for (const argId of ['expectedValue', 'actualValue']) {
|
|
1691
|
+
const v = directArgValueElement(call, argId);
|
|
1692
|
+
if (!v || v['@_class'] !== 'value' || v['@_valueClass'] !== 'string')
|
|
1693
|
+
continue;
|
|
1694
|
+
const text = nodeText(v);
|
|
1695
|
+
if (!ASSERT_WHOLE_EXPR.test(text))
|
|
1696
|
+
continue;
|
|
1697
|
+
const ctx = stepContext(call);
|
|
1698
|
+
return makeViolation(rule, 'AssertValues uses a string literal to reference a variable — the assertion compares the literal text, ' +
|
|
1699
|
+
`not the variable value: '${ctx.title.slice(0, 50)}' ${argId}='${text.slice(0, 60)}' should use ` +
|
|
1700
|
+
`<value class="variable"> (testItemId=${ctx.tid})`);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
/** The `uri` of a UiDoAction's `locator` argument value (`class="uiLocator"`), or '' if absent. */
|
|
1706
|
+
function getUiDoActionLocatorUri(call) {
|
|
1707
|
+
const arg = getCallArguments(call).find((a) => a['@_id'] === 'locator');
|
|
1708
|
+
if (!arg)
|
|
1709
|
+
return '';
|
|
1710
|
+
for (const v of toArr(arg['value'])) {
|
|
1711
|
+
if (v && typeof v === 'object' && v['@_class'] === 'uiLocator') {
|
|
1712
|
+
return v['@_uri'] ?? '';
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return '';
|
|
1716
|
+
}
|
|
1717
|
+
// Standard SF flow buttons whose locator name must use the corpus-validated casing/path.
|
|
1718
|
+
const UI_WRONG_BUTTONS = [
|
|
1719
|
+
['Cancel', "use 'name=cancel' (lowercase)"],
|
|
1720
|
+
['continue', "the Continue button on record type selection screens uses 'name=save&path=selectRecordType'"],
|
|
1721
|
+
['Continue', "the Continue button on record type selection screens uses 'name=save&path=selectRecordType'"],
|
|
1722
|
+
];
|
|
1723
|
+
/** UI-LOCATOR-BUTTON-CASING-001 — Cancel/Continue flow buttons must use the correct locator name. */
|
|
1724
|
+
function validateUiLocatorButtonCasing(tc, rule) {
|
|
1725
|
+
const offenders = [];
|
|
1726
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1727
|
+
if (call['@_apiId'] !== UI_DO_ACTION_API_ID)
|
|
1728
|
+
continue;
|
|
1729
|
+
const uri = getUiDoActionLocatorUri(call);
|
|
1730
|
+
if (!uri)
|
|
1731
|
+
continue;
|
|
1732
|
+
for (const [wrong, explanation] of UI_WRONG_BUTTONS) {
|
|
1733
|
+
if (new RegExp(`name=${escapeRegExp(wrong)}(&|$)`).test(uri)) {
|
|
1734
|
+
offenders.push({ ctx: stepContext(call), wrong, explanation, uri });
|
|
1735
|
+
break; // only report the first match per step (mirrors the back-end)
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (!offenders.length)
|
|
1740
|
+
return null;
|
|
1741
|
+
const f = offenders[0];
|
|
1742
|
+
return makeViolation(rule, `Step '${f.ctx.title}' uses incorrect button name 'name=${f.wrong}': ${f.explanation}. Incorrect button ` +
|
|
1743
|
+
`names cause Provar to show 'Not Available' and fail at runtime. Current URI: ${f.uri.slice(0, 120)} ` +
|
|
1744
|
+
`(testItemId=${f.ctx.tid})`, offenders.length);
|
|
1745
|
+
}
|
|
1746
|
+
const UI_RECORDTYPE_WRONG = /name=recordType(Id)?(&|$)/;
|
|
1747
|
+
/** UI-LOCATOR-RECORDTYPE-001 — the Record Type picker locator must use `name=RecordType`, not `name=recordType(Id)`. */
|
|
1748
|
+
function validateUiLocatorRecordTypeField(tc, rule) {
|
|
1749
|
+
const offenders = [];
|
|
1750
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1751
|
+
if (call['@_apiId'] !== UI_DO_ACTION_API_ID)
|
|
1752
|
+
continue;
|
|
1753
|
+
const uri = getUiDoActionLocatorUri(call);
|
|
1754
|
+
if (!uri || !UI_RECORDTYPE_WRONG.test(uri))
|
|
1755
|
+
continue;
|
|
1756
|
+
offenders.push({ ctx: stepContext(call), uri });
|
|
1757
|
+
}
|
|
1758
|
+
if (!offenders.length)
|
|
1759
|
+
return null;
|
|
1760
|
+
const f = offenders[0];
|
|
1761
|
+
return makeViolation(rule, `Step '${f.ctx.title}' uses an incorrect Record Type field locator. The Record Type picker must use ` +
|
|
1762
|
+
"'name=RecordType' (not 'name=recordTypeId' or 'name=recordType') with 'field=RecordTypeId' in the binding. " +
|
|
1763
|
+
`Current URI: ${f.uri.slice(0, 150)} (testItemId=${f.ctx.tid})`, offenders.length);
|
|
1764
|
+
}
|
|
1765
|
+
// ── Structural / load-affecting check types (Tier 5) ─────────────────────────
|
|
1766
|
+
// Faithful ports of nine Quality Hub structural validators. Six emit a single
|
|
1767
|
+
// first-offender violation with no `count`; validFuncCallId, the two UiAssert
|
|
1768
|
+
// structure checks, and bindingParameterOrder collect every offender and emit
|
|
1769
|
+
// one violation carrying `count` (capped at 5 for funcCall), matching the
|
|
1770
|
+
// back-end so the weighted-deduction score stays in parity with the Lambda.
|
|
1771
|
+
const UI_ASSERT_API_ID = 'com.provar.plugins.forcedotcom.core.ui.UiAssert';
|
|
1772
|
+
// FUNCCALL-VALID-001 — Provar's built-in funcCall ids (exact back-end whitelist, 20 entries).
|
|
1773
|
+
const VALID_FUNCCALL_IDS = new Set([
|
|
1774
|
+
'TestCaseName',
|
|
1775
|
+
'TestCasePath',
|
|
1776
|
+
'TestCaseOutcome',
|
|
1777
|
+
'TestCaseSuccessful',
|
|
1778
|
+
'TestCaseErrors',
|
|
1779
|
+
'TestRunErrors',
|
|
1780
|
+
'StringReplace',
|
|
1781
|
+
'StringTrim',
|
|
1782
|
+
'StringNormalize',
|
|
1783
|
+
'DateAdd',
|
|
1784
|
+
'DateFormat',
|
|
1785
|
+
'DateParse',
|
|
1786
|
+
'Count',
|
|
1787
|
+
'Round',
|
|
1788
|
+
'NumberFormat',
|
|
1789
|
+
'Not',
|
|
1790
|
+
'IsSorted',
|
|
1791
|
+
'GetEnvironmentVariable',
|
|
1792
|
+
'GetSelectedEnvironment',
|
|
1793
|
+
'UniqueId',
|
|
1794
|
+
]);
|
|
1795
|
+
/** FUNCCALL-VALID-001 — every `<value class="funcCall">` `id` must be a real Provar built-in function. */
|
|
1796
|
+
function validateValidFuncCallId(tc, rule) {
|
|
1797
|
+
const offenders = [];
|
|
1798
|
+
for (const v of collectValueElements(tc)) {
|
|
1799
|
+
if (v['@_class'] !== 'funcCall')
|
|
1800
|
+
continue;
|
|
1801
|
+
const id = v['@_id'] ?? '';
|
|
1802
|
+
if (id && !VALID_FUNCCALL_IDS.has(id))
|
|
1803
|
+
offenders.push(id);
|
|
1804
|
+
}
|
|
1805
|
+
if (!offenders.length)
|
|
1806
|
+
return null;
|
|
1807
|
+
const MAX = 5;
|
|
1808
|
+
let msg = `Unknown funcCall id(s) — these functions do not exist in Provar: ${offenders
|
|
1809
|
+
.slice(0, 3)
|
|
1810
|
+
.map((id) => `'${id}'`)
|
|
1811
|
+
.join(', ')}`;
|
|
1812
|
+
if (offenders.length > 3)
|
|
1813
|
+
msg += ` (+${offenders.length - 3} more)`;
|
|
1814
|
+
msg +=
|
|
1815
|
+
'. Valid functions include Count, DateAdd, DateFormat, Round, StringReplace, UniqueId, etc. ' +
|
|
1816
|
+
'For string concatenation use <value class="compound"><parts>…</parts></value>.';
|
|
1817
|
+
return makeViolation(rule, msg, Math.min(offenders.length, MAX));
|
|
1818
|
+
}
|
|
1819
|
+
// RENDER-ROOT-001 — the only attributes allowed on the root <testCase> element.
|
|
1820
|
+
const VALID_ROOT_ATTRS = new Set([
|
|
1821
|
+
'guid',
|
|
1822
|
+
'id',
|
|
1823
|
+
'name',
|
|
1824
|
+
'visibility',
|
|
1825
|
+
'registryId',
|
|
1826
|
+
'failureBehaviour',
|
|
1827
|
+
]);
|
|
1828
|
+
/** RENDER-ROOT-001 — the root `<testCase>` element must not carry unknown attributes. */
|
|
1829
|
+
function validateRootAttributes(tc, rule) {
|
|
1830
|
+
const unknown = Object.keys(tc)
|
|
1831
|
+
.filter((k) => k.startsWith('@_'))
|
|
1832
|
+
.map((k) => k.slice(2))
|
|
1833
|
+
.filter((a) => !VALID_ROOT_ATTRS.has(a));
|
|
1834
|
+
if (!unknown.length)
|
|
1835
|
+
return null;
|
|
1836
|
+
return makeViolation(rule, `Unknown root attributes: ${unknown.join(', ')}`);
|
|
1837
|
+
}
|
|
1838
|
+
/** SETVALUES-STRUCTURE-001 — every SetValues step must contain a `<namedValues>` container. */
|
|
1839
|
+
function validateSetValuesStructure(tc, rule) {
|
|
1840
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1841
|
+
if (call['@_apiId'] !== SETVALUES_API_ID)
|
|
1842
|
+
continue;
|
|
1843
|
+
// Data-driven SetValues pull their values from an external source declared in
|
|
1844
|
+
// <parameterValueSources> (e.g. an Excel/CSV binding) and legitimately carry an
|
|
1845
|
+
// empty <value class="valueList"/> with no inline <namedValues>. Not a defect.
|
|
1846
|
+
if (call['parameterValueSources'] != null)
|
|
1847
|
+
continue;
|
|
1848
|
+
if (collectElementsByTag(call, 'namedValues').length)
|
|
1849
|
+
continue;
|
|
1850
|
+
return makeViolation(rule, `SetValues step missing <namedValues> container (testItemId=${stepContext(call).tid})`);
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
/** SETVALUES-NAME-001 — every `<namedValue>` in a SetValues step must carry a `name` attribute. */
|
|
1855
|
+
function validateNamedValueName(tc, rule) {
|
|
1856
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1857
|
+
if (call['@_apiId'] !== SETVALUES_API_ID)
|
|
1858
|
+
continue;
|
|
1859
|
+
for (const nv of collectElementsByTag(call, 'namedValue')) {
|
|
1860
|
+
if (!nv || typeof nv !== 'object' || nv['@_name'])
|
|
1861
|
+
continue;
|
|
1862
|
+
return makeViolation(rule, `namedValue in SetValues step missing name attribute (testItemId=${stepContext(call).tid})`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
// The QEditor SetValues form stores each assignment as a triple of namedValue slots:
|
|
1868
|
+
// `valuePath` (target field), `value` (data), `valueScope`. Any of these may be left
|
|
1869
|
+
// empty — a blank `value` sets the field to blank, and a wholly-blank row is an unused
|
|
1870
|
+
// row Provar simply ignores. So an empty structural slot is NOT a "missing value" defect.
|
|
1871
|
+
const SETVALUES_BLANKABLE_SLOTS = new Set(['valuePath', 'value', 'valueScope']);
|
|
1872
|
+
/** SETVALUES-VALUE-001 — every `<namedValue>` in a SetValues step must contain a child `<value>`. */
|
|
1873
|
+
function validateNamedValueValue(tc, rule) {
|
|
1874
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1875
|
+
if (call['@_apiId'] !== SETVALUES_API_ID)
|
|
1876
|
+
continue;
|
|
1877
|
+
for (const nv of collectElementsByTag(call, 'namedValue')) {
|
|
1878
|
+
if (!nv || typeof nv !== 'object')
|
|
1879
|
+
continue;
|
|
1880
|
+
const node = nv;
|
|
1881
|
+
if (node['value'] != null)
|
|
1882
|
+
continue;
|
|
1883
|
+
// A blank structural slot (valuePath/value/valueScope) is valid (empty value /
|
|
1884
|
+
// unused row). Only a non-standard namedValue missing its value is a real defect.
|
|
1885
|
+
if (SETVALUES_BLANKABLE_SLOTS.has(node['@_name'] ?? ''))
|
|
1886
|
+
continue;
|
|
1887
|
+
return makeViolation(rule, `namedValue in SetValues step missing value element (testItemId=${stepContext(call).tid})`);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
/** UI-ASSERT-STRUCT-002 — UiAssert steps must not contain a (hallucinated) `<generatedParameters>` element. */
|
|
1893
|
+
function validateUiAssertHallucinatedGeneratedParameters(tc, rule) {
|
|
1894
|
+
const offenders = [];
|
|
1895
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1896
|
+
if (call['@_apiId'] !== UI_ASSERT_API_ID || call['generatedParameters'] == null)
|
|
1897
|
+
continue;
|
|
1898
|
+
offenders.push(stepContext(call));
|
|
1899
|
+
}
|
|
1900
|
+
if (!offenders.length)
|
|
1901
|
+
return null;
|
|
1902
|
+
const f = offenders[0];
|
|
1903
|
+
return makeViolation(rule, `UiAssert step '${f.title}' contains a hallucinated <generatedParameters> element — UiAssert steps never ` +
|
|
1904
|
+
`contain generatedParameters; remove the entire section (testItemId=${f.tid})`, offenders.length);
|
|
1905
|
+
}
|
|
1906
|
+
// UI-ASSERT-STRUCT-001 — arguments every UiAssert step must declare (even if empty).
|
|
1907
|
+
const UI_ASSERT_REQUIRED_ARGS = [
|
|
1908
|
+
'fieldAssertions',
|
|
1909
|
+
'columnAssertions',
|
|
1910
|
+
'pageAssertions',
|
|
1911
|
+
'resultScope',
|
|
1912
|
+
'captureAfter',
|
|
1913
|
+
'beforeWait',
|
|
1914
|
+
'autoRetry',
|
|
1915
|
+
];
|
|
1916
|
+
/** UI-ASSERT-STRUCT-001 — a UiAssert step is missing one or more of its required arguments. */
|
|
1917
|
+
function validateUiAssertMissingArguments(tc, rule) {
|
|
1918
|
+
const offenders = [];
|
|
1919
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1920
|
+
if (call['@_apiId'] !== UI_ASSERT_API_ID)
|
|
1921
|
+
continue;
|
|
1922
|
+
const argsNode = call['arguments'];
|
|
1923
|
+
let missing;
|
|
1924
|
+
if (!argsNode || typeof argsNode !== 'object') {
|
|
1925
|
+
missing = [...UI_ASSERT_REQUIRED_ARGS];
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
const existing = new Set();
|
|
1929
|
+
for (const a of getCallArguments(call)) {
|
|
1930
|
+
const id = a['@_id'];
|
|
1931
|
+
if (id)
|
|
1932
|
+
existing.add(id);
|
|
1933
|
+
}
|
|
1934
|
+
missing = UI_ASSERT_REQUIRED_ARGS.filter((r) => !existing.has(r));
|
|
1935
|
+
}
|
|
1936
|
+
if (missing.length)
|
|
1937
|
+
offenders.push({ ctx: stepContext(call), missing });
|
|
1938
|
+
}
|
|
1939
|
+
if (!offenders.length)
|
|
1940
|
+
return null;
|
|
1941
|
+
const f = offenders[0];
|
|
1942
|
+
return makeViolation(rule, `UiAssert step '${f.ctx.title}' is missing required arguments: ${f.missing.join(', ')}. All UiAssert steps ` +
|
|
1943
|
+
'must include fieldAssertions, columnAssertions, pageAssertions, resultScope, captureAfter, beforeWait, and ' +
|
|
1944
|
+
`autoRetry (even if empty) (testItemId=${f.ctx.tid})`, offenders.length);
|
|
1945
|
+
}
|
|
1946
|
+
// UI-BINDING-ORDER-001 — binding URIs must list object= before action=/field= (percent-encoded).
|
|
1947
|
+
const BINDING_WRONG_ACTION_FIRST = /object%3Faction%3D[^%]+%26object%3D/;
|
|
1948
|
+
const BINDING_WRONG_FIELD_FIRST = /object%3Ffield%3D[^%]+%26object%3D/;
|
|
1949
|
+
const BINDING_ACTION_EXTRACT = /action%3D([^%&]+)%26object%3D([^%&"]+)/;
|
|
1950
|
+
const BINDING_FIELD_EXTRACT = /field%3D([^%&]+)%26object%3D([^%&"]+)/;
|
|
1951
|
+
/** Classify a binding URI's parameter order, returning the wrong/correct pair or null when fine. */
|
|
1952
|
+
function classifyBindingOrder(uri) {
|
|
1953
|
+
if (BINDING_WRONG_ACTION_FIRST.test(uri)) {
|
|
1954
|
+
const m = BINDING_ACTION_EXTRACT.exec(uri);
|
|
1955
|
+
if (m) {
|
|
1956
|
+
const o = m[2].replace(/&/g, '').replace(/&/g, '');
|
|
1957
|
+
return { wrong: `action=${m[1]}&object=${o}`, correct: `object=${o}&action=${m[1]}` };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
else if (BINDING_WRONG_FIELD_FIRST.test(uri)) {
|
|
1961
|
+
const m = BINDING_FIELD_EXTRACT.exec(uri);
|
|
1962
|
+
if (m) {
|
|
1963
|
+
const o = m[2].replace(/&/g, '').replace(/&/g, '');
|
|
1964
|
+
return { wrong: `field=${m[1]}&object=${o}`, correct: `object=${o}&field=${m[1]}` };
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
/** UI-BINDING-ORDER-001 — a `uiLocator` binding lists object= after action=/field= (non-standard order). */
|
|
1970
|
+
function validateBindingParameterOrder(tc, rule) {
|
|
1971
|
+
const seen = new Set();
|
|
1972
|
+
const offenders = [];
|
|
1973
|
+
for (const call of getAllApiCalls(tc)) {
|
|
1974
|
+
const ctx = stepContext(call);
|
|
1975
|
+
for (const v of collectValueElements(call)) {
|
|
1976
|
+
if (v['@_class'] !== 'uiLocator' || seen.has(v))
|
|
1977
|
+
continue;
|
|
1978
|
+
seen.add(v);
|
|
1979
|
+
const uri = v['@_uri'] ?? '';
|
|
1980
|
+
if (!uri || !uri.includes('binding='))
|
|
1981
|
+
continue;
|
|
1982
|
+
const verdict = classifyBindingOrder(uri);
|
|
1983
|
+
if (verdict)
|
|
1984
|
+
offenders.push({ ctx, ...verdict });
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
if (!offenders.length)
|
|
1988
|
+
return null;
|
|
1989
|
+
const f = offenders[0];
|
|
1990
|
+
return makeViolation(rule, `UI binding in step '${f.ctx.title}' lists parameters in a non-standard order: found '${f.wrong}', the ` +
|
|
1991
|
+
`corpus-majority convention lists object= first ('${f.correct}'). (testItemId=${f.ctx.tid})`, offenders.length);
|
|
1992
|
+
}
|
|
1993
|
+
// UI-CONN-LITERAL-001 — UI step types whose uiConnectionName must be a literal, not a variable.
|
|
1994
|
+
const UI_CONN_LITERAL_APIS = new Set([
|
|
1995
|
+
'com.provar.plugins.forcedotcom.core.ui.UiWithScreen',
|
|
1996
|
+
'com.provar.plugins.forcedotcom.core.ui.UiDoAction',
|
|
1997
|
+
'com.provar.plugins.forcedotcom.core.ui.UiAssert',
|
|
1998
|
+
'com.provar.plugins.forcedotcom.core.ui.UiWithRow',
|
|
1999
|
+
]);
|
|
2000
|
+
/** UI-CONN-LITERAL-001 — a UI step's `uiConnectionName` uses a `class="variable"` value instead of a literal. */
|
|
2001
|
+
function validateUiConnectionNameLiteral(tc, rule) {
|
|
2002
|
+
for (const call of getAllApiCalls(tc)) {
|
|
2003
|
+
if (!UI_CONN_LITERAL_APIS.has(call['@_apiId']))
|
|
2004
|
+
continue;
|
|
2005
|
+
const v = directArgValueElement(call, 'uiConnectionName');
|
|
2006
|
+
if (!v || v['@_class'] !== 'variable')
|
|
2007
|
+
continue;
|
|
2008
|
+
const ctx = stepContext(call);
|
|
2009
|
+
return makeViolation(rule, `UI step '${ctx.title}' uses a variable reference for uiConnectionName; it must be a literal string ` +
|
|
2010
|
+
`(testItemId=${ctx.tid})`);
|
|
2011
|
+
}
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
982
2014
|
const VALIDATOR_REGISTRY = {
|
|
983
2015
|
unknownApiId: validateUnknownApiId,
|
|
984
2016
|
validIdentifier: validateValidIdentifier,
|
|
@@ -991,6 +2023,33 @@ const VALIDATOR_REGISTRY = {
|
|
|
991
2023
|
detectDuplicatesLiterals: validateDetectDuplicatesLiterals,
|
|
992
2024
|
uniqueResultNames: validateUniqueResultNames,
|
|
993
2025
|
uiWithScreenTarget: validateUiWithScreenTarget,
|
|
2026
|
+
mustContainArgument: validateMustContainArgument,
|
|
2027
|
+
// Tier 2 — render / load-blocking check types (ports of the QH load-blocking validators)
|
|
2028
|
+
valueClassCasing: validateValueClassCasing,
|
|
2029
|
+
booleanCasing: validateBooleanCasing,
|
|
2030
|
+
invalidValueClass: validateInvalidValueClass,
|
|
2031
|
+
dateValueClassFormat: validateDateValueClassFormat,
|
|
2032
|
+
apexConnectReuseConnection: validateApexConnectReuseConnection,
|
|
2033
|
+
apexConnectValidArguments: validateApexConnectValidArguments,
|
|
2034
|
+
apexConnectConnectionIdValueClass: validateApexConnectConnectionIdValueClass,
|
|
2035
|
+
setValuesInvalidElements: validateSetValuesInvalidElements,
|
|
2036
|
+
// Tier 4 — back-end-only rules (single-violation ports)
|
|
2037
|
+
dbConnectResultNameMismatch: validateDbConnectResultNameMismatch,
|
|
2038
|
+
setValuesFuncCallString: validateSetValuesFuncCallString,
|
|
2039
|
+
setValuesZeroIndexString: validateSetValuesZeroIndexString,
|
|
2040
|
+
assertValuesStringExpr: validateAssertValuesStringExpr,
|
|
2041
|
+
uiLocatorButtonCasing: validateUiLocatorButtonCasing,
|
|
2042
|
+
uiLocatorRecordTypeField: validateUiLocatorRecordTypeField,
|
|
2043
|
+
// Tier 5 — structural / load-affecting check types
|
|
2044
|
+
validFuncCallId: validateValidFuncCallId,
|
|
2045
|
+
rootAttributes: validateRootAttributes,
|
|
2046
|
+
setValuesStructure: validateSetValuesStructure,
|
|
2047
|
+
namedValueName: validateNamedValueName,
|
|
2048
|
+
namedValueValue: validateNamedValueValue,
|
|
2049
|
+
uiAssertHallucinatedGeneratedParameters: validateUiAssertHallucinatedGeneratedParameters,
|
|
2050
|
+
uiAssertMissingArguments: validateUiAssertMissingArguments,
|
|
2051
|
+
bindingParameterOrder: validateBindingParameterOrder,
|
|
2052
|
+
uiConnectionNameLiteral: validateUiConnectionNameLiteral,
|
|
994
2053
|
// 'regex' is dispatched separately (needs metadata)
|
|
995
2054
|
// 'uiActionNestingStructure' is dispatched separately (emits one violation per offending step)
|
|
996
2055
|
};
|
|
@@ -998,6 +2057,8 @@ const MULTI_VALIDATOR_REGISTRY = {
|
|
|
998
2057
|
uiActionNestingStructure: validateUiActionNestingStructure,
|
|
999
2058
|
nitroxConnectInvalidArgs: validateNitroxConnectInvalidArgs,
|
|
1000
2059
|
nitroxVariantArgRequired: validateNitroxVariantArgRequired,
|
|
2060
|
+
// Tier 4 — emits one violation per offending value (back-end returns a list)
|
|
2061
|
+
varStringLiteral: validateVarStringLiteral,
|
|
1001
2062
|
};
|
|
1002
2063
|
// ── XML parser (shared settings) ─────────────────────────────────────────────
|
|
1003
2064
|
const XML_PARSER = new XMLParser({
|