@validate.qa/runner 1.0.66 → 1.0.68
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/dist/cli.js +499 -122
- package/dist/cli.mjs +499 -122
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -983,7 +983,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
983
983
|
const value = nonEmpty3(attrs.value);
|
|
984
984
|
const label = name ?? labelAttr ?? value;
|
|
985
985
|
const accessibilityId = name;
|
|
986
|
-
const text =
|
|
986
|
+
const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
|
|
987
987
|
const bounds = parseIOSBounds(attrs);
|
|
988
988
|
const enabled = attrBoolDefaultTrue(attrs.enabled);
|
|
989
989
|
const visible = attrBoolDefaultTrue(attrs.visible);
|
|
@@ -1130,6 +1130,42 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1130
1130
|
}
|
|
1131
1131
|
}
|
|
1132
1132
|
}
|
|
1133
|
+
function annotateIdentityUniqueness(elements) {
|
|
1134
|
+
const a11yCounts = /* @__PURE__ */ new Map();
|
|
1135
|
+
const ridCounts = /* @__PURE__ */ new Map();
|
|
1136
|
+
const textCounts = /* @__PURE__ */ new Map();
|
|
1137
|
+
const bump = (map2, value) => {
|
|
1138
|
+
const key = value?.trim();
|
|
1139
|
+
if (!key)
|
|
1140
|
+
return;
|
|
1141
|
+
map2.set(key, (map2.get(key) ?? 0) + 1);
|
|
1142
|
+
};
|
|
1143
|
+
for (const element of elements) {
|
|
1144
|
+
bump(a11yCounts, element.accessibilityId);
|
|
1145
|
+
bump(ridCounts, element.resourceId);
|
|
1146
|
+
const identityValues = new Set([element.text, element.accessibilityId].map((value) => value?.trim()).filter((value) => Boolean(value)));
|
|
1147
|
+
for (const value of identityValues)
|
|
1148
|
+
bump(textCounts, value);
|
|
1149
|
+
}
|
|
1150
|
+
const isUnique = (map2, value) => {
|
|
1151
|
+
const key = value?.trim();
|
|
1152
|
+
if (!key)
|
|
1153
|
+
return void 0;
|
|
1154
|
+
return (map2.get(key) ?? 0) <= 1;
|
|
1155
|
+
};
|
|
1156
|
+
for (const element of elements) {
|
|
1157
|
+
const accessibilityId = isUnique(a11yCounts, element.accessibilityId);
|
|
1158
|
+
const resourceId = isUnique(ridCounts, element.resourceId);
|
|
1159
|
+
const text = isUnique(textCounts, element.text);
|
|
1160
|
+
if (accessibilityId === void 0 && resourceId === void 0 && text === void 0)
|
|
1161
|
+
continue;
|
|
1162
|
+
element.identityUnique = {
|
|
1163
|
+
...accessibilityId !== void 0 ? { accessibilityId } : {},
|
|
1164
|
+
...resourceId !== void 0 ? { resourceId } : {},
|
|
1165
|
+
...text !== void 0 ? { text } : {}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1133
1169
|
function flatten(root, platform3) {
|
|
1134
1170
|
const out = [];
|
|
1135
1171
|
const stack = [];
|
|
@@ -1175,6 +1211,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1175
1211
|
}
|
|
1176
1212
|
}
|
|
1177
1213
|
markRepeatedBottomBarNavigation(out);
|
|
1214
|
+
annotateIdentityUniqueness(out);
|
|
1178
1215
|
return out;
|
|
1179
1216
|
}
|
|
1180
1217
|
function fnv1a(input) {
|
|
@@ -1256,6 +1293,13 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1256
1293
|
function isEditableElement(el) {
|
|
1257
1294
|
return EDITABLE_CLASS_RE.test(el.role.trim());
|
|
1258
1295
|
}
|
|
1296
|
+
var STATE_VALUE_CLASS_RE = /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/;
|
|
1297
|
+
function isStateValueRole(role) {
|
|
1298
|
+
return STATE_VALUE_CLASS_RE.test(role.trim());
|
|
1299
|
+
}
|
|
1300
|
+
function isStateValueElement(el) {
|
|
1301
|
+
return isStateValueRole(el.role);
|
|
1302
|
+
}
|
|
1259
1303
|
function xpathAttrLiteral(value) {
|
|
1260
1304
|
if (!value.includes("'"))
|
|
1261
1305
|
return `'${value}'`;
|
|
@@ -1266,17 +1310,18 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1266
1310
|
function semanticXPath(el, platform3) {
|
|
1267
1311
|
const cls = nonBlank(el.role) ?? "*";
|
|
1268
1312
|
const a11y = nonBlank(el.accessibilityId);
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1313
|
+
const rid = platform3 === "ANDROID" ? nonBlank(el.resourceId) : void 0;
|
|
1314
|
+
const text = isEditableElement(el) || isStateValueElement(el) ? void 0 : nonBlank(el.text);
|
|
1315
|
+
const a11yXpath = a11y ? platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]` : void 0;
|
|
1316
|
+
const ridXpath = rid ? `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]` : void 0;
|
|
1317
|
+
const textXpath = text ? platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]` : void 0;
|
|
1318
|
+
const candidates = [
|
|
1319
|
+
{ xpath: a11yXpath, unique: el.identityUnique?.accessibilityId },
|
|
1320
|
+
{ xpath: ridXpath, unique: el.identityUnique?.resourceId },
|
|
1321
|
+
{ xpath: textXpath, unique: el.identityUnique?.text }
|
|
1322
|
+
];
|
|
1323
|
+
const uniqueFirst = candidates.find((c) => c.xpath && c.unique !== false);
|
|
1324
|
+
return (uniqueFirst ?? candidates.find((c) => c.xpath))?.xpath;
|
|
1280
1325
|
}
|
|
1281
1326
|
function nonBlank(value) {
|
|
1282
1327
|
if (typeof value !== "string")
|
|
@@ -1285,30 +1330,40 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1285
1330
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
1286
1331
|
}
|
|
1287
1332
|
function buildLocatorBundle(el, platform3) {
|
|
1333
|
+
const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
|
|
1288
1334
|
const bundle = {
|
|
1289
1335
|
// Attribute-anchored xpath when the element has identity; the absolute
|
|
1290
1336
|
// indexed path only for identity-less elements (nothing else can find them).
|
|
1291
1337
|
xpath: semanticXPath(el, platform3) ?? el.xpath,
|
|
1292
1338
|
className: el.role
|
|
1293
1339
|
};
|
|
1294
|
-
|
|
1340
|
+
const a11yUnique = el.identityUnique?.accessibilityId !== false;
|
|
1341
|
+
const ridUnique = el.identityUnique?.resourceId !== false;
|
|
1342
|
+
const textUnique = el.identityUnique?.text !== false;
|
|
1343
|
+
const hasUniqueAlternativeToA11y = Boolean(el.resourceId && platform3 === "ANDROID" && ridUnique || durableText && textUnique);
|
|
1344
|
+
const hasUniqueAlternativeToRid = Boolean(el.accessibilityId && a11yUnique || durableText && textUnique);
|
|
1345
|
+
const includeA11y = Boolean(el.accessibilityId && (a11yUnique || !hasUniqueAlternativeToA11y));
|
|
1346
|
+
const includeRid = Boolean(platform3 === "ANDROID" && el.resourceId && (ridUnique || !hasUniqueAlternativeToRid));
|
|
1347
|
+
if (includeA11y && el.accessibilityId)
|
|
1295
1348
|
bundle.accessibilityId = el.accessibilityId;
|
|
1296
|
-
if (
|
|
1349
|
+
if (includeRid && el.resourceId)
|
|
1297
1350
|
bundle.resourceId = el.resourceId;
|
|
1298
|
-
if (
|
|
1299
|
-
bundle.text =
|
|
1300
|
-
if (platform3 === "ANDROID" && el.accessibilityId)
|
|
1351
|
+
if (durableText)
|
|
1352
|
+
bundle.text = durableText;
|
|
1353
|
+
if (includeA11y && platform3 === "ANDROID" && el.accessibilityId)
|
|
1301
1354
|
bundle.contentDesc = el.accessibilityId;
|
|
1302
1355
|
return bundle;
|
|
1303
1356
|
}
|
|
1304
1357
|
function preferredLocator(el) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1358
|
+
const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
|
|
1359
|
+
const candidates = [
|
|
1360
|
+
{ value: el.accessibilityId, unique: el.identityUnique?.accessibilityId },
|
|
1361
|
+
{ value: el.resourceId, unique: el.identityUnique?.resourceId },
|
|
1362
|
+
{ value: durableText, unique: el.identityUnique?.text }
|
|
1363
|
+
];
|
|
1364
|
+
const uniqueFirst = candidates.find((c) => c.value && c.unique !== false);
|
|
1365
|
+
const chosen = (uniqueFirst ?? candidates.find((c) => c.value))?.value;
|
|
1366
|
+
return chosen ?? el.xpath;
|
|
1312
1367
|
}
|
|
1313
1368
|
function buildScreenSignal(elements, root, platform3) {
|
|
1314
1369
|
const signal = {
|
|
@@ -1971,6 +2026,11 @@ function isEditableMobileClassName(className) {
|
|
|
1971
2026
|
return false;
|
|
1972
2027
|
return /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/.test(className.trim());
|
|
1973
2028
|
}
|
|
2029
|
+
function isStateValueMobileClassName(className) {
|
|
2030
|
+
if (!className)
|
|
2031
|
+
return false;
|
|
2032
|
+
return /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/.test(className.trim());
|
|
2033
|
+
}
|
|
1974
2034
|
function mobileCredentialPlaceholder(key) {
|
|
1975
2035
|
return `{{${key}}}`;
|
|
1976
2036
|
}
|
|
@@ -2100,9 +2160,10 @@ function buildStepTarget(metadata, selector) {
|
|
|
2100
2160
|
locatorBundle.resourceId,
|
|
2101
2161
|
// For editable fields the text attribute IS the currently-typed value — a
|
|
2102
2162
|
// self-invalidating locator that only matches while the recorded text is
|
|
2103
|
-
// still in the field (and it can leak typed input into locators).
|
|
2104
|
-
//
|
|
2105
|
-
|
|
2163
|
+
// still in the field (and it can leak typed input into locators). State
|
|
2164
|
+
// controls' text is their current state ("1"/"ON") and collides with every
|
|
2165
|
+
// same-state sibling. Static elements keep their text fallback.
|
|
2166
|
+
isEditableMobileClassName(locatorBundle.className) || isStateValueMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
|
|
2106
2167
|
locatorBundle.contentDesc,
|
|
2107
2168
|
locatorBundle.xpath,
|
|
2108
2169
|
selector
|
|
@@ -2241,12 +2302,26 @@ function selectorForValue(value) {
|
|
|
2241
2302
|
function isIdentityMobileXpath(value) {
|
|
2242
2303
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
2243
2304
|
}
|
|
2305
|
+
function xpathLiteral(value) {
|
|
2306
|
+
if (!value.includes("'"))
|
|
2307
|
+
return `'${value}'`;
|
|
2308
|
+
if (!value.includes('"'))
|
|
2309
|
+
return `"${value}"`;
|
|
2310
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
2311
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
2312
|
+
}
|
|
2313
|
+
function identityTextXpath(value) {
|
|
2314
|
+
const lit = xpathLiteral(value);
|
|
2315
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
2316
|
+
}
|
|
2244
2317
|
function mobileLocatorBundleHasIdentity(bundle) {
|
|
2245
2318
|
if (!bundle)
|
|
2246
2319
|
return false;
|
|
2247
2320
|
if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
|
|
2248
2321
|
return true;
|
|
2249
|
-
if (nonEmpty(bundle.text)
|
|
2322
|
+
if (nonEmpty(bundle.text) && !isStateValueMobileClassName(bundle.className))
|
|
2323
|
+
return true;
|
|
2324
|
+
if (nonEmpty(bundle.contentDesc))
|
|
2250
2325
|
return true;
|
|
2251
2326
|
const xpath = nonEmpty(bundle.xpath);
|
|
2252
2327
|
return !!xpath && isIdentityMobileXpath(xpath);
|
|
@@ -2269,14 +2344,25 @@ function selectorCandidatesForStep(step) {
|
|
|
2269
2344
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2270
2345
|
if (nonEmpty(bundle.xpath))
|
|
2271
2346
|
candidates.push(bundle.xpath);
|
|
2347
|
+
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2348
|
+
const textIdentity = (isGateStep || !isEditableMobileClassName(bundle.className)) && !isStateValueMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2349
|
+
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2350
|
+
const trimmed = nonEmpty(value);
|
|
2351
|
+
if (trimmed)
|
|
2352
|
+
candidates.push(identityTextXpath(trimmed));
|
|
2353
|
+
}
|
|
2272
2354
|
}
|
|
2273
2355
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2356
|
+
const bundleContributedIdentity = candidates.some((value) => !(value.startsWith("/") || value.startsWith("(//")) || isIdentityMobileXpath(value));
|
|
2274
2357
|
const pushValueCandidates = (value) => {
|
|
2275
2358
|
candidates.push(selectorForValue(value));
|
|
2276
2359
|
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2277
2360
|
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2278
2361
|
candidates.push(`id=${value}`);
|
|
2279
2362
|
}
|
|
2363
|
+
if (!bundleContributedIdentity && !/^(~|id=|css=|\/|\()/.test(value)) {
|
|
2364
|
+
candidates.push(identityTextXpath(value));
|
|
2365
|
+
}
|
|
2280
2366
|
};
|
|
2281
2367
|
if (nonEmpty(target.primary))
|
|
2282
2368
|
pushValueCandidates(target.primary);
|
|
@@ -2715,7 +2801,7 @@ function uniq(values) {
|
|
|
2715
2801
|
}
|
|
2716
2802
|
return result;
|
|
2717
2803
|
}
|
|
2718
|
-
function
|
|
2804
|
+
function xpathLiteral2(value) {
|
|
2719
2805
|
if (!value.includes("'"))
|
|
2720
2806
|
return `'${value}'`;
|
|
2721
2807
|
if (!value.includes('"'))
|
|
@@ -2723,7 +2809,7 @@ function xpathLiteral(value) {
|
|
|
2723
2809
|
return `concat(${value.split("'").map((part) => `'${part}'`).join(`, "'", `)})`;
|
|
2724
2810
|
}
|
|
2725
2811
|
function mobileTextXPath(value, platform3) {
|
|
2726
|
-
const literal =
|
|
2812
|
+
const literal = xpathLiteral2(value);
|
|
2727
2813
|
if (platform3 === "IOS") {
|
|
2728
2814
|
return `//*[@name=${literal} or @label=${literal} or @value=${literal}]`;
|
|
2729
2815
|
}
|
|
@@ -2971,7 +3057,8 @@ function isWebViewStep(step) {
|
|
|
2971
3057
|
function hasCrossPlatformIdentity(bundle) {
|
|
2972
3058
|
if (!bundle)
|
|
2973
3059
|
return false;
|
|
2974
|
-
|
|
3060
|
+
const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
|
|
3061
|
+
return Boolean(durableText?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
2975
3062
|
}
|
|
2976
3063
|
function stepIsDivergentForPlatform(step, platform3) {
|
|
2977
3064
|
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
@@ -3053,7 +3140,8 @@ function summarizeMobileReadiness(steps, authoringPlatform) {
|
|
|
3053
3140
|
function hasStableNativeIdentity(bundle) {
|
|
3054
3141
|
if (!bundle)
|
|
3055
3142
|
return false;
|
|
3056
|
-
|
|
3143
|
+
const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
|
|
3144
|
+
return Boolean(bundle.accessibilityId?.trim() || bundle.resourceId?.trim() || durableText?.trim() || bundle.contentDesc?.trim() || bundle.xpath?.trim() && XPATH_ATTRIBUTE_EQUALITY_RE.test(bundle.xpath) && !/(?:contains|starts-with)\s*\(/i.test(bundle.xpath));
|
|
3057
3145
|
}
|
|
3058
3146
|
function isPositionalXpath(xpath) {
|
|
3059
3147
|
const value = xpath?.trim();
|
|
@@ -3133,6 +3221,7 @@ var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS, XPATH_ATTRIBUTE_EQUALITY_RE;
|
|
|
3133
3221
|
var init_mobile_cross_platform = __esm({
|
|
3134
3222
|
"../shared/dist/mobile-cross-platform.js"() {
|
|
3135
3223
|
"use strict";
|
|
3224
|
+
init_mobile_test_generation();
|
|
3136
3225
|
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
3137
3226
|
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
3138
3227
|
"tap",
|
|
@@ -3871,6 +3960,43 @@ function accumulateStatements(lines) {
|
|
|
3871
3960
|
}
|
|
3872
3961
|
return out;
|
|
3873
3962
|
}
|
|
3963
|
+
function classifyMobileAssertValue(value, context = {}) {
|
|
3964
|
+
const text = value.trim();
|
|
3965
|
+
if (!text)
|
|
3966
|
+
return null;
|
|
3967
|
+
if (/^\{\{[A-Z0-9_]+\}\}$/.test(text))
|
|
3968
|
+
return null;
|
|
3969
|
+
const t1 = matchFirst(TIER1_PATTERNS, text);
|
|
3970
|
+
if (t1)
|
|
3971
|
+
return { tier: 1, reason: t1[1] };
|
|
3972
|
+
const authIdentityValues = [];
|
|
3973
|
+
if (context.authenticatedUserEmail)
|
|
3974
|
+
authIdentityValues.push(context.authenticatedUserEmail);
|
|
3975
|
+
if (context.authenticatedUserName && context.authenticatedUserName.length >= 3) {
|
|
3976
|
+
authIdentityValues.push(context.authenticatedUserName);
|
|
3977
|
+
}
|
|
3978
|
+
for (const auth of authIdentityValues) {
|
|
3979
|
+
if (auth.length >= 3 && text.toLowerCase().includes(auth.toLowerCase())) {
|
|
3980
|
+
return { tier: 2, reason: "asserts the authenticated user identity \u2014 fails under any other test account" };
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
const knownValues = [
|
|
3984
|
+
...context.knownUserInputs ?? [],
|
|
3985
|
+
...context.scenarioCreatedValues ?? []
|
|
3986
|
+
].filter((v) => {
|
|
3987
|
+
for (const auth of authIdentityValues) {
|
|
3988
|
+
if (auth.length >= 3 && v.toLowerCase().includes(auth.toLowerCase()))
|
|
3989
|
+
return false;
|
|
3990
|
+
}
|
|
3991
|
+
return true;
|
|
3992
|
+
});
|
|
3993
|
+
if (containsKnownValue(text, knownValues))
|
|
3994
|
+
return null;
|
|
3995
|
+
const t2 = matchFirst(TIER2_PATTERNS, text);
|
|
3996
|
+
if (t2)
|
|
3997
|
+
return { tier: 2, reason: t2[1] };
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
3874
4000
|
function parenDepthDelta(text) {
|
|
3875
4001
|
let depth = 0;
|
|
3876
4002
|
let quote = null;
|
|
@@ -6044,6 +6170,7 @@ __export(dist_exports, {
|
|
|
6044
6170
|
classifyAssertionBlock: () => classifyAssertionBlock,
|
|
6045
6171
|
classifyAssertionLine: () => classifyAssertionLine,
|
|
6046
6172
|
classifyErrorMessage: () => classifyErrorMessage,
|
|
6173
|
+
classifyMobileAssertValue: () => classifyMobileAssertValue,
|
|
6047
6174
|
classifyTestHealth: () => classifyTestHealth,
|
|
6048
6175
|
coldSessionConfigMessage: () => coldSessionConfigMessage,
|
|
6049
6176
|
computeMobileSwipePointers: () => computeMobileSwipePointers,
|
|
@@ -6097,6 +6224,7 @@ __export(dist_exports, {
|
|
|
6097
6224
|
isEditableMobileClassName: () => isEditableMobileClassName,
|
|
6098
6225
|
isEmailFromIdentity: () => isEmailFromIdentity,
|
|
6099
6226
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
6227
|
+
isIdentityMobileXpath: () => isIdentityMobileXpath,
|
|
6100
6228
|
isMobilePlatform: () => isMobilePlatform,
|
|
6101
6229
|
isPricingPlan: () => isPricingPlan,
|
|
6102
6230
|
isRunnableStep: () => isRunnableStep,
|
|
@@ -6104,6 +6232,7 @@ __export(dist_exports, {
|
|
|
6104
6232
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
6105
6233
|
isSensitiveQueryKey: () => isSensitiveQueryKey,
|
|
6106
6234
|
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
6235
|
+
isStateValueMobileClassName: () => isStateValueMobileClassName,
|
|
6107
6236
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
6108
6237
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6109
6238
|
isTrialExpired: () => isTrialExpired,
|
|
@@ -7011,6 +7140,7 @@ var require_mobile_observation = __commonJS({
|
|
|
7011
7140
|
exports.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
|
|
7012
7141
|
exports.extractNativeMobileLandmarks = extractNativeMobileLandmarks;
|
|
7013
7142
|
exports.safeMobileElementLabel = safeMobileElementLabel;
|
|
7143
|
+
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
7014
7144
|
var DEFAULT_MAX_ELEMENTS = 60;
|
|
7015
7145
|
var MAX_LABEL_LENGTH = 80;
|
|
7016
7146
|
var EVIDENCE_LABEL_LIMIT = 40;
|
|
@@ -7101,7 +7231,8 @@ var require_mobile_observation = __commonJS({
|
|
|
7101
7231
|
const locators = [];
|
|
7102
7232
|
if (stableId)
|
|
7103
7233
|
locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
|
|
7104
|
-
const
|
|
7234
|
+
const platform3 = element.role.startsWith("XCUIElementType") ? "IOS" : "ANDROID";
|
|
7235
|
+
const xpath = (0, mobile_source_parser_js_1.buildLocatorBundle)(element, platform3).xpath?.trim();
|
|
7105
7236
|
if (xpath)
|
|
7106
7237
|
locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
|
|
7107
7238
|
const collected = {
|
|
@@ -78350,6 +78481,10 @@ var require_discover_ai_planner = __commonJS({
|
|
|
78350
78481
|
if (context.appBrief) {
|
|
78351
78482
|
sections.push(`## Application Brief
|
|
78352
78483
|
${context.appBrief.slice(0, 500)}`);
|
|
78484
|
+
}
|
|
78485
|
+
if (context.knowledgeContext && context.knowledgeContext.trim().length > 0) {
|
|
78486
|
+
sections.push(`## Known Facts
|
|
78487
|
+
${context.knowledgeContext.slice(0, 4e3)}`);
|
|
78353
78488
|
}
|
|
78354
78489
|
sections.push(`## Base URL
|
|
78355
78490
|
${compressed.baseUrl}`);
|
|
@@ -80888,6 +81023,45 @@ ${constraints.join("\n")}`);
|
|
|
80888
81023
|
}
|
|
80889
81024
|
});
|
|
80890
81025
|
|
|
81026
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
81027
|
+
var require_discovery_lint_flags = __commonJS({
|
|
81028
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
81029
|
+
"use strict";
|
|
81030
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81031
|
+
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
81032
|
+
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
81033
|
+
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
81034
|
+
exports.mobileSelectorQualityLintMode = mobileSelectorQualityLintMode;
|
|
81035
|
+
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
81036
|
+
function strictOn(value) {
|
|
81037
|
+
return value === "true" || value === "1";
|
|
81038
|
+
}
|
|
81039
|
+
function structuralLocatorLintMode() {
|
|
81040
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
81041
|
+
if (v === "warn")
|
|
81042
|
+
return "warn";
|
|
81043
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
81044
|
+
return "enforce";
|
|
81045
|
+
return "off";
|
|
81046
|
+
}
|
|
81047
|
+
function isPwMethodPreflightEnabled() {
|
|
81048
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
81049
|
+
}
|
|
81050
|
+
function isVqaLoginDedupeEnabled() {
|
|
81051
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
81052
|
+
}
|
|
81053
|
+
function mobileSelectorQualityLintMode() {
|
|
81054
|
+
const v = (process.env.MOBILE_SELECTOR_QUALITY_LINT || "").trim().toLowerCase();
|
|
81055
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
81056
|
+
return "enforce";
|
|
81057
|
+
return "off";
|
|
81058
|
+
}
|
|
81059
|
+
function isTestgenEmailVerifyEnabled() {
|
|
81060
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
81061
|
+
}
|
|
81062
|
+
}
|
|
81063
|
+
});
|
|
81064
|
+
|
|
80891
81065
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
80892
81066
|
var require_mobile_generator = __commonJS({
|
|
80893
81067
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
@@ -80907,6 +81081,7 @@ var require_mobile_generator = __commonJS({
|
|
|
80907
81081
|
var ai_queue_js_1 = require_ai_queue();
|
|
80908
81082
|
var ai_retry_js_1 = require_ai_retry();
|
|
80909
81083
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81084
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80910
81085
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
80911
81086
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
80912
81087
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81076,7 +81251,15 @@ var require_mobile_generator = __commonJS({
|
|
|
81076
81251
|
properties: {
|
|
81077
81252
|
name: { type: "string", description: 'Descriptive test name (e.g. "Login - Happy Path").' },
|
|
81078
81253
|
description: { type: "string", description: "One-line description of what the test proves." },
|
|
81079
|
-
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." }
|
|
81254
|
+
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." },
|
|
81255
|
+
suspectedBug: {
|
|
81256
|
+
type: "boolean",
|
|
81257
|
+
description: "True when the app itself misbehaved (crash, error screen, broken flow) and the scenario could not behave as specified. Requires bugDescription and at least one honored assert proving what you observed."
|
|
81258
|
+
},
|
|
81259
|
+
bugDescription: {
|
|
81260
|
+
type: "string",
|
|
81261
|
+
description: "What is broken in the APP (not the test) \u2014 concrete screen + observed vs expected."
|
|
81262
|
+
}
|
|
81080
81263
|
},
|
|
81081
81264
|
required: ["name", "description", "verified"]
|
|
81082
81265
|
}
|
|
@@ -81322,6 +81505,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81322
81505
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
81323
81506
|
- Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
|
|
81324
81507
|
- Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
|
|
81508
|
+
- STATE OWNERSHIP for mutation scenarios (edit / rename / archive / remove): CREATE the record first in this same recording, then act on the row YOU created \u2014 never mutate pre-existing data (it may not exist on a clean install, so the replay fails forever, and another test may depend on it). If the app offers no way to create one, do NOT act on someone else's data: assert what you can safely prove and finish with verified:false, explaining why in the description.
|
|
81509
|
+
- If the APP itself misbehaves (crash, error screen, a flow that cannot proceed as specified), do not force a green path: assert the failure you observed (e.g. the error text), then call finish_generate with suspectedBug:true and a concrete bugDescription of what is broken in the app.
|
|
81325
81510
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
81326
81511
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
81327
81512
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
@@ -81427,6 +81612,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
81427
81612
|
let assertCount = 0;
|
|
81428
81613
|
let finish = null;
|
|
81429
81614
|
let finishIntentRejections = 0;
|
|
81615
|
+
let finishStrengthRejections = 0;
|
|
81430
81616
|
let emptyTurnsAfterFinishReject = 0;
|
|
81431
81617
|
let latest = null;
|
|
81432
81618
|
const getLatest = () => latest;
|
|
@@ -81919,31 +82105,49 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81919
82105
|
const claimedVerified = Boolean(toolArgs.verified);
|
|
81920
82106
|
const intentReason = scenarioIntentMismatchReason(scenario, actions);
|
|
81921
82107
|
const verifiedWithoutAssert = claimedVerified && assertCount === 0;
|
|
81922
|
-
const
|
|
81923
|
-
|
|
81924
|
-
|
|
82108
|
+
const intentStyleReject = finishIntentRejections < MAX_FINISH_INTENT_REJECTIONS && iteration < maxIterations && (intentReason !== null || verifiedWithoutAssert);
|
|
82109
|
+
let strengthRejectReason = null;
|
|
82110
|
+
if (!intentStyleReject && claimedVerified && assertCount > 0 && intentReason === null && finishStrengthRejections < 1 && iteration < maxIterations) {
|
|
82111
|
+
const verdict = evaluateMobileAssertionStrength(actions);
|
|
82112
|
+
if (verdict.severity === "block") {
|
|
82113
|
+
strengthRejectReason = verdict.reason;
|
|
82114
|
+
}
|
|
82115
|
+
}
|
|
82116
|
+
if (intentStyleReject || strengthRejectReason !== null) {
|
|
82117
|
+
if (intentStyleReject)
|
|
82118
|
+
finishIntentRejections += 1;
|
|
82119
|
+
if (strengthRejectReason !== null)
|
|
82120
|
+
finishStrengthRejections += 1;
|
|
81925
82121
|
const reasons = [];
|
|
81926
82122
|
if (intentReason)
|
|
81927
82123
|
reasons.push(intentReason);
|
|
81928
82124
|
if (verifiedWithoutAssert) {
|
|
81929
82125
|
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
81930
82126
|
}
|
|
82127
|
+
if (strengthRejectReason !== null) {
|
|
82128
|
+
reasons.push(`assertion strength: ${strengthRejectReason} \u2014 your only live proof re-checks a control you already used. Assert the scenario OUTCOME instead: an element on the resulting screen, the created/updated row, or the success/error message.`);
|
|
82129
|
+
}
|
|
81931
82130
|
const reasonText = reasons.join("; ");
|
|
81932
|
-
log2(` finish_generate REJECTED (${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}): ${reasonText}`);
|
|
82131
|
+
log2(` finish_generate REJECTED (intent ${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}, strength ${finishStrengthRejections}/1): ${reasonText}`);
|
|
81933
82132
|
pushResult({
|
|
81934
82133
|
ok: false,
|
|
81935
82134
|
error: `Cannot finish yet \u2014 the recorded steps do not complete this scenario: ${reasonText}. Continue driving the app (fill fields, commit forms, navigate, assert the REAL outcome), then call finish_generate again. Do NOT re-open menus or thrash Back.`,
|
|
81936
82135
|
intentMismatch: intentReason,
|
|
81937
82136
|
verifiedWithoutAssert,
|
|
81938
|
-
|
|
81939
|
-
|
|
82137
|
+
...strengthRejectReason !== null ? { strengthBlock: strengthRejectReason } : {},
|
|
82138
|
+
rejection: finishIntentRejections + finishStrengthRejections,
|
|
82139
|
+
maxRejections: MAX_FINISH_INTENT_REJECTIONS + 1
|
|
81940
82140
|
});
|
|
81941
82141
|
continue;
|
|
81942
82142
|
}
|
|
81943
82143
|
finish = {
|
|
81944
82144
|
name: redactCredentialValues(nonEmpty3(String(toolArgs.name ?? "")) ?? scenario.name, redactionCredentials),
|
|
81945
82145
|
description: redactCredentialValues(nonEmpty3(String(toolArgs.description ?? "")) ?? scenario.goal, redactionCredentials),
|
|
81946
|
-
verified: claimedVerified
|
|
82146
|
+
verified: claimedVerified,
|
|
82147
|
+
...toolArgs.suspectedBug === true ? {
|
|
82148
|
+
suspectedBug: true,
|
|
82149
|
+
bugDescription: redactCredentialValues(nonEmpty3(String(toolArgs.bugDescription ?? "")) ?? "", redactionCredentials)
|
|
82150
|
+
} : {}
|
|
81947
82151
|
};
|
|
81948
82152
|
log2(` finish_generate: "${finish.name}" (claimed verified: ${finish.verified}, asserts: ${assertCount})`);
|
|
81949
82153
|
pushResult({ acknowledged: true });
|
|
@@ -82007,6 +82211,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82007
82211
|
ok: false,
|
|
82008
82212
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
82009
82213
|
});
|
|
82214
|
+
const recordedTypeValues = () => recordedActions.filter((action) => action.type === "TYPE" && typeof action.value === "string" && action.value.length > 0).map((action) => action.value);
|
|
82010
82215
|
const actionPayload = (result, absorbed) => {
|
|
82011
82216
|
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
82012
82217
|
return {
|
|
@@ -82407,7 +82612,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82407
82612
|
latest,
|
|
82408
82613
|
screenName,
|
|
82409
82614
|
platform: platform3,
|
|
82410
|
-
recordAction
|
|
82615
|
+
recordAction,
|
|
82616
|
+
typedValues: recordedTypeValues
|
|
82411
82617
|
});
|
|
82412
82618
|
if (!recorded.ok)
|
|
82413
82619
|
return recorded;
|
|
@@ -82476,6 +82682,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82476
82682
|
error: `Live text assertion failed: expected ${JSON.stringify(expected)} was not present on ref ${element.ref}. Re-snapshot after the outcome appears and assert the text that is actually visible.`
|
|
82477
82683
|
};
|
|
82478
82684
|
}
|
|
82685
|
+
const volatileExpected = (0, shared_1.classifyMobileAssertValue)(expected, {
|
|
82686
|
+
knownUserInputs: recordedTypeValues()
|
|
82687
|
+
});
|
|
82688
|
+
if (volatileExpected) {
|
|
82689
|
+
return {
|
|
82690
|
+
ok: false,
|
|
82691
|
+
error: `Expected text ${JSON.stringify(expected)} is ${volatileExpected.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileExpected.reason}) \u2014 it changes between runs, so the test would fail forever on a healthy app. Assert durable post-state instead: an element unique to the destination screen, a stable label, or text your flow itself entered.`
|
|
82692
|
+
};
|
|
82693
|
+
}
|
|
82479
82694
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
82480
82695
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82481
82696
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
@@ -82531,6 +82746,17 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82531
82746
|
if (!accessibilityId && !text) {
|
|
82532
82747
|
return { ok: false, error: "mobile_assert_visible needs a ref (preferred), accessibilityId, or text." };
|
|
82533
82748
|
}
|
|
82749
|
+
if (!accessibilityId && text) {
|
|
82750
|
+
const volatileIdentity = (0, shared_1.classifyMobileAssertValue)(text, {
|
|
82751
|
+
knownUserInputs: ctx.typedValues?.() ?? []
|
|
82752
|
+
});
|
|
82753
|
+
if (volatileIdentity) {
|
|
82754
|
+
return {
|
|
82755
|
+
ok: false,
|
|
82756
|
+
error: `${JSON.stringify(text)} is ${volatileIdentity.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileIdentity.reason}) \u2014 a text identity must be durable to resolve at replay. Assert a stable element of the destination screen instead (prefer a ref from mobile_snapshot).`
|
|
82757
|
+
};
|
|
82758
|
+
}
|
|
82759
|
+
}
|
|
82534
82760
|
const directTarget = accessibilityId ?? text;
|
|
82535
82761
|
if (directTarget && isStructuralAssertionIdentity(directTarget)) {
|
|
82536
82762
|
return {
|
|
@@ -83166,7 +83392,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83166
83392
|
if (claimedVerified && !verified) {
|
|
83167
83393
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
83168
83394
|
}
|
|
83169
|
-
const
|
|
83395
|
+
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
83396
|
+
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
83397
|
+
const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
|
|
83398
|
+
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
83399
|
+
log2(" suspectedBug declared but NOT honored: " + (process.env.DISCOVERY_BUG_WITNESS_ENABLED !== "true" ? "bug-witness channel disabled (DISCOVERY_BUG_WITNESS_ENABLED)." : bugDescription.length < 12 ? "bugDescription too short." : "no honored assert backs the observation."));
|
|
83400
|
+
}
|
|
83401
|
+
const strengthVerdict = bugWitnessHonored ? {
|
|
83402
|
+
...baseStrengthVerdict,
|
|
83403
|
+
ok: false,
|
|
83404
|
+
severity: "route-bug",
|
|
83405
|
+
reason: `Model declared an app bug during generation: ${bugDescription}`
|
|
83406
|
+
} : baseStrengthVerdict;
|
|
83407
|
+
if (bugWitnessHonored) {
|
|
83408
|
+
log2(` bug-witness honored \u2014 routing as app bug: ${bugDescription}`);
|
|
83409
|
+
}
|
|
83170
83410
|
if (strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug") {
|
|
83171
83411
|
if (verified) {
|
|
83172
83412
|
log2(` downgraded verified -> false: assertion strength ${strengthVerdict.severity} \u2014 ${strengthVerdict.reason}`);
|
|
@@ -83182,7 +83422,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83182
83422
|
verified = false;
|
|
83183
83423
|
}
|
|
83184
83424
|
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
83185
|
-
const
|
|
83425
|
+
const selectorLintEnforced = (0, discovery_lint_flags_js_1.mobileSelectorQualityLintMode)() === "enforce" && selectorQuality.summary.high > 0;
|
|
83426
|
+
if (selectorLintEnforced && verified) {
|
|
83427
|
+
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
83428
|
+
}
|
|
83429
|
+
const reviewTag = verified && !selectorLintEnforced ? "@verified" : "@needs-review";
|
|
83186
83430
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
83187
83431
|
if (strengthVerdict.severity === "downgrade")
|
|
83188
83432
|
tags.push("@smoke");
|
|
@@ -86262,38 +86506,6 @@ var require_run_callback_proxy = __commonJS({
|
|
|
86262
86506
|
}
|
|
86263
86507
|
});
|
|
86264
86508
|
|
|
86265
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
86266
|
-
var require_discovery_lint_flags = __commonJS({
|
|
86267
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
86268
|
-
"use strict";
|
|
86269
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86270
|
-
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
86271
|
-
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
86272
|
-
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
86273
|
-
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
86274
|
-
function strictOn(value) {
|
|
86275
|
-
return value === "true" || value === "1";
|
|
86276
|
-
}
|
|
86277
|
-
function structuralLocatorLintMode() {
|
|
86278
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
86279
|
-
if (v === "warn")
|
|
86280
|
-
return "warn";
|
|
86281
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
86282
|
-
return "enforce";
|
|
86283
|
-
return "off";
|
|
86284
|
-
}
|
|
86285
|
-
function isPwMethodPreflightEnabled() {
|
|
86286
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
86287
|
-
}
|
|
86288
|
-
function isVqaLoginDedupeEnabled() {
|
|
86289
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
86290
|
-
}
|
|
86291
|
-
function isTestgenEmailVerifyEnabled() {
|
|
86292
|
-
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
86293
|
-
}
|
|
86294
|
-
}
|
|
86295
|
-
});
|
|
86296
|
-
|
|
86297
86509
|
// ../runner-core/dist/services/auth-recipe.js
|
|
86298
86510
|
var require_auth_recipe = __commonJS({
|
|
86299
86511
|
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
@@ -92154,6 +92366,15 @@ var require_agentic_discovery_prompts = __commonJS({
|
|
|
92154
92366
|
lines.push(input.appBrief.trim());
|
|
92155
92367
|
lines.push("");
|
|
92156
92368
|
}
|
|
92369
|
+
if (input.knownFactsBlock && input.knownFactsBlock.trim()) {
|
|
92370
|
+
lines.push("## Known facts about this feature (from project documentation)");
|
|
92371
|
+
lines.push("");
|
|
92372
|
+
lines.push("Advisory hints about routes/data/constraints \u2014 verify against the live");
|
|
92373
|
+
lines.push("app; the browser is the source of truth for locators.");
|
|
92374
|
+
lines.push("");
|
|
92375
|
+
lines.push(input.knownFactsBlock.trim());
|
|
92376
|
+
lines.push("");
|
|
92377
|
+
}
|
|
92157
92378
|
if (input.credentials && input.credentials.length > 0) {
|
|
92158
92379
|
lines.push("## Test credentials (throwaway accounts)");
|
|
92159
92380
|
lines.push("");
|
|
@@ -328821,6 +329042,15 @@ var require_agentic_survey_prompts = __commonJS({
|
|
|
328821
329042
|
lines.push(input.appBrief.trim());
|
|
328822
329043
|
lines.push("");
|
|
328823
329044
|
}
|
|
329045
|
+
if (input.knownFactsBlock && input.knownFactsBlock.trim().length > 0) {
|
|
329046
|
+
lines.push("## Known facts about this app (from project documentation)");
|
|
329047
|
+
lines.push("");
|
|
329048
|
+
lines.push("Use these as hints for what features/routes/constraints exist. They are");
|
|
329049
|
+
lines.push("advisory \u2014 verify against the live app; the browser is the source of truth.");
|
|
329050
|
+
lines.push("");
|
|
329051
|
+
lines.push(input.knownFactsBlock.trim());
|
|
329052
|
+
lines.push("");
|
|
329053
|
+
}
|
|
328824
329054
|
if (input.credentials && input.credentials.length > 0) {
|
|
328825
329055
|
lines.push("## Test credentials (throwaway accounts)");
|
|
328826
329056
|
lines.push("");
|
|
@@ -329448,6 +329678,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
329448
329678
|
const briefInput = {
|
|
329449
329679
|
baseUrl: ctx.baseUrl,
|
|
329450
329680
|
appBrief: ctx.appBrief,
|
|
329681
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
329451
329682
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
329452
329683
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
329453
329684
|
focusAreas: ctx.focusAreas,
|
|
@@ -331143,6 +331374,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
331143
331374
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
331144
331375
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
331145
331376
|
appBrief: ctx.appBrief,
|
|
331377
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
331146
331378
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
331147
331379
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
331148
331380
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -332080,6 +332312,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
332080
332312
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
332081
332313
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
332082
332314
|
appBrief: ctx.appBrief,
|
|
332315
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
332083
332316
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
332084
332317
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
332085
332318
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -333171,6 +333404,7 @@ ${preview}`);
|
|
|
333171
333404
|
const briefInput = {
|
|
333172
333405
|
baseUrl: ctx.baseUrl,
|
|
333173
333406
|
appBrief: ctx.appBrief,
|
|
333407
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
333174
333408
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
333175
333409
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
333176
333410
|
focusAreas: ctx.focusAreas,
|
|
@@ -334030,6 +334264,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
334030
334264
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
334031
334265
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
334032
334266
|
appBrief: ctx.appBrief,
|
|
334267
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
334033
334268
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
334034
334269
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
334035
334270
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -335884,7 +336119,8 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
335884
336119
|
// scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
|
|
335885
336120
|
// auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
|
|
335886
336121
|
emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
|
|
335887
|
-
testBudget
|
|
336122
|
+
testBudget,
|
|
336123
|
+
knowledgeContext: ctx.knowledgeContext
|
|
335888
336124
|
}, {
|
|
335889
336125
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
335890
336126
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -335996,6 +336232,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
335996
336232
|
interactionLog: exploreResult.interactionLog,
|
|
335997
336233
|
modelOverride: ctx.generateModel,
|
|
335998
336234
|
appBrief: ctx.appBrief,
|
|
336235
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
335999
336236
|
onAICall,
|
|
336000
336237
|
onProgress,
|
|
336001
336238
|
mcpClient: mcpClient ?? void 0,
|
|
@@ -336475,7 +336712,8 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
336475
336712
|
// configured inbox), so the planner never promises what generation can't do.
|
|
336476
336713
|
// Scoped to AUTH features: a non-auth feature has no signup surface to test.
|
|
336477
336714
|
emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
|
|
336478
|
-
testBudget: featureTestBudget
|
|
336715
|
+
testBudget: featureTestBudget,
|
|
336716
|
+
knowledgeContext: ctx.knowledgeContext
|
|
336479
336717
|
}, {
|
|
336480
336718
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
336481
336719
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -336521,6 +336759,7 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
336521
336759
|
interactionLog: exploreResult.interactionLog,
|
|
336522
336760
|
modelOverride: ctx.generateModel,
|
|
336523
336761
|
appBrief: ctx.appBrief,
|
|
336762
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
336524
336763
|
onAICall,
|
|
336525
336764
|
mcpClient: mcpClient ?? void 0,
|
|
336526
336765
|
// Pooled browser for parallel scenario generation (Fargate opt-in via
|
|
@@ -347734,6 +347973,12 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347734
347973
|
"Use {{TEST_USER_EMAIL}}/{{TEST_USER_PASSWORD}} for credentials. Include",
|
|
347735
347974
|
"assertVisible or assertText unless tagging @needs-review. When an incremental",
|
|
347736
347975
|
"allow-list is present, extract ONLY from those files.",
|
|
347976
|
+
"",
|
|
347977
|
+
"Every tap/type/clear/waitForElement/assertVisible/assertText target MUST",
|
|
347978
|
+
"carry an IDENTITY locator: ~accessibility-id, id=resource-id, visible text,",
|
|
347979
|
+
"or an attribute-EQUALITY xpath (//android.widget.Button[@text='Submit']).",
|
|
347980
|
+
"Positional/index-only xpaths (/hierarchy/\u2026/View[3]) are rejected \u2014 imported",
|
|
347981
|
+
"steps have no recorded coordinates, so such a step can never replay.",
|
|
347737
347982
|
"Write strictly valid JSON."
|
|
347738
347983
|
].join("\n");
|
|
347739
347984
|
}
|
|
@@ -347751,6 +347996,32 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347751
347996
|
function isPlainObject2(value) {
|
|
347752
347997
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
347753
347998
|
}
|
|
347999
|
+
var LOCATOR_REQUIRING_IMPORT_ACTIONS = /* @__PURE__ */ new Set([
|
|
348000
|
+
"tap",
|
|
348001
|
+
"type",
|
|
348002
|
+
"clear",
|
|
348003
|
+
"waitForElement",
|
|
348004
|
+
"assertVisible",
|
|
348005
|
+
"assertText"
|
|
348006
|
+
]);
|
|
348007
|
+
function selectorValueHasIdentity(value) {
|
|
348008
|
+
const trimmed = value.trim();
|
|
348009
|
+
if (!trimmed)
|
|
348010
|
+
return false;
|
|
348011
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("(//"))
|
|
348012
|
+
return (0, shared_1.isIdentityMobileXpath)(trimmed);
|
|
348013
|
+
return true;
|
|
348014
|
+
}
|
|
348015
|
+
function stepLocatorLacksIdentity(step) {
|
|
348016
|
+
if (!LOCATOR_REQUIRING_IMPORT_ACTIONS.has(step.action))
|
|
348017
|
+
return false;
|
|
348018
|
+
const target = step.target;
|
|
348019
|
+
if (!target)
|
|
348020
|
+
return true;
|
|
348021
|
+
if ((0, shared_1.mobileLocatorBundleHasIdentity)(target.locatorBundle))
|
|
348022
|
+
return false;
|
|
348023
|
+
return ![target.primary, ...target.fallbacks ?? []].some((value) => typeof value === "string" && selectorValueHasIdentity(value));
|
|
348024
|
+
}
|
|
347754
348025
|
function isValidStep(value) {
|
|
347755
348026
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
347756
348027
|
return false;
|
|
@@ -347792,12 +348063,7 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347792
348063
|
const o = value;
|
|
347793
348064
|
if (!isNonEmptyString(o.name))
|
|
347794
348065
|
return { ok: false, reason: "name: missing" };
|
|
347795
|
-
|
|
347796
|
-
return { ok: false, reason: "appiumCode: missing" };
|
|
347797
|
-
const code = o.appiumCode;
|
|
347798
|
-
if (!/webdriverio|remote\s*\(|driver\s*\./i.test(code)) {
|
|
347799
|
-
return { ok: false, reason: "appiumCode: not a WebdriverIO/Appium script" };
|
|
347800
|
-
}
|
|
348066
|
+
const code = isNonEmptyString(o.appiumCode) ? o.appiumCode : "";
|
|
347801
348067
|
if (!Array.isArray(o.steps) || o.steps.length === 0) {
|
|
347802
348068
|
return { ok: false, reason: "steps: missing or empty" };
|
|
347803
348069
|
}
|
|
@@ -347807,6 +348073,13 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347807
348073
|
if (!o.steps.every(isValidStep)) {
|
|
347808
348074
|
return { ok: false, reason: "steps: invalid step shape" };
|
|
347809
348075
|
}
|
|
348076
|
+
const identityless = o.steps.find(stepLocatorLacksIdentity);
|
|
348077
|
+
if (identityless) {
|
|
348078
|
+
return {
|
|
348079
|
+
ok: false,
|
|
348080
|
+
reason: `steps: step ${identityless.order} (${identityless.action}) has no identity locator \u2014 a positional/index xpath cannot replay (imported steps have no recorded coordinates). Use ~accessibility-id, id=resource-id, visible text, or an attribute-equality xpath like //android.widget.Button[@text='Submit'].`
|
|
348081
|
+
};
|
|
348082
|
+
}
|
|
347810
348083
|
const steps = o.steps.map((step, order) => ({
|
|
347811
348084
|
...step,
|
|
347812
348085
|
order,
|
|
@@ -351100,7 +351373,7 @@ var require_package4 = __commonJS({
|
|
|
351100
351373
|
"package.json"(exports, module) {
|
|
351101
351374
|
module.exports = {
|
|
351102
351375
|
name: "@validate.qa/runner",
|
|
351103
|
-
version: "1.0.
|
|
351376
|
+
version: "1.0.68",
|
|
351104
351377
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351105
351378
|
bin: {
|
|
351106
351379
|
"validate-runner": "dist/cli.js"
|
|
@@ -351496,6 +351769,9 @@ function sanitizeAppiumLogLine(line) {
|
|
|
351496
351769
|
}
|
|
351497
351770
|
function setAppiumWorkloadActive(active) {
|
|
351498
351771
|
activeWorkloadCount = active ? activeWorkloadCount + 1 : Math.max(0, activeWorkloadCount - 1);
|
|
351772
|
+
if (activeWorkloadCount === 0) {
|
|
351773
|
+
consecutiveHealthFailures = 0;
|
|
351774
|
+
}
|
|
351499
351775
|
}
|
|
351500
351776
|
function shouldRestartAfterHealthFailure(workloadCount, failureCount) {
|
|
351501
351777
|
return workloadCount <= 0 && failureCount >= HEALTH_FAILURES_BEFORE_RESTART;
|
|
@@ -351991,17 +352267,31 @@ function rectMatchesIntendedBounds(rect, bounds) {
|
|
|
351991
352267
|
return rectCenterInBounds || boundsCenterInRect;
|
|
351992
352268
|
}
|
|
351993
352269
|
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
351994
|
-
const
|
|
352270
|
+
const rawBox = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
352271
|
+
const box = rawBox && rawBox.width > 0 && rawBox.height > 0 ? rawBox : null;
|
|
351995
352272
|
let elementId = await getActiveElementId(sessionPath);
|
|
351996
|
-
|
|
352273
|
+
let rejectedElementId = null;
|
|
352274
|
+
let rejectedForMismatch = false;
|
|
352275
|
+
if (elementId && box) {
|
|
351997
352276
|
const rect = await getElementRect(sessionPath, elementId);
|
|
351998
352277
|
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
352278
|
+
rejectedElementId = elementId;
|
|
352279
|
+
rejectedForMismatch = !!rect;
|
|
351999
352280
|
elementId = null;
|
|
352000
352281
|
}
|
|
352001
352282
|
}
|
|
352002
352283
|
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
352003
352284
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
352004
352285
|
elementId = await getActiveElementId(sessionPath);
|
|
352286
|
+
if (elementId && box) {
|
|
352287
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
352288
|
+
const provablyWrong = rect ? !rectMatchesIntendedBounds(rect, box) : rejectedForMismatch && elementId === rejectedElementId;
|
|
352289
|
+
if (provablyWrong) {
|
|
352290
|
+
throw new Error(
|
|
352291
|
+
"Could not focus the intended field: a different element kept keyboard focus after tapping the target bounds. Tap the field directly, then retry."
|
|
352292
|
+
);
|
|
352293
|
+
}
|
|
352294
|
+
}
|
|
352005
352295
|
}
|
|
352006
352296
|
return elementId;
|
|
352007
352297
|
}
|
|
@@ -354554,7 +354844,7 @@ function boundsForRun(step, platform3, isCrossPlatform) {
|
|
|
354554
354844
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
354555
354845
|
return boundsFromStep(step);
|
|
354556
354846
|
}
|
|
354557
|
-
function locatorStrategyForValue(value) {
|
|
354847
|
+
function locatorStrategyForValue(value, platform3) {
|
|
354558
354848
|
const trimmed = value?.trim();
|
|
354559
354849
|
if (!trimmed) {
|
|
354560
354850
|
return [];
|
|
@@ -354570,7 +354860,8 @@ function locatorStrategyForValue(value) {
|
|
|
354570
354860
|
}
|
|
354571
354861
|
return [
|
|
354572
354862
|
{ using: "accessibility id", value: trimmed },
|
|
354573
|
-
{ using: "id", value: trimmed }
|
|
354863
|
+
{ using: "id", value: trimmed },
|
|
354864
|
+
...textIdentityStrategies(trimmed, platform3)
|
|
354574
354865
|
];
|
|
354575
354866
|
}
|
|
354576
354867
|
function buildLocatorStrategies(target, platform3) {
|
|
@@ -354580,12 +354871,19 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354580
354871
|
if (bundle) {
|
|
354581
354872
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354582
354873
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354583
|
-
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354874
|
+
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354875
|
+
for (const value of [
|
|
354876
|
+
isEditableMobileClassName(bundle.className) || isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354877
|
+
bundle.contentDesc
|
|
354878
|
+
]) {
|
|
354879
|
+
const trimmed = value?.trim();
|
|
354880
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354881
|
+
}
|
|
354584
354882
|
}
|
|
354585
354883
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354586
|
-
strategies.push(...locatorStrategyForValue(target.primary));
|
|
354884
|
+
strategies.push(...locatorStrategyForValue(target.primary, platform3));
|
|
354587
354885
|
for (const fallback of target.fallbacks ?? []) {
|
|
354588
|
-
strategies.push(...locatorStrategyForValue(fallback));
|
|
354886
|
+
strategies.push(...locatorStrategyForValue(fallback, platform3));
|
|
354589
354887
|
}
|
|
354590
354888
|
}
|
|
354591
354889
|
return strategies.filter((strategy) => strategy.using !== "xpath" || isIdentityXpath(strategy.value)).filter(
|
|
@@ -354597,22 +354895,41 @@ function dedupeStrategies(strategies) {
|
|
|
354597
354895
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
354598
354896
|
);
|
|
354599
354897
|
}
|
|
354600
|
-
function
|
|
354898
|
+
function xpathLiteral3(value) {
|
|
354601
354899
|
if (!value.includes("'")) return `'${value}'`;
|
|
354602
354900
|
if (!value.includes('"')) return `"${value}"`;
|
|
354603
354901
|
const parts = value.split("'").map((p) => `'${p}'`);
|
|
354604
354902
|
return `concat(${parts.join(`, "'", `)})`;
|
|
354605
354903
|
}
|
|
354606
|
-
function
|
|
354607
|
-
const lit =
|
|
354904
|
+
function identityTextXpath2(value) {
|
|
354905
|
+
const lit = xpathLiteral3(value);
|
|
354608
354906
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354609
354907
|
}
|
|
354908
|
+
function iosPredicateLiteral(value) {
|
|
354909
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
354910
|
+
}
|
|
354911
|
+
function iosTextIdentityPredicate(value) {
|
|
354912
|
+
const lit = iosPredicateLiteral(value);
|
|
354913
|
+
return `label == ${lit} OR name == ${lit} OR value == ${lit}`;
|
|
354914
|
+
}
|
|
354915
|
+
function textIdentityStrategies(value, platform3) {
|
|
354916
|
+
const strategies = [];
|
|
354917
|
+
if (platform3 === "IOS") {
|
|
354918
|
+
strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicate(value) });
|
|
354919
|
+
}
|
|
354920
|
+
strategies.push({ using: "xpath", value: identityTextXpath2(value) });
|
|
354921
|
+
return strategies;
|
|
354922
|
+
}
|
|
354923
|
+
function isIdentityIosPredicate(value) {
|
|
354924
|
+
return /(?:^|[\s(])(?:name|label|value)\s*==\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
|
|
354925
|
+
}
|
|
354610
354926
|
function isIdentityXpath(value) {
|
|
354611
354927
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
354612
354928
|
}
|
|
354613
354929
|
function isIdentityStrategy(strategy) {
|
|
354614
354930
|
if (strategy.using === "accessibility id" || strategy.using === "id") return true;
|
|
354615
354931
|
if (strategy.using === "xpath") return isIdentityXpath(strategy.value);
|
|
354932
|
+
if (strategy.using === "-ios predicate string") return isIdentityIosPredicate(strategy.value);
|
|
354616
354933
|
return false;
|
|
354617
354934
|
}
|
|
354618
354935
|
function buildIdentityLocatorStrategies(target, platform3) {
|
|
@@ -354622,15 +354939,18 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354622
354939
|
if (bundle) {
|
|
354623
354940
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354624
354941
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354625
|
-
|
|
354942
|
+
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354943
|
+
for (const value of [
|
|
354944
|
+
isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354945
|
+
bundle.contentDesc
|
|
354946
|
+
]) {
|
|
354626
354947
|
const trimmed = value?.trim();
|
|
354627
|
-
if (trimmed) strategies.push(
|
|
354948
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354628
354949
|
}
|
|
354629
|
-
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354630
354950
|
}
|
|
354631
354951
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354632
354952
|
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
354633
|
-
for (const strategy of locatorStrategyForValue(value)) {
|
|
354953
|
+
for (const strategy of locatorStrategyForValue(value, platform3)) {
|
|
354634
354954
|
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
354635
354955
|
}
|
|
354636
354956
|
}
|
|
@@ -355104,6 +355424,19 @@ async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
|
355104
355424
|
}
|
|
355105
355425
|
return values;
|
|
355106
355426
|
}
|
|
355427
|
+
var controlPortSlotsByDevice = /* @__PURE__ */ new Map();
|
|
355428
|
+
function acquireControlPortSlot(deviceId) {
|
|
355429
|
+
const existing = controlPortSlotsByDevice.get(deviceId);
|
|
355430
|
+
if (existing !== void 0) return existing;
|
|
355431
|
+
const taken = new Set(controlPortSlotsByDevice.values());
|
|
355432
|
+
let slot = 0;
|
|
355433
|
+
while (taken.has(slot)) slot += 1;
|
|
355434
|
+
controlPortSlotsByDevice.set(deviceId, slot);
|
|
355435
|
+
return slot;
|
|
355436
|
+
}
|
|
355437
|
+
function releaseControlPortSlot(deviceId) {
|
|
355438
|
+
controlPortSlotsByDevice.delete(deviceId);
|
|
355439
|
+
}
|
|
355107
355440
|
async function executeMobileTest(options) {
|
|
355108
355441
|
const logLines = [];
|
|
355109
355442
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -355123,6 +355456,7 @@ async function executeMobileTest(options) {
|
|
|
355123
355456
|
let captureStopped = false;
|
|
355124
355457
|
let recordingStarted = false;
|
|
355125
355458
|
let recordingStopped = false;
|
|
355459
|
+
let acquiredSlotDeviceId = null;
|
|
355126
355460
|
let video;
|
|
355127
355461
|
let failingSource;
|
|
355128
355462
|
const stopCapture = async () => {
|
|
@@ -355225,7 +355559,8 @@ async function executeMobileTest(options) {
|
|
|
355225
355559
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
355226
355560
|
}
|
|
355227
355561
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
355228
|
-
const leasedPortOffset =
|
|
355562
|
+
const leasedPortOffset = acquireControlPortSlot(selectedDevice.id);
|
|
355563
|
+
acquiredSlotDeviceId = selectedDevice.id;
|
|
355229
355564
|
try {
|
|
355230
355565
|
sessionId = await createSessionWithRetry({
|
|
355231
355566
|
appId: options.target.appId,
|
|
@@ -355502,6 +355837,7 @@ async function executeMobileTest(options) {
|
|
|
355502
355837
|
await deleteSession(sessionId);
|
|
355503
355838
|
log2("Session destroyed");
|
|
355504
355839
|
}
|
|
355840
|
+
if (acquiredSlotDeviceId) releaseControlPortSlot(acquiredSlotDeviceId);
|
|
355505
355841
|
}
|
|
355506
355842
|
}
|
|
355507
355843
|
async function createMobileDiscoverySession(target, options) {
|
|
@@ -355547,21 +355883,28 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355547
355883
|
isPhysical: selectedDevice.isPhysical === true,
|
|
355548
355884
|
platformVersion: selectedDevice.platformVersion
|
|
355549
355885
|
});
|
|
355550
|
-
const
|
|
355551
|
-
|
|
355552
|
-
|
|
355553
|
-
|
|
355554
|
-
|
|
355555
|
-
|
|
355556
|
-
|
|
355557
|
-
|
|
355558
|
-
|
|
355886
|
+
const discoveryPortSlotDeviceId = selectedDevice.id;
|
|
355887
|
+
let sessionId;
|
|
355888
|
+
try {
|
|
355889
|
+
sessionId = await createSessionWithRetry({
|
|
355890
|
+
appId: target.appId,
|
|
355891
|
+
platform: target.platform,
|
|
355892
|
+
deviceId: selectedDevice.id,
|
|
355893
|
+
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
355894
|
+
isPhysical: selectedDevice.isPhysical,
|
|
355895
|
+
portOffset: acquireControlPortSlot(discoveryPortSlotDeviceId)
|
|
355896
|
+
});
|
|
355897
|
+
} catch (err) {
|
|
355898
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355899
|
+
throw err;
|
|
355900
|
+
}
|
|
355559
355901
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
355560
355902
|
try {
|
|
355561
355903
|
await openDeepLink(sessionId, target.platform, target.deeplink, target.appId);
|
|
355562
355904
|
} catch (err) {
|
|
355563
355905
|
await deleteSession(sessionId).catch(() => {
|
|
355564
355906
|
});
|
|
355907
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355565
355908
|
throw new Error(
|
|
355566
355909
|
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
|
|
355567
355910
|
);
|
|
@@ -355583,6 +355926,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355583
355926
|
} catch {
|
|
355584
355927
|
}
|
|
355585
355928
|
await deleteSession(sessionId);
|
|
355929
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355586
355930
|
}
|
|
355587
355931
|
};
|
|
355588
355932
|
}
|
|
@@ -355600,26 +355944,26 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355600
355944
|
}
|
|
355601
355945
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355602
355946
|
if (platform3 === "IOS") {
|
|
355603
|
-
if (attrs.name) return `//*[@name=${
|
|
355604
|
-
|
|
355605
|
-
if (text) return identityTextXpath(text);
|
|
355947
|
+
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355948
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355606
355949
|
return "//*";
|
|
355607
355950
|
}
|
|
355608
|
-
if (attrs.resourceId) return `//*[@resource-id=${
|
|
355609
|
-
if (attrs.text) return
|
|
355610
|
-
if (attrs.contentDesc) return `//*[@content-desc=${
|
|
355951
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
355952
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355953
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral3(attrs.contentDesc)}]`;
|
|
355611
355954
|
return "//*";
|
|
355612
355955
|
}
|
|
355613
355956
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
355614
355957
|
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
355958
|
+
const volatileText = isEditableMobileClassName(className) || isStateValueMobileClassName(className);
|
|
355615
355959
|
if (platform3 === "IOS") {
|
|
355616
355960
|
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
355617
355961
|
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
355618
355962
|
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
355619
|
-
const text2 = label ?? value;
|
|
355963
|
+
const text2 = label ?? (volatileText ? void 0 : value);
|
|
355620
355964
|
if (!name && !text2) return null;
|
|
355621
355965
|
const bundle2 = {
|
|
355622
|
-
xpath: harvestFallbackXpath("IOS", { name,
|
|
355966
|
+
xpath: harvestFallbackXpath("IOS", { name, text: text2 }),
|
|
355623
355967
|
className
|
|
355624
355968
|
};
|
|
355625
355969
|
if (name) bundle2.accessibilityId = name;
|
|
@@ -355628,7 +355972,8 @@ async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
|
355628
355972
|
}
|
|
355629
355973
|
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
355630
355974
|
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
355631
|
-
const
|
|
355975
|
+
const rawText = await readElementAttribute(sessionId, elementId, "text");
|
|
355976
|
+
const text = volatileText ? void 0 : rawText;
|
|
355632
355977
|
if (!resourceId && !contentDesc && !text) return null;
|
|
355633
355978
|
const bundle = {
|
|
355634
355979
|
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
|
@@ -355960,6 +356305,7 @@ var sessionStartInFlight = null;
|
|
|
355960
356305
|
var sessionReaperTimer = null;
|
|
355961
356306
|
var mirrorClientsEmptyAt = null;
|
|
355962
356307
|
var mirrorSessionOwner = null;
|
|
356308
|
+
var actionInFlight = false;
|
|
355963
356309
|
var recordingWorkloadActive = false;
|
|
355964
356310
|
function setRecordingWorkloadActive(active) {
|
|
355965
356311
|
if (recordingWorkloadActive === active) return;
|
|
@@ -356189,7 +356535,11 @@ async function handleHealth(_req, res) {
|
|
|
356189
356535
|
// 409 — it must never be inferred from server-side global run counts, which
|
|
356190
356536
|
// include unrelated projects and runners.
|
|
356191
356537
|
executorBusy: executorBusyCount > 0,
|
|
356192
|
-
|
|
356538
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356539
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356540
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356541
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356542
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356193
356543
|
});
|
|
356194
356544
|
}
|
|
356195
356545
|
function realDeviceCapabilities2(platform3) {
|
|
@@ -356267,7 +356617,11 @@ async function handleDevices(_req, res) {
|
|
|
356267
356617
|
sendJSON(res, 200, {
|
|
356268
356618
|
devices,
|
|
356269
356619
|
executorBusy: executorBusyCount > 0,
|
|
356270
|
-
|
|
356620
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356621
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356622
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356623
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356624
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356271
356625
|
});
|
|
356272
356626
|
}
|
|
356273
356627
|
async function handleSessionStart(req, res) {
|
|
@@ -356356,6 +356710,8 @@ async function startSession(validated, res) {
|
|
|
356356
356710
|
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
356357
356711
|
mirrorSessionOwner = "recording";
|
|
356358
356712
|
setRecordingWorkloadActive(true);
|
|
356713
|
+
mirrorClientsEmptyAt = Date.now();
|
|
356714
|
+
ensureSessionReaper();
|
|
356359
356715
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
356360
356716
|
try {
|
|
356361
356717
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -356709,6 +357065,14 @@ async function handleAction(req, res) {
|
|
|
356709
357065
|
sendJSON(res, 400, { error: "No active Appium session" });
|
|
356710
357066
|
return;
|
|
356711
357067
|
}
|
|
357068
|
+
if (mirrorSessionOwner !== "recording") {
|
|
357069
|
+
sendJSON(res, 409, { error: "No recording session owns the device (a runner-driven session is active)." });
|
|
357070
|
+
return;
|
|
357071
|
+
}
|
|
357072
|
+
if (actionInFlight) {
|
|
357073
|
+
sendJSON(res, 409, { error: "Another action is still executing. Wait for it to finish." });
|
|
357074
|
+
return;
|
|
357075
|
+
}
|
|
356712
357076
|
validateSessionId(state.appiumSessionId);
|
|
356713
357077
|
const body = await parseBody(req);
|
|
356714
357078
|
const type = typeof body.type === "string" ? body.type : null;
|
|
@@ -356720,6 +357084,7 @@ async function handleAction(req, res) {
|
|
|
356720
357084
|
sendJSON(res, 400, { error: "Unknown or unsupported action type" });
|
|
356721
357085
|
return;
|
|
356722
357086
|
}
|
|
357087
|
+
actionInFlight = true;
|
|
356723
357088
|
try {
|
|
356724
357089
|
const sessionPath = `/session/${state.appiumSessionId}`;
|
|
356725
357090
|
switch (type) {
|
|
@@ -356792,15 +357157,23 @@ async function handleAction(req, res) {
|
|
|
356792
357157
|
sendJSON(res, 400, { error: `Unknown action type: ${type}` });
|
|
356793
357158
|
return;
|
|
356794
357159
|
}
|
|
356795
|
-
|
|
357160
|
+
await waitForUiSettle(sessionPath);
|
|
357161
|
+
const screenshot = await getScreenshot2();
|
|
357162
|
+
let source = await getPageSource2();
|
|
357163
|
+
if (source === null) {
|
|
357164
|
+
source = await getPageSource2();
|
|
357165
|
+
}
|
|
356796
357166
|
sendJSON(res, 200, {
|
|
356797
357167
|
ok: true,
|
|
356798
357168
|
screenshot: screenshot ?? null,
|
|
356799
357169
|
source: source ?? null,
|
|
357170
|
+
...source === null || source === void 0 ? { sourceUnavailable: true } : {},
|
|
356800
357171
|
timestamp: Date.now()
|
|
356801
357172
|
});
|
|
356802
357173
|
} catch (err) {
|
|
356803
357174
|
sendJSON(res, 500, { error: `Action failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
357175
|
+
} finally {
|
|
357176
|
+
actionInFlight = false;
|
|
356804
357177
|
}
|
|
356805
357178
|
}
|
|
356806
357179
|
async function handleScreenshot(_req, res) {
|
|
@@ -357038,7 +357411,11 @@ function getBridgeState() {
|
|
|
357038
357411
|
tunnelUrl: state.tunnelUrl,
|
|
357039
357412
|
tunnelToken: state.tunnelToken,
|
|
357040
357413
|
hasActiveSession: !!state.appiumSessionId,
|
|
357041
|
-
|
|
357414
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
357415
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
357416
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
357417
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
357418
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null,
|
|
357042
357419
|
platform: state.platform
|
|
357043
357420
|
};
|
|
357044
357421
|
}
|