@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.js
CHANGED
|
@@ -978,7 +978,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
978
978
|
const value = nonEmpty3(attrs.value);
|
|
979
979
|
const label = name ?? labelAttr ?? value;
|
|
980
980
|
const accessibilityId = name;
|
|
981
|
-
const text =
|
|
981
|
+
const text = labelAttr ?? (isStateValueRole(role) ? void 0 : value);
|
|
982
982
|
const bounds = parseIOSBounds(attrs);
|
|
983
983
|
const enabled = attrBoolDefaultTrue(attrs.enabled);
|
|
984
984
|
const visible = attrBoolDefaultTrue(attrs.visible);
|
|
@@ -1125,6 +1125,42 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1125
1125
|
}
|
|
1126
1126
|
}
|
|
1127
1127
|
}
|
|
1128
|
+
function annotateIdentityUniqueness(elements) {
|
|
1129
|
+
const a11yCounts = /* @__PURE__ */ new Map();
|
|
1130
|
+
const ridCounts = /* @__PURE__ */ new Map();
|
|
1131
|
+
const textCounts = /* @__PURE__ */ new Map();
|
|
1132
|
+
const bump = (map2, value) => {
|
|
1133
|
+
const key = value?.trim();
|
|
1134
|
+
if (!key)
|
|
1135
|
+
return;
|
|
1136
|
+
map2.set(key, (map2.get(key) ?? 0) + 1);
|
|
1137
|
+
};
|
|
1138
|
+
for (const element of elements) {
|
|
1139
|
+
bump(a11yCounts, element.accessibilityId);
|
|
1140
|
+
bump(ridCounts, element.resourceId);
|
|
1141
|
+
const identityValues = new Set([element.text, element.accessibilityId].map((value) => value?.trim()).filter((value) => Boolean(value)));
|
|
1142
|
+
for (const value of identityValues)
|
|
1143
|
+
bump(textCounts, value);
|
|
1144
|
+
}
|
|
1145
|
+
const isUnique = (map2, value) => {
|
|
1146
|
+
const key = value?.trim();
|
|
1147
|
+
if (!key)
|
|
1148
|
+
return void 0;
|
|
1149
|
+
return (map2.get(key) ?? 0) <= 1;
|
|
1150
|
+
};
|
|
1151
|
+
for (const element of elements) {
|
|
1152
|
+
const accessibilityId = isUnique(a11yCounts, element.accessibilityId);
|
|
1153
|
+
const resourceId = isUnique(ridCounts, element.resourceId);
|
|
1154
|
+
const text = isUnique(textCounts, element.text);
|
|
1155
|
+
if (accessibilityId === void 0 && resourceId === void 0 && text === void 0)
|
|
1156
|
+
continue;
|
|
1157
|
+
element.identityUnique = {
|
|
1158
|
+
...accessibilityId !== void 0 ? { accessibilityId } : {},
|
|
1159
|
+
...resourceId !== void 0 ? { resourceId } : {},
|
|
1160
|
+
...text !== void 0 ? { text } : {}
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1128
1164
|
function flatten(root, platform3) {
|
|
1129
1165
|
const out = [];
|
|
1130
1166
|
const stack = [];
|
|
@@ -1170,6 +1206,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1170
1206
|
}
|
|
1171
1207
|
}
|
|
1172
1208
|
markRepeatedBottomBarNavigation(out);
|
|
1209
|
+
annotateIdentityUniqueness(out);
|
|
1173
1210
|
return out;
|
|
1174
1211
|
}
|
|
1175
1212
|
function fnv1a(input) {
|
|
@@ -1251,6 +1288,13 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1251
1288
|
function isEditableElement(el) {
|
|
1252
1289
|
return EDITABLE_CLASS_RE.test(el.role.trim());
|
|
1253
1290
|
}
|
|
1291
|
+
var STATE_VALUE_CLASS_RE = /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/;
|
|
1292
|
+
function isStateValueRole(role) {
|
|
1293
|
+
return STATE_VALUE_CLASS_RE.test(role.trim());
|
|
1294
|
+
}
|
|
1295
|
+
function isStateValueElement(el) {
|
|
1296
|
+
return isStateValueRole(el.role);
|
|
1297
|
+
}
|
|
1254
1298
|
function xpathAttrLiteral(value) {
|
|
1255
1299
|
if (!value.includes("'"))
|
|
1256
1300
|
return `'${value}'`;
|
|
@@ -1261,17 +1305,18 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1261
1305
|
function semanticXPath(el, platform3) {
|
|
1262
1306
|
const cls = nonBlank(el.role) ?? "*";
|
|
1263
1307
|
const a11y = nonBlank(el.accessibilityId);
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1308
|
+
const rid = platform3 === "ANDROID" ? nonBlank(el.resourceId) : void 0;
|
|
1309
|
+
const text = isEditableElement(el) || isStateValueElement(el) ? void 0 : nonBlank(el.text);
|
|
1310
|
+
const a11yXpath = a11y ? platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]` : void 0;
|
|
1311
|
+
const ridXpath = rid ? `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]` : void 0;
|
|
1312
|
+
const textXpath = text ? platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]` : void 0;
|
|
1313
|
+
const candidates = [
|
|
1314
|
+
{ xpath: a11yXpath, unique: el.identityUnique?.accessibilityId },
|
|
1315
|
+
{ xpath: ridXpath, unique: el.identityUnique?.resourceId },
|
|
1316
|
+
{ xpath: textXpath, unique: el.identityUnique?.text }
|
|
1317
|
+
];
|
|
1318
|
+
const uniqueFirst = candidates.find((c) => c.xpath && c.unique !== false);
|
|
1319
|
+
return (uniqueFirst ?? candidates.find((c) => c.xpath))?.xpath;
|
|
1275
1320
|
}
|
|
1276
1321
|
function nonBlank(value) {
|
|
1277
1322
|
if (typeof value !== "string")
|
|
@@ -1280,30 +1325,40 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1280
1325
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
1281
1326
|
}
|
|
1282
1327
|
function buildLocatorBundle(el, platform3) {
|
|
1328
|
+
const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
|
|
1283
1329
|
const bundle = {
|
|
1284
1330
|
// Attribute-anchored xpath when the element has identity; the absolute
|
|
1285
1331
|
// indexed path only for identity-less elements (nothing else can find them).
|
|
1286
1332
|
xpath: semanticXPath(el, platform3) ?? el.xpath,
|
|
1287
1333
|
className: el.role
|
|
1288
1334
|
};
|
|
1289
|
-
|
|
1335
|
+
const a11yUnique = el.identityUnique?.accessibilityId !== false;
|
|
1336
|
+
const ridUnique = el.identityUnique?.resourceId !== false;
|
|
1337
|
+
const textUnique = el.identityUnique?.text !== false;
|
|
1338
|
+
const hasUniqueAlternativeToA11y = Boolean(el.resourceId && platform3 === "ANDROID" && ridUnique || durableText && textUnique);
|
|
1339
|
+
const hasUniqueAlternativeToRid = Boolean(el.accessibilityId && a11yUnique || durableText && textUnique);
|
|
1340
|
+
const includeA11y = Boolean(el.accessibilityId && (a11yUnique || !hasUniqueAlternativeToA11y));
|
|
1341
|
+
const includeRid = Boolean(platform3 === "ANDROID" && el.resourceId && (ridUnique || !hasUniqueAlternativeToRid));
|
|
1342
|
+
if (includeA11y && el.accessibilityId)
|
|
1290
1343
|
bundle.accessibilityId = el.accessibilityId;
|
|
1291
|
-
if (
|
|
1344
|
+
if (includeRid && el.resourceId)
|
|
1292
1345
|
bundle.resourceId = el.resourceId;
|
|
1293
|
-
if (
|
|
1294
|
-
bundle.text =
|
|
1295
|
-
if (platform3 === "ANDROID" && el.accessibilityId)
|
|
1346
|
+
if (durableText)
|
|
1347
|
+
bundle.text = durableText;
|
|
1348
|
+
if (includeA11y && platform3 === "ANDROID" && el.accessibilityId)
|
|
1296
1349
|
bundle.contentDesc = el.accessibilityId;
|
|
1297
1350
|
return bundle;
|
|
1298
1351
|
}
|
|
1299
1352
|
function preferredLocator(el) {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1353
|
+
const durableText = el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
|
|
1354
|
+
const candidates = [
|
|
1355
|
+
{ value: el.accessibilityId, unique: el.identityUnique?.accessibilityId },
|
|
1356
|
+
{ value: el.resourceId, unique: el.identityUnique?.resourceId },
|
|
1357
|
+
{ value: durableText, unique: el.identityUnique?.text }
|
|
1358
|
+
];
|
|
1359
|
+
const uniqueFirst = candidates.find((c) => c.value && c.unique !== false);
|
|
1360
|
+
const chosen = (uniqueFirst ?? candidates.find((c) => c.value))?.value;
|
|
1361
|
+
return chosen ?? el.xpath;
|
|
1307
1362
|
}
|
|
1308
1363
|
function buildScreenSignal(elements, root, platform3) {
|
|
1309
1364
|
const signal = {
|
|
@@ -1966,6 +2021,11 @@ function isEditableMobileClassName(className) {
|
|
|
1966
2021
|
return false;
|
|
1967
2022
|
return /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/.test(className.trim());
|
|
1968
2023
|
}
|
|
2024
|
+
function isStateValueMobileClassName(className) {
|
|
2025
|
+
if (!className)
|
|
2026
|
+
return false;
|
|
2027
|
+
return /(XCUIElementTypeSwitch|XCUIElementTypeToggle|XCUIElementTypeSlider|XCUIElementTypeStepper|XCUIElementTypePageIndicator|XCUIElementTypeProgressIndicator|XCUIElementTypeSegmentedControl|XCUIElementTypePicker|XCUIElementTypePickerWheel|ToggleButton|SeekBar|ProgressBar|RatingBar|NumberPicker)$/.test(className.trim());
|
|
2028
|
+
}
|
|
1969
2029
|
function mobileCredentialPlaceholder(key) {
|
|
1970
2030
|
return `{{${key}}}`;
|
|
1971
2031
|
}
|
|
@@ -2095,9 +2155,10 @@ function buildStepTarget(metadata, selector) {
|
|
|
2095
2155
|
locatorBundle.resourceId,
|
|
2096
2156
|
// For editable fields the text attribute IS the currently-typed value — a
|
|
2097
2157
|
// self-invalidating locator that only matches while the recorded text is
|
|
2098
|
-
// still in the field (and it can leak typed input into locators).
|
|
2099
|
-
//
|
|
2100
|
-
|
|
2158
|
+
// still in the field (and it can leak typed input into locators). State
|
|
2159
|
+
// controls' text is their current state ("1"/"ON") and collides with every
|
|
2160
|
+
// same-state sibling. Static elements keep their text fallback.
|
|
2161
|
+
isEditableMobileClassName(locatorBundle.className) || isStateValueMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
|
|
2101
2162
|
locatorBundle.contentDesc,
|
|
2102
2163
|
locatorBundle.xpath,
|
|
2103
2164
|
selector
|
|
@@ -2236,12 +2297,26 @@ function selectorForValue(value) {
|
|
|
2236
2297
|
function isIdentityMobileXpath(value) {
|
|
2237
2298
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
2238
2299
|
}
|
|
2300
|
+
function xpathLiteral(value) {
|
|
2301
|
+
if (!value.includes("'"))
|
|
2302
|
+
return `'${value}'`;
|
|
2303
|
+
if (!value.includes('"'))
|
|
2304
|
+
return `"${value}"`;
|
|
2305
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
2306
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
2307
|
+
}
|
|
2308
|
+
function identityTextXpath(value) {
|
|
2309
|
+
const lit = xpathLiteral(value);
|
|
2310
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
2311
|
+
}
|
|
2239
2312
|
function mobileLocatorBundleHasIdentity(bundle) {
|
|
2240
2313
|
if (!bundle)
|
|
2241
2314
|
return false;
|
|
2242
2315
|
if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
|
|
2243
2316
|
return true;
|
|
2244
|
-
if (nonEmpty(bundle.text)
|
|
2317
|
+
if (nonEmpty(bundle.text) && !isStateValueMobileClassName(bundle.className))
|
|
2318
|
+
return true;
|
|
2319
|
+
if (nonEmpty(bundle.contentDesc))
|
|
2245
2320
|
return true;
|
|
2246
2321
|
const xpath = nonEmpty(bundle.xpath);
|
|
2247
2322
|
return !!xpath && isIdentityMobileXpath(xpath);
|
|
@@ -2264,14 +2339,25 @@ function selectorCandidatesForStep(step) {
|
|
|
2264
2339
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2265
2340
|
if (nonEmpty(bundle.xpath))
|
|
2266
2341
|
candidates.push(bundle.xpath);
|
|
2342
|
+
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2343
|
+
const textIdentity = (isGateStep || !isEditableMobileClassName(bundle.className)) && !isStateValueMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2344
|
+
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2345
|
+
const trimmed = nonEmpty(value);
|
|
2346
|
+
if (trimmed)
|
|
2347
|
+
candidates.push(identityTextXpath(trimmed));
|
|
2348
|
+
}
|
|
2267
2349
|
}
|
|
2268
2350
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2351
|
+
const bundleContributedIdentity = candidates.some((value) => !(value.startsWith("/") || value.startsWith("(//")) || isIdentityMobileXpath(value));
|
|
2269
2352
|
const pushValueCandidates = (value) => {
|
|
2270
2353
|
candidates.push(selectorForValue(value));
|
|
2271
2354
|
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2272
2355
|
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2273
2356
|
candidates.push(`id=${value}`);
|
|
2274
2357
|
}
|
|
2358
|
+
if (!bundleContributedIdentity && !/^(~|id=|css=|\/|\()/.test(value)) {
|
|
2359
|
+
candidates.push(identityTextXpath(value));
|
|
2360
|
+
}
|
|
2275
2361
|
};
|
|
2276
2362
|
if (nonEmpty(target.primary))
|
|
2277
2363
|
pushValueCandidates(target.primary);
|
|
@@ -2710,7 +2796,7 @@ function uniq(values) {
|
|
|
2710
2796
|
}
|
|
2711
2797
|
return result;
|
|
2712
2798
|
}
|
|
2713
|
-
function
|
|
2799
|
+
function xpathLiteral2(value) {
|
|
2714
2800
|
if (!value.includes("'"))
|
|
2715
2801
|
return `'${value}'`;
|
|
2716
2802
|
if (!value.includes('"'))
|
|
@@ -2718,7 +2804,7 @@ function xpathLiteral(value) {
|
|
|
2718
2804
|
return `concat(${value.split("'").map((part) => `'${part}'`).join(`, "'", `)})`;
|
|
2719
2805
|
}
|
|
2720
2806
|
function mobileTextXPath(value, platform3) {
|
|
2721
|
-
const literal =
|
|
2807
|
+
const literal = xpathLiteral2(value);
|
|
2722
2808
|
if (platform3 === "IOS") {
|
|
2723
2809
|
return `//*[@name=${literal} or @label=${literal} or @value=${literal}]`;
|
|
2724
2810
|
}
|
|
@@ -2966,7 +3052,8 @@ function isWebViewStep(step) {
|
|
|
2966
3052
|
function hasCrossPlatformIdentity(bundle) {
|
|
2967
3053
|
if (!bundle)
|
|
2968
3054
|
return false;
|
|
2969
|
-
|
|
3055
|
+
const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
|
|
3056
|
+
return Boolean(durableText?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
2970
3057
|
}
|
|
2971
3058
|
function stepIsDivergentForPlatform(step, platform3) {
|
|
2972
3059
|
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
@@ -3048,7 +3135,8 @@ function summarizeMobileReadiness(steps, authoringPlatform) {
|
|
|
3048
3135
|
function hasStableNativeIdentity(bundle) {
|
|
3049
3136
|
if (!bundle)
|
|
3050
3137
|
return false;
|
|
3051
|
-
|
|
3138
|
+
const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
|
|
3139
|
+
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));
|
|
3052
3140
|
}
|
|
3053
3141
|
function isPositionalXpath(xpath) {
|
|
3054
3142
|
const value = xpath?.trim();
|
|
@@ -3128,6 +3216,7 @@ var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS, XPATH_ATTRIBUTE_EQUALITY_RE;
|
|
|
3128
3216
|
var init_mobile_cross_platform = __esm({
|
|
3129
3217
|
"../shared/dist/mobile-cross-platform.js"() {
|
|
3130
3218
|
"use strict";
|
|
3219
|
+
init_mobile_test_generation();
|
|
3131
3220
|
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
3132
3221
|
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
3133
3222
|
"tap",
|
|
@@ -3866,6 +3955,43 @@ function accumulateStatements(lines) {
|
|
|
3866
3955
|
}
|
|
3867
3956
|
return out;
|
|
3868
3957
|
}
|
|
3958
|
+
function classifyMobileAssertValue(value, context = {}) {
|
|
3959
|
+
const text = value.trim();
|
|
3960
|
+
if (!text)
|
|
3961
|
+
return null;
|
|
3962
|
+
if (/^\{\{[A-Z0-9_]+\}\}$/.test(text))
|
|
3963
|
+
return null;
|
|
3964
|
+
const t1 = matchFirst(TIER1_PATTERNS, text);
|
|
3965
|
+
if (t1)
|
|
3966
|
+
return { tier: 1, reason: t1[1] };
|
|
3967
|
+
const authIdentityValues = [];
|
|
3968
|
+
if (context.authenticatedUserEmail)
|
|
3969
|
+
authIdentityValues.push(context.authenticatedUserEmail);
|
|
3970
|
+
if (context.authenticatedUserName && context.authenticatedUserName.length >= 3) {
|
|
3971
|
+
authIdentityValues.push(context.authenticatedUserName);
|
|
3972
|
+
}
|
|
3973
|
+
for (const auth of authIdentityValues) {
|
|
3974
|
+
if (auth.length >= 3 && text.toLowerCase().includes(auth.toLowerCase())) {
|
|
3975
|
+
return { tier: 2, reason: "asserts the authenticated user identity \u2014 fails under any other test account" };
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
const knownValues = [
|
|
3979
|
+
...context.knownUserInputs ?? [],
|
|
3980
|
+
...context.scenarioCreatedValues ?? []
|
|
3981
|
+
].filter((v) => {
|
|
3982
|
+
for (const auth of authIdentityValues) {
|
|
3983
|
+
if (auth.length >= 3 && v.toLowerCase().includes(auth.toLowerCase()))
|
|
3984
|
+
return false;
|
|
3985
|
+
}
|
|
3986
|
+
return true;
|
|
3987
|
+
});
|
|
3988
|
+
if (containsKnownValue(text, knownValues))
|
|
3989
|
+
return null;
|
|
3990
|
+
const t2 = matchFirst(TIER2_PATTERNS, text);
|
|
3991
|
+
if (t2)
|
|
3992
|
+
return { tier: 2, reason: t2[1] };
|
|
3993
|
+
return null;
|
|
3994
|
+
}
|
|
3869
3995
|
function parenDepthDelta(text) {
|
|
3870
3996
|
let depth = 0;
|
|
3871
3997
|
let quote = null;
|
|
@@ -6039,6 +6165,7 @@ __export(dist_exports, {
|
|
|
6039
6165
|
classifyAssertionBlock: () => classifyAssertionBlock,
|
|
6040
6166
|
classifyAssertionLine: () => classifyAssertionLine,
|
|
6041
6167
|
classifyErrorMessage: () => classifyErrorMessage,
|
|
6168
|
+
classifyMobileAssertValue: () => classifyMobileAssertValue,
|
|
6042
6169
|
classifyTestHealth: () => classifyTestHealth,
|
|
6043
6170
|
coldSessionConfigMessage: () => coldSessionConfigMessage,
|
|
6044
6171
|
computeMobileSwipePointers: () => computeMobileSwipePointers,
|
|
@@ -6092,6 +6219,7 @@ __export(dist_exports, {
|
|
|
6092
6219
|
isEditableMobileClassName: () => isEditableMobileClassName,
|
|
6093
6220
|
isEmailFromIdentity: () => isEmailFromIdentity,
|
|
6094
6221
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
6222
|
+
isIdentityMobileXpath: () => isIdentityMobileXpath,
|
|
6095
6223
|
isMobilePlatform: () => isMobilePlatform,
|
|
6096
6224
|
isPricingPlan: () => isPricingPlan,
|
|
6097
6225
|
isRunnableStep: () => isRunnableStep,
|
|
@@ -6099,6 +6227,7 @@ __export(dist_exports, {
|
|
|
6099
6227
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
6100
6228
|
isSensitiveQueryKey: () => isSensitiveQueryKey,
|
|
6101
6229
|
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
6230
|
+
isStateValueMobileClassName: () => isStateValueMobileClassName,
|
|
6102
6231
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
6103
6232
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6104
6233
|
isTrialExpired: () => isTrialExpired,
|
|
@@ -7006,6 +7135,7 @@ var require_mobile_observation = __commonJS({
|
|
|
7006
7135
|
exports2.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
|
|
7007
7136
|
exports2.extractNativeMobileLandmarks = extractNativeMobileLandmarks;
|
|
7008
7137
|
exports2.safeMobileElementLabel = safeMobileElementLabel;
|
|
7138
|
+
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
7009
7139
|
var DEFAULT_MAX_ELEMENTS = 60;
|
|
7010
7140
|
var MAX_LABEL_LENGTH = 80;
|
|
7011
7141
|
var EVIDENCE_LABEL_LIMIT = 40;
|
|
@@ -7096,7 +7226,8 @@ var require_mobile_observation = __commonJS({
|
|
|
7096
7226
|
const locators = [];
|
|
7097
7227
|
if (stableId)
|
|
7098
7228
|
locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
|
|
7099
|
-
const
|
|
7229
|
+
const platform3 = element.role.startsWith("XCUIElementType") ? "IOS" : "ANDROID";
|
|
7230
|
+
const xpath = (0, mobile_source_parser_js_1.buildLocatorBundle)(element, platform3).xpath?.trim();
|
|
7100
7231
|
if (xpath)
|
|
7101
7232
|
locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
|
|
7102
7233
|
const collected = {
|
|
@@ -78345,6 +78476,10 @@ var require_discover_ai_planner = __commonJS({
|
|
|
78345
78476
|
if (context.appBrief) {
|
|
78346
78477
|
sections.push(`## Application Brief
|
|
78347
78478
|
${context.appBrief.slice(0, 500)}`);
|
|
78479
|
+
}
|
|
78480
|
+
if (context.knowledgeContext && context.knowledgeContext.trim().length > 0) {
|
|
78481
|
+
sections.push(`## Known Facts
|
|
78482
|
+
${context.knowledgeContext.slice(0, 4e3)}`);
|
|
78348
78483
|
}
|
|
78349
78484
|
sections.push(`## Base URL
|
|
78350
78485
|
${compressed.baseUrl}`);
|
|
@@ -80883,6 +81018,45 @@ ${constraints.join("\n")}`);
|
|
|
80883
81018
|
}
|
|
80884
81019
|
});
|
|
80885
81020
|
|
|
81021
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
81022
|
+
var require_discovery_lint_flags = __commonJS({
|
|
81023
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
81024
|
+
"use strict";
|
|
81025
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
81026
|
+
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
81027
|
+
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
81028
|
+
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
81029
|
+
exports2.mobileSelectorQualityLintMode = mobileSelectorQualityLintMode;
|
|
81030
|
+
exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
81031
|
+
function strictOn(value) {
|
|
81032
|
+
return value === "true" || value === "1";
|
|
81033
|
+
}
|
|
81034
|
+
function structuralLocatorLintMode() {
|
|
81035
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
81036
|
+
if (v === "warn")
|
|
81037
|
+
return "warn";
|
|
81038
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
81039
|
+
return "enforce";
|
|
81040
|
+
return "off";
|
|
81041
|
+
}
|
|
81042
|
+
function isPwMethodPreflightEnabled() {
|
|
81043
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
81044
|
+
}
|
|
81045
|
+
function isVqaLoginDedupeEnabled() {
|
|
81046
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
81047
|
+
}
|
|
81048
|
+
function mobileSelectorQualityLintMode() {
|
|
81049
|
+
const v = (process.env.MOBILE_SELECTOR_QUALITY_LINT || "").trim().toLowerCase();
|
|
81050
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
81051
|
+
return "enforce";
|
|
81052
|
+
return "off";
|
|
81053
|
+
}
|
|
81054
|
+
function isTestgenEmailVerifyEnabled() {
|
|
81055
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
81056
|
+
}
|
|
81057
|
+
}
|
|
81058
|
+
});
|
|
81059
|
+
|
|
80886
81060
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
80887
81061
|
var require_mobile_generator = __commonJS({
|
|
80888
81062
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
@@ -80902,6 +81076,7 @@ var require_mobile_generator = __commonJS({
|
|
|
80902
81076
|
var ai_queue_js_1 = require_ai_queue();
|
|
80903
81077
|
var ai_retry_js_1 = require_ai_retry();
|
|
80904
81078
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81079
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80905
81080
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
80906
81081
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
80907
81082
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81071,7 +81246,15 @@ var require_mobile_generator = __commonJS({
|
|
|
81071
81246
|
properties: {
|
|
81072
81247
|
name: { type: "string", description: 'Descriptive test name (e.g. "Login - Happy Path").' },
|
|
81073
81248
|
description: { type: "string", description: "One-line description of what the test proves." },
|
|
81074
|
-
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." }
|
|
81249
|
+
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." },
|
|
81250
|
+
suspectedBug: {
|
|
81251
|
+
type: "boolean",
|
|
81252
|
+
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."
|
|
81253
|
+
},
|
|
81254
|
+
bugDescription: {
|
|
81255
|
+
type: "string",
|
|
81256
|
+
description: "What is broken in the APP (not the test) \u2014 concrete screen + observed vs expected."
|
|
81257
|
+
}
|
|
81075
81258
|
},
|
|
81076
81259
|
required: ["name", "description", "verified"]
|
|
81077
81260
|
}
|
|
@@ -81317,6 +81500,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81317
81500
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
81318
81501
|
- 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.
|
|
81319
81502
|
- 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.
|
|
81503
|
+
- 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.
|
|
81504
|
+
- 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.
|
|
81320
81505
|
- 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.
|
|
81321
81506
|
- 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.
|
|
81322
81507
|
- 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.
|
|
@@ -81422,6 +81607,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
81422
81607
|
let assertCount = 0;
|
|
81423
81608
|
let finish = null;
|
|
81424
81609
|
let finishIntentRejections = 0;
|
|
81610
|
+
let finishStrengthRejections = 0;
|
|
81425
81611
|
let emptyTurnsAfterFinishReject = 0;
|
|
81426
81612
|
let latest = null;
|
|
81427
81613
|
const getLatest = () => latest;
|
|
@@ -81914,31 +82100,49 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81914
82100
|
const claimedVerified = Boolean(toolArgs.verified);
|
|
81915
82101
|
const intentReason = scenarioIntentMismatchReason(scenario, actions);
|
|
81916
82102
|
const verifiedWithoutAssert = claimedVerified && assertCount === 0;
|
|
81917
|
-
const
|
|
81918
|
-
|
|
81919
|
-
|
|
82103
|
+
const intentStyleReject = finishIntentRejections < MAX_FINISH_INTENT_REJECTIONS && iteration < maxIterations && (intentReason !== null || verifiedWithoutAssert);
|
|
82104
|
+
let strengthRejectReason = null;
|
|
82105
|
+
if (!intentStyleReject && claimedVerified && assertCount > 0 && intentReason === null && finishStrengthRejections < 1 && iteration < maxIterations) {
|
|
82106
|
+
const verdict = evaluateMobileAssertionStrength(actions);
|
|
82107
|
+
if (verdict.severity === "block") {
|
|
82108
|
+
strengthRejectReason = verdict.reason;
|
|
82109
|
+
}
|
|
82110
|
+
}
|
|
82111
|
+
if (intentStyleReject || strengthRejectReason !== null) {
|
|
82112
|
+
if (intentStyleReject)
|
|
82113
|
+
finishIntentRejections += 1;
|
|
82114
|
+
if (strengthRejectReason !== null)
|
|
82115
|
+
finishStrengthRejections += 1;
|
|
81920
82116
|
const reasons = [];
|
|
81921
82117
|
if (intentReason)
|
|
81922
82118
|
reasons.push(intentReason);
|
|
81923
82119
|
if (verifiedWithoutAssert) {
|
|
81924
82120
|
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
81925
82121
|
}
|
|
82122
|
+
if (strengthRejectReason !== null) {
|
|
82123
|
+
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.`);
|
|
82124
|
+
}
|
|
81926
82125
|
const reasonText = reasons.join("; ");
|
|
81927
|
-
log2(` finish_generate REJECTED (${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}): ${reasonText}`);
|
|
82126
|
+
log2(` finish_generate REJECTED (intent ${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}, strength ${finishStrengthRejections}/1): ${reasonText}`);
|
|
81928
82127
|
pushResult({
|
|
81929
82128
|
ok: false,
|
|
81930
82129
|
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.`,
|
|
81931
82130
|
intentMismatch: intentReason,
|
|
81932
82131
|
verifiedWithoutAssert,
|
|
81933
|
-
|
|
81934
|
-
|
|
82132
|
+
...strengthRejectReason !== null ? { strengthBlock: strengthRejectReason } : {},
|
|
82133
|
+
rejection: finishIntentRejections + finishStrengthRejections,
|
|
82134
|
+
maxRejections: MAX_FINISH_INTENT_REJECTIONS + 1
|
|
81935
82135
|
});
|
|
81936
82136
|
continue;
|
|
81937
82137
|
}
|
|
81938
82138
|
finish = {
|
|
81939
82139
|
name: redactCredentialValues(nonEmpty3(String(toolArgs.name ?? "")) ?? scenario.name, redactionCredentials),
|
|
81940
82140
|
description: redactCredentialValues(nonEmpty3(String(toolArgs.description ?? "")) ?? scenario.goal, redactionCredentials),
|
|
81941
|
-
verified: claimedVerified
|
|
82141
|
+
verified: claimedVerified,
|
|
82142
|
+
...toolArgs.suspectedBug === true ? {
|
|
82143
|
+
suspectedBug: true,
|
|
82144
|
+
bugDescription: redactCredentialValues(nonEmpty3(String(toolArgs.bugDescription ?? "")) ?? "", redactionCredentials)
|
|
82145
|
+
} : {}
|
|
81942
82146
|
};
|
|
81943
82147
|
log2(` finish_generate: "${finish.name}" (claimed verified: ${finish.verified}, asserts: ${assertCount})`);
|
|
81944
82148
|
pushResult({ acknowledged: true });
|
|
@@ -82002,6 +82206,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82002
82206
|
ok: false,
|
|
82003
82207
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
82004
82208
|
});
|
|
82209
|
+
const recordedTypeValues = () => recordedActions.filter((action) => action.type === "TYPE" && typeof action.value === "string" && action.value.length > 0).map((action) => action.value);
|
|
82005
82210
|
const actionPayload = (result, absorbed) => {
|
|
82006
82211
|
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
82007
82212
|
return {
|
|
@@ -82402,7 +82607,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82402
82607
|
latest,
|
|
82403
82608
|
screenName,
|
|
82404
82609
|
platform: platform3,
|
|
82405
|
-
recordAction
|
|
82610
|
+
recordAction,
|
|
82611
|
+
typedValues: recordedTypeValues
|
|
82406
82612
|
});
|
|
82407
82613
|
if (!recorded.ok)
|
|
82408
82614
|
return recorded;
|
|
@@ -82471,6 +82677,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82471
82677
|
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.`
|
|
82472
82678
|
};
|
|
82473
82679
|
}
|
|
82680
|
+
const volatileExpected = (0, shared_1.classifyMobileAssertValue)(expected, {
|
|
82681
|
+
knownUserInputs: recordedTypeValues()
|
|
82682
|
+
});
|
|
82683
|
+
if (volatileExpected) {
|
|
82684
|
+
return {
|
|
82685
|
+
ok: false,
|
|
82686
|
+
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.`
|
|
82687
|
+
};
|
|
82688
|
+
}
|
|
82474
82689
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
82475
82690
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82476
82691
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
@@ -82526,6 +82741,17 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82526
82741
|
if (!accessibilityId && !text) {
|
|
82527
82742
|
return { ok: false, error: "mobile_assert_visible needs a ref (preferred), accessibilityId, or text." };
|
|
82528
82743
|
}
|
|
82744
|
+
if (!accessibilityId && text) {
|
|
82745
|
+
const volatileIdentity = (0, shared_1.classifyMobileAssertValue)(text, {
|
|
82746
|
+
knownUserInputs: ctx.typedValues?.() ?? []
|
|
82747
|
+
});
|
|
82748
|
+
if (volatileIdentity) {
|
|
82749
|
+
return {
|
|
82750
|
+
ok: false,
|
|
82751
|
+
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).`
|
|
82752
|
+
};
|
|
82753
|
+
}
|
|
82754
|
+
}
|
|
82529
82755
|
const directTarget = accessibilityId ?? text;
|
|
82530
82756
|
if (directTarget && isStructuralAssertionIdentity(directTarget)) {
|
|
82531
82757
|
return {
|
|
@@ -83161,7 +83387,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83161
83387
|
if (claimedVerified && !verified) {
|
|
83162
83388
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
83163
83389
|
}
|
|
83164
|
-
const
|
|
83390
|
+
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
83391
|
+
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
83392
|
+
const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
|
|
83393
|
+
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
83394
|
+
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."));
|
|
83395
|
+
}
|
|
83396
|
+
const strengthVerdict = bugWitnessHonored ? {
|
|
83397
|
+
...baseStrengthVerdict,
|
|
83398
|
+
ok: false,
|
|
83399
|
+
severity: "route-bug",
|
|
83400
|
+
reason: `Model declared an app bug during generation: ${bugDescription}`
|
|
83401
|
+
} : baseStrengthVerdict;
|
|
83402
|
+
if (bugWitnessHonored) {
|
|
83403
|
+
log2(` bug-witness honored \u2014 routing as app bug: ${bugDescription}`);
|
|
83404
|
+
}
|
|
83165
83405
|
if (strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug") {
|
|
83166
83406
|
if (verified) {
|
|
83167
83407
|
log2(` downgraded verified -> false: assertion strength ${strengthVerdict.severity} \u2014 ${strengthVerdict.reason}`);
|
|
@@ -83177,7 +83417,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83177
83417
|
verified = false;
|
|
83178
83418
|
}
|
|
83179
83419
|
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
83180
|
-
const
|
|
83420
|
+
const selectorLintEnforced = (0, discovery_lint_flags_js_1.mobileSelectorQualityLintMode)() === "enforce" && selectorQuality.summary.high > 0;
|
|
83421
|
+
if (selectorLintEnforced && verified) {
|
|
83422
|
+
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
83423
|
+
}
|
|
83424
|
+
const reviewTag = verified && !selectorLintEnforced ? "@verified" : "@needs-review";
|
|
83181
83425
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
83182
83426
|
if (strengthVerdict.severity === "downgrade")
|
|
83183
83427
|
tags.push("@smoke");
|
|
@@ -86257,38 +86501,6 @@ var require_run_callback_proxy = __commonJS({
|
|
|
86257
86501
|
}
|
|
86258
86502
|
});
|
|
86259
86503
|
|
|
86260
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
86261
|
-
var require_discovery_lint_flags = __commonJS({
|
|
86262
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
86263
|
-
"use strict";
|
|
86264
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
86265
|
-
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
86266
|
-
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
86267
|
-
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
86268
|
-
exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
86269
|
-
function strictOn(value) {
|
|
86270
|
-
return value === "true" || value === "1";
|
|
86271
|
-
}
|
|
86272
|
-
function structuralLocatorLintMode() {
|
|
86273
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
86274
|
-
if (v === "warn")
|
|
86275
|
-
return "warn";
|
|
86276
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
86277
|
-
return "enforce";
|
|
86278
|
-
return "off";
|
|
86279
|
-
}
|
|
86280
|
-
function isPwMethodPreflightEnabled() {
|
|
86281
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
86282
|
-
}
|
|
86283
|
-
function isVqaLoginDedupeEnabled() {
|
|
86284
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
86285
|
-
}
|
|
86286
|
-
function isTestgenEmailVerifyEnabled() {
|
|
86287
|
-
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
86288
|
-
}
|
|
86289
|
-
}
|
|
86290
|
-
});
|
|
86291
|
-
|
|
86292
86504
|
// ../runner-core/dist/services/auth-recipe.js
|
|
86293
86505
|
var require_auth_recipe = __commonJS({
|
|
86294
86506
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
@@ -92149,6 +92361,15 @@ var require_agentic_discovery_prompts = __commonJS({
|
|
|
92149
92361
|
lines.push(input.appBrief.trim());
|
|
92150
92362
|
lines.push("");
|
|
92151
92363
|
}
|
|
92364
|
+
if (input.knownFactsBlock && input.knownFactsBlock.trim()) {
|
|
92365
|
+
lines.push("## Known facts about this feature (from project documentation)");
|
|
92366
|
+
lines.push("");
|
|
92367
|
+
lines.push("Advisory hints about routes/data/constraints \u2014 verify against the live");
|
|
92368
|
+
lines.push("app; the browser is the source of truth for locators.");
|
|
92369
|
+
lines.push("");
|
|
92370
|
+
lines.push(input.knownFactsBlock.trim());
|
|
92371
|
+
lines.push("");
|
|
92372
|
+
}
|
|
92152
92373
|
if (input.credentials && input.credentials.length > 0) {
|
|
92153
92374
|
lines.push("## Test credentials (throwaway accounts)");
|
|
92154
92375
|
lines.push("");
|
|
@@ -328816,6 +329037,15 @@ var require_agentic_survey_prompts = __commonJS({
|
|
|
328816
329037
|
lines.push(input.appBrief.trim());
|
|
328817
329038
|
lines.push("");
|
|
328818
329039
|
}
|
|
329040
|
+
if (input.knownFactsBlock && input.knownFactsBlock.trim().length > 0) {
|
|
329041
|
+
lines.push("## Known facts about this app (from project documentation)");
|
|
329042
|
+
lines.push("");
|
|
329043
|
+
lines.push("Use these as hints for what features/routes/constraints exist. They are");
|
|
329044
|
+
lines.push("advisory \u2014 verify against the live app; the browser is the source of truth.");
|
|
329045
|
+
lines.push("");
|
|
329046
|
+
lines.push(input.knownFactsBlock.trim());
|
|
329047
|
+
lines.push("");
|
|
329048
|
+
}
|
|
328819
329049
|
if (input.credentials && input.credentials.length > 0) {
|
|
328820
329050
|
lines.push("## Test credentials (throwaway accounts)");
|
|
328821
329051
|
lines.push("");
|
|
@@ -329443,6 +329673,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
329443
329673
|
const briefInput = {
|
|
329444
329674
|
baseUrl: ctx.baseUrl,
|
|
329445
329675
|
appBrief: ctx.appBrief,
|
|
329676
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
329446
329677
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
329447
329678
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
329448
329679
|
focusAreas: ctx.focusAreas,
|
|
@@ -331138,6 +331369,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
331138
331369
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
331139
331370
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
331140
331371
|
appBrief: ctx.appBrief,
|
|
331372
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
331141
331373
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
331142
331374
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
331143
331375
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -332075,6 +332307,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
332075
332307
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
332076
332308
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
332077
332309
|
appBrief: ctx.appBrief,
|
|
332310
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
332078
332311
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
332079
332312
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
332080
332313
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -333166,6 +333399,7 @@ ${preview}`);
|
|
|
333166
333399
|
const briefInput = {
|
|
333167
333400
|
baseUrl: ctx.baseUrl,
|
|
333168
333401
|
appBrief: ctx.appBrief,
|
|
333402
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
333169
333403
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
333170
333404
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
333171
333405
|
focusAreas: ctx.focusAreas,
|
|
@@ -334025,6 +334259,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
334025
334259
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
334026
334260
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
334027
334261
|
appBrief: ctx.appBrief,
|
|
334262
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
334028
334263
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
334029
334264
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
334030
334265
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -335879,7 +336114,8 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
335879
336114
|
// scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
|
|
335880
336115
|
// auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
|
|
335881
336116
|
emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
|
|
335882
|
-
testBudget
|
|
336117
|
+
testBudget,
|
|
336118
|
+
knowledgeContext: ctx.knowledgeContext
|
|
335883
336119
|
}, {
|
|
335884
336120
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
335885
336121
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -335991,6 +336227,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
335991
336227
|
interactionLog: exploreResult.interactionLog,
|
|
335992
336228
|
modelOverride: ctx.generateModel,
|
|
335993
336229
|
appBrief: ctx.appBrief,
|
|
336230
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
335994
336231
|
onAICall,
|
|
335995
336232
|
onProgress,
|
|
335996
336233
|
mcpClient: mcpClient ?? void 0,
|
|
@@ -336470,7 +336707,8 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
336470
336707
|
// configured inbox), so the planner never promises what generation can't do.
|
|
336471
336708
|
// Scoped to AUTH features: a non-auth feature has no signup surface to test.
|
|
336472
336709
|
emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
|
|
336473
|
-
testBudget: featureTestBudget
|
|
336710
|
+
testBudget: featureTestBudget,
|
|
336711
|
+
knowledgeContext: ctx.knowledgeContext
|
|
336474
336712
|
}, {
|
|
336475
336713
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
336476
336714
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -336516,6 +336754,7 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
336516
336754
|
interactionLog: exploreResult.interactionLog,
|
|
336517
336755
|
modelOverride: ctx.generateModel,
|
|
336518
336756
|
appBrief: ctx.appBrief,
|
|
336757
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
336519
336758
|
onAICall,
|
|
336520
336759
|
mcpClient: mcpClient ?? void 0,
|
|
336521
336760
|
// Pooled browser for parallel scenario generation (Fargate opt-in via
|
|
@@ -347729,6 +347968,12 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347729
347968
|
"Use {{TEST_USER_EMAIL}}/{{TEST_USER_PASSWORD}} for credentials. Include",
|
|
347730
347969
|
"assertVisible or assertText unless tagging @needs-review. When an incremental",
|
|
347731
347970
|
"allow-list is present, extract ONLY from those files.",
|
|
347971
|
+
"",
|
|
347972
|
+
"Every tap/type/clear/waitForElement/assertVisible/assertText target MUST",
|
|
347973
|
+
"carry an IDENTITY locator: ~accessibility-id, id=resource-id, visible text,",
|
|
347974
|
+
"or an attribute-EQUALITY xpath (//android.widget.Button[@text='Submit']).",
|
|
347975
|
+
"Positional/index-only xpaths (/hierarchy/\u2026/View[3]) are rejected \u2014 imported",
|
|
347976
|
+
"steps have no recorded coordinates, so such a step can never replay.",
|
|
347732
347977
|
"Write strictly valid JSON."
|
|
347733
347978
|
].join("\n");
|
|
347734
347979
|
}
|
|
@@ -347746,6 +347991,32 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347746
347991
|
function isPlainObject2(value) {
|
|
347747
347992
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
347748
347993
|
}
|
|
347994
|
+
var LOCATOR_REQUIRING_IMPORT_ACTIONS = /* @__PURE__ */ new Set([
|
|
347995
|
+
"tap",
|
|
347996
|
+
"type",
|
|
347997
|
+
"clear",
|
|
347998
|
+
"waitForElement",
|
|
347999
|
+
"assertVisible",
|
|
348000
|
+
"assertText"
|
|
348001
|
+
]);
|
|
348002
|
+
function selectorValueHasIdentity(value) {
|
|
348003
|
+
const trimmed = value.trim();
|
|
348004
|
+
if (!trimmed)
|
|
348005
|
+
return false;
|
|
348006
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("(//"))
|
|
348007
|
+
return (0, shared_1.isIdentityMobileXpath)(trimmed);
|
|
348008
|
+
return true;
|
|
348009
|
+
}
|
|
348010
|
+
function stepLocatorLacksIdentity(step) {
|
|
348011
|
+
if (!LOCATOR_REQUIRING_IMPORT_ACTIONS.has(step.action))
|
|
348012
|
+
return false;
|
|
348013
|
+
const target = step.target;
|
|
348014
|
+
if (!target)
|
|
348015
|
+
return true;
|
|
348016
|
+
if ((0, shared_1.mobileLocatorBundleHasIdentity)(target.locatorBundle))
|
|
348017
|
+
return false;
|
|
348018
|
+
return ![target.primary, ...target.fallbacks ?? []].some((value) => typeof value === "string" && selectorValueHasIdentity(value));
|
|
348019
|
+
}
|
|
347749
348020
|
function isValidStep(value) {
|
|
347750
348021
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
347751
348022
|
return false;
|
|
@@ -347787,12 +348058,7 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347787
348058
|
const o = value;
|
|
347788
348059
|
if (!isNonEmptyString(o.name))
|
|
347789
348060
|
return { ok: false, reason: "name: missing" };
|
|
347790
|
-
|
|
347791
|
-
return { ok: false, reason: "appiumCode: missing" };
|
|
347792
|
-
const code = o.appiumCode;
|
|
347793
|
-
if (!/webdriverio|remote\s*\(|driver\s*\./i.test(code)) {
|
|
347794
|
-
return { ok: false, reason: "appiumCode: not a WebdriverIO/Appium script" };
|
|
347795
|
-
}
|
|
348061
|
+
const code = isNonEmptyString(o.appiumCode) ? o.appiumCode : "";
|
|
347796
348062
|
if (!Array.isArray(o.steps) || o.steps.length === 0) {
|
|
347797
348063
|
return { ok: false, reason: "steps: missing or empty" };
|
|
347798
348064
|
}
|
|
@@ -347802,6 +348068,13 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347802
348068
|
if (!o.steps.every(isValidStep)) {
|
|
347803
348069
|
return { ok: false, reason: "steps: invalid step shape" };
|
|
347804
348070
|
}
|
|
348071
|
+
const identityless = o.steps.find(stepLocatorLacksIdentity);
|
|
348072
|
+
if (identityless) {
|
|
348073
|
+
return {
|
|
348074
|
+
ok: false,
|
|
348075
|
+
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'].`
|
|
348076
|
+
};
|
|
348077
|
+
}
|
|
347805
348078
|
const steps = o.steps.map((step, order) => ({
|
|
347806
348079
|
...step,
|
|
347807
348080
|
order,
|
|
@@ -351095,7 +351368,7 @@ var require_package4 = __commonJS({
|
|
|
351095
351368
|
"package.json"(exports2, module2) {
|
|
351096
351369
|
module2.exports = {
|
|
351097
351370
|
name: "@validate.qa/runner",
|
|
351098
|
-
version: "1.0.
|
|
351371
|
+
version: "1.0.68",
|
|
351099
351372
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351100
351373
|
bin: {
|
|
351101
351374
|
"validate-runner": "dist/cli.js"
|
|
@@ -351491,6 +351764,9 @@ function sanitizeAppiumLogLine(line) {
|
|
|
351491
351764
|
}
|
|
351492
351765
|
function setAppiumWorkloadActive(active) {
|
|
351493
351766
|
activeWorkloadCount = active ? activeWorkloadCount + 1 : Math.max(0, activeWorkloadCount - 1);
|
|
351767
|
+
if (activeWorkloadCount === 0) {
|
|
351768
|
+
consecutiveHealthFailures = 0;
|
|
351769
|
+
}
|
|
351494
351770
|
}
|
|
351495
351771
|
function shouldRestartAfterHealthFailure(workloadCount, failureCount) {
|
|
351496
351772
|
return workloadCount <= 0 && failureCount >= HEALTH_FAILURES_BEFORE_RESTART;
|
|
@@ -351986,17 +352262,31 @@ function rectMatchesIntendedBounds(rect, bounds) {
|
|
|
351986
352262
|
return rectCenterInBounds || boundsCenterInRect;
|
|
351987
352263
|
}
|
|
351988
352264
|
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
351989
|
-
const
|
|
352265
|
+
const rawBox = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
352266
|
+
const box = rawBox && rawBox.width > 0 && rawBox.height > 0 ? rawBox : null;
|
|
351990
352267
|
let elementId = await getActiveElementId(sessionPath);
|
|
351991
|
-
|
|
352268
|
+
let rejectedElementId = null;
|
|
352269
|
+
let rejectedForMismatch = false;
|
|
352270
|
+
if (elementId && box) {
|
|
351992
352271
|
const rect = await getElementRect(sessionPath, elementId);
|
|
351993
352272
|
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
352273
|
+
rejectedElementId = elementId;
|
|
352274
|
+
rejectedForMismatch = !!rect;
|
|
351994
352275
|
elementId = null;
|
|
351995
352276
|
}
|
|
351996
352277
|
}
|
|
351997
352278
|
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
351998
352279
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
351999
352280
|
elementId = await getActiveElementId(sessionPath);
|
|
352281
|
+
if (elementId && box) {
|
|
352282
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
352283
|
+
const provablyWrong = rect ? !rectMatchesIntendedBounds(rect, box) : rejectedForMismatch && elementId === rejectedElementId;
|
|
352284
|
+
if (provablyWrong) {
|
|
352285
|
+
throw new Error(
|
|
352286
|
+
"Could not focus the intended field: a different element kept keyboard focus after tapping the target bounds. Tap the field directly, then retry."
|
|
352287
|
+
);
|
|
352288
|
+
}
|
|
352289
|
+
}
|
|
352000
352290
|
}
|
|
352001
352291
|
return elementId;
|
|
352002
352292
|
}
|
|
@@ -354546,7 +354836,7 @@ function boundsForRun(step, platform3, isCrossPlatform) {
|
|
|
354546
354836
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
354547
354837
|
return boundsFromStep(step);
|
|
354548
354838
|
}
|
|
354549
|
-
function locatorStrategyForValue(value) {
|
|
354839
|
+
function locatorStrategyForValue(value, platform3) {
|
|
354550
354840
|
const trimmed = value?.trim();
|
|
354551
354841
|
if (!trimmed) {
|
|
354552
354842
|
return [];
|
|
@@ -354562,7 +354852,8 @@ function locatorStrategyForValue(value) {
|
|
|
354562
354852
|
}
|
|
354563
354853
|
return [
|
|
354564
354854
|
{ using: "accessibility id", value: trimmed },
|
|
354565
|
-
{ using: "id", value: trimmed }
|
|
354855
|
+
{ using: "id", value: trimmed },
|
|
354856
|
+
...textIdentityStrategies(trimmed, platform3)
|
|
354566
354857
|
];
|
|
354567
354858
|
}
|
|
354568
354859
|
function buildLocatorStrategies(target, platform3) {
|
|
@@ -354572,12 +354863,19 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354572
354863
|
if (bundle) {
|
|
354573
354864
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354574
354865
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354575
|
-
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354866
|
+
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354867
|
+
for (const value of [
|
|
354868
|
+
isEditableMobileClassName(bundle.className) || isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354869
|
+
bundle.contentDesc
|
|
354870
|
+
]) {
|
|
354871
|
+
const trimmed = value?.trim();
|
|
354872
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354873
|
+
}
|
|
354576
354874
|
}
|
|
354577
354875
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354578
|
-
strategies.push(...locatorStrategyForValue(target.primary));
|
|
354876
|
+
strategies.push(...locatorStrategyForValue(target.primary, platform3));
|
|
354579
354877
|
for (const fallback of target.fallbacks ?? []) {
|
|
354580
|
-
strategies.push(...locatorStrategyForValue(fallback));
|
|
354878
|
+
strategies.push(...locatorStrategyForValue(fallback, platform3));
|
|
354581
354879
|
}
|
|
354582
354880
|
}
|
|
354583
354881
|
return strategies.filter((strategy) => strategy.using !== "xpath" || isIdentityXpath(strategy.value)).filter(
|
|
@@ -354589,22 +354887,41 @@ function dedupeStrategies(strategies) {
|
|
|
354589
354887
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
354590
354888
|
);
|
|
354591
354889
|
}
|
|
354592
|
-
function
|
|
354890
|
+
function xpathLiteral3(value) {
|
|
354593
354891
|
if (!value.includes("'")) return `'${value}'`;
|
|
354594
354892
|
if (!value.includes('"')) return `"${value}"`;
|
|
354595
354893
|
const parts = value.split("'").map((p) => `'${p}'`);
|
|
354596
354894
|
return `concat(${parts.join(`, "'", `)})`;
|
|
354597
354895
|
}
|
|
354598
|
-
function
|
|
354599
|
-
const lit =
|
|
354896
|
+
function identityTextXpath2(value) {
|
|
354897
|
+
const lit = xpathLiteral3(value);
|
|
354600
354898
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354601
354899
|
}
|
|
354900
|
+
function iosPredicateLiteral(value) {
|
|
354901
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
354902
|
+
}
|
|
354903
|
+
function iosTextIdentityPredicate(value) {
|
|
354904
|
+
const lit = iosPredicateLiteral(value);
|
|
354905
|
+
return `label == ${lit} OR name == ${lit} OR value == ${lit}`;
|
|
354906
|
+
}
|
|
354907
|
+
function textIdentityStrategies(value, platform3) {
|
|
354908
|
+
const strategies = [];
|
|
354909
|
+
if (platform3 === "IOS") {
|
|
354910
|
+
strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicate(value) });
|
|
354911
|
+
}
|
|
354912
|
+
strategies.push({ using: "xpath", value: identityTextXpath2(value) });
|
|
354913
|
+
return strategies;
|
|
354914
|
+
}
|
|
354915
|
+
function isIdentityIosPredicate(value) {
|
|
354916
|
+
return /(?:^|[\s(])(?:name|label|value)\s*==\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
|
|
354917
|
+
}
|
|
354602
354918
|
function isIdentityXpath(value) {
|
|
354603
354919
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
354604
354920
|
}
|
|
354605
354921
|
function isIdentityStrategy(strategy) {
|
|
354606
354922
|
if (strategy.using === "accessibility id" || strategy.using === "id") return true;
|
|
354607
354923
|
if (strategy.using === "xpath") return isIdentityXpath(strategy.value);
|
|
354924
|
+
if (strategy.using === "-ios predicate string") return isIdentityIosPredicate(strategy.value);
|
|
354608
354925
|
return false;
|
|
354609
354926
|
}
|
|
354610
354927
|
function buildIdentityLocatorStrategies(target, platform3) {
|
|
@@ -354614,15 +354931,18 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354614
354931
|
if (bundle) {
|
|
354615
354932
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354616
354933
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354617
|
-
|
|
354934
|
+
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354935
|
+
for (const value of [
|
|
354936
|
+
isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354937
|
+
bundle.contentDesc
|
|
354938
|
+
]) {
|
|
354618
354939
|
const trimmed = value?.trim();
|
|
354619
|
-
if (trimmed) strategies.push(
|
|
354940
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354620
354941
|
}
|
|
354621
|
-
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354622
354942
|
}
|
|
354623
354943
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354624
354944
|
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
354625
|
-
for (const strategy of locatorStrategyForValue(value)) {
|
|
354945
|
+
for (const strategy of locatorStrategyForValue(value, platform3)) {
|
|
354626
354946
|
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
354627
354947
|
}
|
|
354628
354948
|
}
|
|
@@ -355096,6 +355416,19 @@ async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
|
355096
355416
|
}
|
|
355097
355417
|
return values;
|
|
355098
355418
|
}
|
|
355419
|
+
var controlPortSlotsByDevice = /* @__PURE__ */ new Map();
|
|
355420
|
+
function acquireControlPortSlot(deviceId) {
|
|
355421
|
+
const existing = controlPortSlotsByDevice.get(deviceId);
|
|
355422
|
+
if (existing !== void 0) return existing;
|
|
355423
|
+
const taken = new Set(controlPortSlotsByDevice.values());
|
|
355424
|
+
let slot = 0;
|
|
355425
|
+
while (taken.has(slot)) slot += 1;
|
|
355426
|
+
controlPortSlotsByDevice.set(deviceId, slot);
|
|
355427
|
+
return slot;
|
|
355428
|
+
}
|
|
355429
|
+
function releaseControlPortSlot(deviceId) {
|
|
355430
|
+
controlPortSlotsByDevice.delete(deviceId);
|
|
355431
|
+
}
|
|
355099
355432
|
async function executeMobileTest(options) {
|
|
355100
355433
|
const logLines = [];
|
|
355101
355434
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -355115,6 +355448,7 @@ async function executeMobileTest(options) {
|
|
|
355115
355448
|
let captureStopped = false;
|
|
355116
355449
|
let recordingStarted = false;
|
|
355117
355450
|
let recordingStopped = false;
|
|
355451
|
+
let acquiredSlotDeviceId = null;
|
|
355118
355452
|
let video;
|
|
355119
355453
|
let failingSource;
|
|
355120
355454
|
const stopCapture = async () => {
|
|
@@ -355217,7 +355551,8 @@ async function executeMobileTest(options) {
|
|
|
355217
355551
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
355218
355552
|
}
|
|
355219
355553
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
355220
|
-
const leasedPortOffset =
|
|
355554
|
+
const leasedPortOffset = acquireControlPortSlot(selectedDevice.id);
|
|
355555
|
+
acquiredSlotDeviceId = selectedDevice.id;
|
|
355221
355556
|
try {
|
|
355222
355557
|
sessionId = await createSessionWithRetry({
|
|
355223
355558
|
appId: options.target.appId,
|
|
@@ -355494,6 +355829,7 @@ async function executeMobileTest(options) {
|
|
|
355494
355829
|
await deleteSession(sessionId);
|
|
355495
355830
|
log2("Session destroyed");
|
|
355496
355831
|
}
|
|
355832
|
+
if (acquiredSlotDeviceId) releaseControlPortSlot(acquiredSlotDeviceId);
|
|
355497
355833
|
}
|
|
355498
355834
|
}
|
|
355499
355835
|
async function createMobileDiscoverySession(target, options) {
|
|
@@ -355539,21 +355875,28 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355539
355875
|
isPhysical: selectedDevice.isPhysical === true,
|
|
355540
355876
|
platformVersion: selectedDevice.platformVersion
|
|
355541
355877
|
});
|
|
355542
|
-
const
|
|
355543
|
-
|
|
355544
|
-
|
|
355545
|
-
|
|
355546
|
-
|
|
355547
|
-
|
|
355548
|
-
|
|
355549
|
-
|
|
355550
|
-
|
|
355878
|
+
const discoveryPortSlotDeviceId = selectedDevice.id;
|
|
355879
|
+
let sessionId;
|
|
355880
|
+
try {
|
|
355881
|
+
sessionId = await createSessionWithRetry({
|
|
355882
|
+
appId: target.appId,
|
|
355883
|
+
platform: target.platform,
|
|
355884
|
+
deviceId: selectedDevice.id,
|
|
355885
|
+
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
355886
|
+
isPhysical: selectedDevice.isPhysical,
|
|
355887
|
+
portOffset: acquireControlPortSlot(discoveryPortSlotDeviceId)
|
|
355888
|
+
});
|
|
355889
|
+
} catch (err) {
|
|
355890
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355891
|
+
throw err;
|
|
355892
|
+
}
|
|
355551
355893
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
355552
355894
|
try {
|
|
355553
355895
|
await openDeepLink(sessionId, target.platform, target.deeplink, target.appId);
|
|
355554
355896
|
} catch (err) {
|
|
355555
355897
|
await deleteSession(sessionId).catch(() => {
|
|
355556
355898
|
});
|
|
355899
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355557
355900
|
throw new Error(
|
|
355558
355901
|
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
|
|
355559
355902
|
);
|
|
@@ -355575,6 +355918,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355575
355918
|
} catch {
|
|
355576
355919
|
}
|
|
355577
355920
|
await deleteSession(sessionId);
|
|
355921
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355578
355922
|
}
|
|
355579
355923
|
};
|
|
355580
355924
|
}
|
|
@@ -355592,26 +355936,26 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355592
355936
|
}
|
|
355593
355937
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355594
355938
|
if (platform3 === "IOS") {
|
|
355595
|
-
if (attrs.name) return `//*[@name=${
|
|
355596
|
-
|
|
355597
|
-
if (text) return identityTextXpath(text);
|
|
355939
|
+
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355940
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355598
355941
|
return "//*";
|
|
355599
355942
|
}
|
|
355600
|
-
if (attrs.resourceId) return `//*[@resource-id=${
|
|
355601
|
-
if (attrs.text) return
|
|
355602
|
-
if (attrs.contentDesc) return `//*[@content-desc=${
|
|
355943
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
355944
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355945
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral3(attrs.contentDesc)}]`;
|
|
355603
355946
|
return "//*";
|
|
355604
355947
|
}
|
|
355605
355948
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
355606
355949
|
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
355950
|
+
const volatileText = isEditableMobileClassName(className) || isStateValueMobileClassName(className);
|
|
355607
355951
|
if (platform3 === "IOS") {
|
|
355608
355952
|
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
355609
355953
|
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
355610
355954
|
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
355611
|
-
const text2 = label ?? value;
|
|
355955
|
+
const text2 = label ?? (volatileText ? void 0 : value);
|
|
355612
355956
|
if (!name && !text2) return null;
|
|
355613
355957
|
const bundle2 = {
|
|
355614
|
-
xpath: harvestFallbackXpath("IOS", { name,
|
|
355958
|
+
xpath: harvestFallbackXpath("IOS", { name, text: text2 }),
|
|
355615
355959
|
className
|
|
355616
355960
|
};
|
|
355617
355961
|
if (name) bundle2.accessibilityId = name;
|
|
@@ -355620,7 +355964,8 @@ async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
|
355620
355964
|
}
|
|
355621
355965
|
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
355622
355966
|
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
355623
|
-
const
|
|
355967
|
+
const rawText = await readElementAttribute(sessionId, elementId, "text");
|
|
355968
|
+
const text = volatileText ? void 0 : rawText;
|
|
355624
355969
|
if (!resourceId && !contentDesc && !text) return null;
|
|
355625
355970
|
const bundle = {
|
|
355626
355971
|
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
|
@@ -355952,6 +356297,7 @@ var sessionStartInFlight = null;
|
|
|
355952
356297
|
var sessionReaperTimer = null;
|
|
355953
356298
|
var mirrorClientsEmptyAt = null;
|
|
355954
356299
|
var mirrorSessionOwner = null;
|
|
356300
|
+
var actionInFlight = false;
|
|
355955
356301
|
var recordingWorkloadActive = false;
|
|
355956
356302
|
function setRecordingWorkloadActive(active) {
|
|
355957
356303
|
if (recordingWorkloadActive === active) return;
|
|
@@ -356181,7 +356527,11 @@ async function handleHealth(_req, res) {
|
|
|
356181
356527
|
// 409 — it must never be inferred from server-side global run counts, which
|
|
356182
356528
|
// include unrelated projects and runners.
|
|
356183
356529
|
executorBusy: executorBusyCount > 0,
|
|
356184
|
-
|
|
356530
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356531
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356532
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356533
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356534
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356185
356535
|
});
|
|
356186
356536
|
}
|
|
356187
356537
|
function realDeviceCapabilities2(platform3) {
|
|
@@ -356259,7 +356609,11 @@ async function handleDevices(_req, res) {
|
|
|
356259
356609
|
sendJSON(res, 200, {
|
|
356260
356610
|
devices,
|
|
356261
356611
|
executorBusy: executorBusyCount > 0,
|
|
356262
|
-
|
|
356612
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356613
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356614
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356615
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356616
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356263
356617
|
});
|
|
356264
356618
|
}
|
|
356265
356619
|
async function handleSessionStart(req, res) {
|
|
@@ -356348,6 +356702,8 @@ async function startSession(validated, res) {
|
|
|
356348
356702
|
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
356349
356703
|
mirrorSessionOwner = "recording";
|
|
356350
356704
|
setRecordingWorkloadActive(true);
|
|
356705
|
+
mirrorClientsEmptyAt = Date.now();
|
|
356706
|
+
ensureSessionReaper();
|
|
356351
356707
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
356352
356708
|
try {
|
|
356353
356709
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -356701,6 +357057,14 @@ async function handleAction(req, res) {
|
|
|
356701
357057
|
sendJSON(res, 400, { error: "No active Appium session" });
|
|
356702
357058
|
return;
|
|
356703
357059
|
}
|
|
357060
|
+
if (mirrorSessionOwner !== "recording") {
|
|
357061
|
+
sendJSON(res, 409, { error: "No recording session owns the device (a runner-driven session is active)." });
|
|
357062
|
+
return;
|
|
357063
|
+
}
|
|
357064
|
+
if (actionInFlight) {
|
|
357065
|
+
sendJSON(res, 409, { error: "Another action is still executing. Wait for it to finish." });
|
|
357066
|
+
return;
|
|
357067
|
+
}
|
|
356704
357068
|
validateSessionId(state.appiumSessionId);
|
|
356705
357069
|
const body = await parseBody(req);
|
|
356706
357070
|
const type = typeof body.type === "string" ? body.type : null;
|
|
@@ -356712,6 +357076,7 @@ async function handleAction(req, res) {
|
|
|
356712
357076
|
sendJSON(res, 400, { error: "Unknown or unsupported action type" });
|
|
356713
357077
|
return;
|
|
356714
357078
|
}
|
|
357079
|
+
actionInFlight = true;
|
|
356715
357080
|
try {
|
|
356716
357081
|
const sessionPath = `/session/${state.appiumSessionId}`;
|
|
356717
357082
|
switch (type) {
|
|
@@ -356784,15 +357149,23 @@ async function handleAction(req, res) {
|
|
|
356784
357149
|
sendJSON(res, 400, { error: `Unknown action type: ${type}` });
|
|
356785
357150
|
return;
|
|
356786
357151
|
}
|
|
356787
|
-
|
|
357152
|
+
await waitForUiSettle(sessionPath);
|
|
357153
|
+
const screenshot = await getScreenshot2();
|
|
357154
|
+
let source = await getPageSource2();
|
|
357155
|
+
if (source === null) {
|
|
357156
|
+
source = await getPageSource2();
|
|
357157
|
+
}
|
|
356788
357158
|
sendJSON(res, 200, {
|
|
356789
357159
|
ok: true,
|
|
356790
357160
|
screenshot: screenshot ?? null,
|
|
356791
357161
|
source: source ?? null,
|
|
357162
|
+
...source === null || source === void 0 ? { sourceUnavailable: true } : {},
|
|
356792
357163
|
timestamp: Date.now()
|
|
356793
357164
|
});
|
|
356794
357165
|
} catch (err) {
|
|
356795
357166
|
sendJSON(res, 500, { error: `Action failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
357167
|
+
} finally {
|
|
357168
|
+
actionInFlight = false;
|
|
356796
357169
|
}
|
|
356797
357170
|
}
|
|
356798
357171
|
async function handleScreenshot(_req, res) {
|
|
@@ -357030,7 +357403,11 @@ function getBridgeState() {
|
|
|
357030
357403
|
tunnelUrl: state.tunnelUrl,
|
|
357031
357404
|
tunnelToken: state.tunnelToken,
|
|
357032
357405
|
hasActiveSession: !!state.appiumSessionId,
|
|
357033
|
-
|
|
357406
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
357407
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
357408
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
357409
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
357410
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null,
|
|
357034
357411
|
platform: state.platform
|
|
357035
357412
|
};
|
|
357036
357413
|
}
|