@validate.qa/runner 1.0.67 → 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 +221 -53
- package/dist/cli.mjs +221 -53
- 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
|
|
@@ -2253,7 +2314,9 @@ function mobileLocatorBundleHasIdentity(bundle) {
|
|
|
2253
2314
|
return false;
|
|
2254
2315
|
if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
|
|
2255
2316
|
return true;
|
|
2256
|
-
if (nonEmpty(bundle.text)
|
|
2317
|
+
if (nonEmpty(bundle.text) && !isStateValueMobileClassName(bundle.className))
|
|
2318
|
+
return true;
|
|
2319
|
+
if (nonEmpty(bundle.contentDesc))
|
|
2257
2320
|
return true;
|
|
2258
2321
|
const xpath = nonEmpty(bundle.xpath);
|
|
2259
2322
|
return !!xpath && isIdentityMobileXpath(xpath);
|
|
@@ -2274,23 +2337,27 @@ function selectorCandidatesForStep(step) {
|
|
|
2274
2337
|
candidates.push(`~${bundle.accessibilityId}`);
|
|
2275
2338
|
if (nonEmpty(bundle.resourceId))
|
|
2276
2339
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2340
|
+
if (nonEmpty(bundle.xpath))
|
|
2341
|
+
candidates.push(bundle.xpath);
|
|
2277
2342
|
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2278
|
-
const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2343
|
+
const textIdentity = (isGateStep || !isEditableMobileClassName(bundle.className)) && !isStateValueMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2279
2344
|
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2280
2345
|
const trimmed = nonEmpty(value);
|
|
2281
2346
|
if (trimmed)
|
|
2282
2347
|
candidates.push(identityTextXpath(trimmed));
|
|
2283
2348
|
}
|
|
2284
|
-
if (nonEmpty(bundle.xpath))
|
|
2285
|
-
candidates.push(bundle.xpath);
|
|
2286
2349
|
}
|
|
2287
2350
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2351
|
+
const bundleContributedIdentity = candidates.some((value) => !(value.startsWith("/") || value.startsWith("(//")) || isIdentityMobileXpath(value));
|
|
2288
2352
|
const pushValueCandidates = (value) => {
|
|
2289
2353
|
candidates.push(selectorForValue(value));
|
|
2290
2354
|
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2291
2355
|
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2292
2356
|
candidates.push(`id=${value}`);
|
|
2293
2357
|
}
|
|
2358
|
+
if (!bundleContributedIdentity && !/^(~|id=|css=|\/|\()/.test(value)) {
|
|
2359
|
+
candidates.push(identityTextXpath(value));
|
|
2360
|
+
}
|
|
2294
2361
|
};
|
|
2295
2362
|
if (nonEmpty(target.primary))
|
|
2296
2363
|
pushValueCandidates(target.primary);
|
|
@@ -2985,7 +3052,8 @@ function isWebViewStep(step) {
|
|
|
2985
3052
|
function hasCrossPlatformIdentity(bundle) {
|
|
2986
3053
|
if (!bundle)
|
|
2987
3054
|
return false;
|
|
2988
|
-
|
|
3055
|
+
const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
|
|
3056
|
+
return Boolean(durableText?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
2989
3057
|
}
|
|
2990
3058
|
function stepIsDivergentForPlatform(step, platform3) {
|
|
2991
3059
|
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
@@ -3067,7 +3135,8 @@ function summarizeMobileReadiness(steps, authoringPlatform) {
|
|
|
3067
3135
|
function hasStableNativeIdentity(bundle) {
|
|
3068
3136
|
if (!bundle)
|
|
3069
3137
|
return false;
|
|
3070
|
-
|
|
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));
|
|
3071
3140
|
}
|
|
3072
3141
|
function isPositionalXpath(xpath) {
|
|
3073
3142
|
const value = xpath?.trim();
|
|
@@ -3147,6 +3216,7 @@ var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS, XPATH_ATTRIBUTE_EQUALITY_RE;
|
|
|
3147
3216
|
var init_mobile_cross_platform = __esm({
|
|
3148
3217
|
"../shared/dist/mobile-cross-platform.js"() {
|
|
3149
3218
|
"use strict";
|
|
3219
|
+
init_mobile_test_generation();
|
|
3150
3220
|
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
3151
3221
|
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
3152
3222
|
"tap",
|
|
@@ -6149,6 +6219,7 @@ __export(dist_exports, {
|
|
|
6149
6219
|
isEditableMobileClassName: () => isEditableMobileClassName,
|
|
6150
6220
|
isEmailFromIdentity: () => isEmailFromIdentity,
|
|
6151
6221
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
6222
|
+
isIdentityMobileXpath: () => isIdentityMobileXpath,
|
|
6152
6223
|
isMobilePlatform: () => isMobilePlatform,
|
|
6153
6224
|
isPricingPlan: () => isPricingPlan,
|
|
6154
6225
|
isRunnableStep: () => isRunnableStep,
|
|
@@ -6156,6 +6227,7 @@ __export(dist_exports, {
|
|
|
6156
6227
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
6157
6228
|
isSensitiveQueryKey: () => isSensitiveQueryKey,
|
|
6158
6229
|
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
6230
|
+
isStateValueMobileClassName: () => isStateValueMobileClassName,
|
|
6159
6231
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
6160
6232
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6161
6233
|
isTrialExpired: () => isTrialExpired,
|
|
@@ -7063,6 +7135,7 @@ var require_mobile_observation = __commonJS({
|
|
|
7063
7135
|
exports2.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
|
|
7064
7136
|
exports2.extractNativeMobileLandmarks = extractNativeMobileLandmarks;
|
|
7065
7137
|
exports2.safeMobileElementLabel = safeMobileElementLabel;
|
|
7138
|
+
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
7066
7139
|
var DEFAULT_MAX_ELEMENTS = 60;
|
|
7067
7140
|
var MAX_LABEL_LENGTH = 80;
|
|
7068
7141
|
var EVIDENCE_LABEL_LIMIT = 40;
|
|
@@ -7153,7 +7226,8 @@ var require_mobile_observation = __commonJS({
|
|
|
7153
7226
|
const locators = [];
|
|
7154
7227
|
if (stableId)
|
|
7155
7228
|
locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
|
|
7156
|
-
const
|
|
7229
|
+
const platform3 = element.role.startsWith("XCUIElementType") ? "IOS" : "ANDROID";
|
|
7230
|
+
const xpath = (0, mobile_source_parser_js_1.buildLocatorBundle)(element, platform3).xpath?.trim();
|
|
7157
7231
|
if (xpath)
|
|
7158
7232
|
locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
|
|
7159
7233
|
const collected = {
|
|
@@ -78402,6 +78476,10 @@ var require_discover_ai_planner = __commonJS({
|
|
|
78402
78476
|
if (context.appBrief) {
|
|
78403
78477
|
sections.push(`## Application Brief
|
|
78404
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)}`);
|
|
78405
78483
|
}
|
|
78406
78484
|
sections.push(`## Base URL
|
|
78407
78485
|
${compressed.baseUrl}`);
|
|
@@ -92283,6 +92361,15 @@ var require_agentic_discovery_prompts = __commonJS({
|
|
|
92283
92361
|
lines.push(input.appBrief.trim());
|
|
92284
92362
|
lines.push("");
|
|
92285
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
|
+
}
|
|
92286
92373
|
if (input.credentials && input.credentials.length > 0) {
|
|
92287
92374
|
lines.push("## Test credentials (throwaway accounts)");
|
|
92288
92375
|
lines.push("");
|
|
@@ -328950,6 +329037,15 @@ var require_agentic_survey_prompts = __commonJS({
|
|
|
328950
329037
|
lines.push(input.appBrief.trim());
|
|
328951
329038
|
lines.push("");
|
|
328952
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
|
+
}
|
|
328953
329049
|
if (input.credentials && input.credentials.length > 0) {
|
|
328954
329050
|
lines.push("## Test credentials (throwaway accounts)");
|
|
328955
329051
|
lines.push("");
|
|
@@ -329577,6 +329673,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
329577
329673
|
const briefInput = {
|
|
329578
329674
|
baseUrl: ctx.baseUrl,
|
|
329579
329675
|
appBrief: ctx.appBrief,
|
|
329676
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
329580
329677
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
329581
329678
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
329582
329679
|
focusAreas: ctx.focusAreas,
|
|
@@ -331272,6 +331369,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
331272
331369
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
331273
331370
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
331274
331371
|
appBrief: ctx.appBrief,
|
|
331372
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
331275
331373
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
331276
331374
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
331277
331375
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -332209,6 +332307,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
332209
332307
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
332210
332308
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
332211
332309
|
appBrief: ctx.appBrief,
|
|
332310
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
332212
332311
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
332213
332312
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
332214
332313
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -333300,6 +333399,7 @@ ${preview}`);
|
|
|
333300
333399
|
const briefInput = {
|
|
333301
333400
|
baseUrl: ctx.baseUrl,
|
|
333302
333401
|
appBrief: ctx.appBrief,
|
|
333402
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
333303
333403
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
333304
333404
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
333305
333405
|
focusAreas: ctx.focusAreas,
|
|
@@ -334159,6 +334259,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
334159
334259
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
334160
334260
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
334161
334261
|
appBrief: ctx.appBrief,
|
|
334262
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
334162
334263
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
334163
334264
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
334164
334265
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -336013,7 +336114,8 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
336013
336114
|
// scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
|
|
336014
336115
|
// auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
|
|
336015
336116
|
emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
|
|
336016
|
-
testBudget
|
|
336117
|
+
testBudget,
|
|
336118
|
+
knowledgeContext: ctx.knowledgeContext
|
|
336017
336119
|
}, {
|
|
336018
336120
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
336019
336121
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -336125,6 +336227,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
336125
336227
|
interactionLog: exploreResult.interactionLog,
|
|
336126
336228
|
modelOverride: ctx.generateModel,
|
|
336127
336229
|
appBrief: ctx.appBrief,
|
|
336230
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
336128
336231
|
onAICall,
|
|
336129
336232
|
onProgress,
|
|
336130
336233
|
mcpClient: mcpClient ?? void 0,
|
|
@@ -336604,7 +336707,8 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
336604
336707
|
// configured inbox), so the planner never promises what generation can't do.
|
|
336605
336708
|
// Scoped to AUTH features: a non-auth feature has no signup surface to test.
|
|
336606
336709
|
emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
|
|
336607
|
-
testBudget: featureTestBudget
|
|
336710
|
+
testBudget: featureTestBudget,
|
|
336711
|
+
knowledgeContext: ctx.knowledgeContext
|
|
336608
336712
|
}, {
|
|
336609
336713
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
336610
336714
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -336650,6 +336754,7 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
336650
336754
|
interactionLog: exploreResult.interactionLog,
|
|
336651
336755
|
modelOverride: ctx.generateModel,
|
|
336652
336756
|
appBrief: ctx.appBrief,
|
|
336757
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
336653
336758
|
onAICall,
|
|
336654
336759
|
mcpClient: mcpClient ?? void 0,
|
|
336655
336760
|
// Pooled browser for parallel scenario generation (Fargate opt-in via
|
|
@@ -347863,6 +347968,12 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347863
347968
|
"Use {{TEST_USER_EMAIL}}/{{TEST_USER_PASSWORD}} for credentials. Include",
|
|
347864
347969
|
"assertVisible or assertText unless tagging @needs-review. When an incremental",
|
|
347865
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.",
|
|
347866
347977
|
"Write strictly valid JSON."
|
|
347867
347978
|
].join("\n");
|
|
347868
347979
|
}
|
|
@@ -347880,6 +347991,32 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347880
347991
|
function isPlainObject2(value) {
|
|
347881
347992
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
347882
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
|
+
}
|
|
347883
348020
|
function isValidStep(value) {
|
|
347884
348021
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
347885
348022
|
return false;
|
|
@@ -347931,6 +348068,13 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347931
348068
|
if (!o.steps.every(isValidStep)) {
|
|
347932
348069
|
return { ok: false, reason: "steps: invalid step shape" };
|
|
347933
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
|
+
}
|
|
347934
348078
|
const steps = o.steps.map((step, order) => ({
|
|
347935
348079
|
...step,
|
|
347936
348080
|
order,
|
|
@@ -351224,7 +351368,7 @@ var require_package4 = __commonJS({
|
|
|
351224
351368
|
"package.json"(exports2, module2) {
|
|
351225
351369
|
module2.exports = {
|
|
351226
351370
|
name: "@validate.qa/runner",
|
|
351227
|
-
version: "1.0.
|
|
351371
|
+
version: "1.0.68",
|
|
351228
351372
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351229
351373
|
bin: {
|
|
351230
351374
|
"validate-runner": "dist/cli.js"
|
|
@@ -354692,7 +354836,7 @@ function boundsForRun(step, platform3, isCrossPlatform) {
|
|
|
354692
354836
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
354693
354837
|
return boundsFromStep(step);
|
|
354694
354838
|
}
|
|
354695
|
-
function locatorStrategyForValue(value) {
|
|
354839
|
+
function locatorStrategyForValue(value, platform3) {
|
|
354696
354840
|
const trimmed = value?.trim();
|
|
354697
354841
|
if (!trimmed) {
|
|
354698
354842
|
return [];
|
|
@@ -354708,7 +354852,8 @@ function locatorStrategyForValue(value) {
|
|
|
354708
354852
|
}
|
|
354709
354853
|
return [
|
|
354710
354854
|
{ using: "accessibility id", value: trimmed },
|
|
354711
|
-
{ using: "id", value: trimmed }
|
|
354855
|
+
{ using: "id", value: trimmed },
|
|
354856
|
+
...textIdentityStrategies(trimmed, platform3)
|
|
354712
354857
|
];
|
|
354713
354858
|
}
|
|
354714
354859
|
function buildLocatorStrategies(target, platform3) {
|
|
@@ -354718,19 +354863,19 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354718
354863
|
if (bundle) {
|
|
354719
354864
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354720
354865
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354866
|
+
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354721
354867
|
for (const value of [
|
|
354722
|
-
isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354868
|
+
isEditableMobileClassName(bundle.className) || isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354723
354869
|
bundle.contentDesc
|
|
354724
354870
|
]) {
|
|
354725
354871
|
const trimmed = value?.trim();
|
|
354726
|
-
if (trimmed) strategies.push(
|
|
354872
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354727
354873
|
}
|
|
354728
|
-
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354729
354874
|
}
|
|
354730
354875
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354731
|
-
strategies.push(...locatorStrategyForValue(target.primary));
|
|
354876
|
+
strategies.push(...locatorStrategyForValue(target.primary, platform3));
|
|
354732
354877
|
for (const fallback of target.fallbacks ?? []) {
|
|
354733
|
-
strategies.push(...locatorStrategyForValue(fallback));
|
|
354878
|
+
strategies.push(...locatorStrategyForValue(fallback, platform3));
|
|
354734
354879
|
}
|
|
354735
354880
|
}
|
|
354736
354881
|
return strategies.filter((strategy) => strategy.using !== "xpath" || isIdentityXpath(strategy.value)).filter(
|
|
@@ -354752,12 +354897,31 @@ function identityTextXpath2(value) {
|
|
|
354752
354897
|
const lit = xpathLiteral3(value);
|
|
354753
354898
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354754
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
|
+
}
|
|
354755
354918
|
function isIdentityXpath(value) {
|
|
354756
354919
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
354757
354920
|
}
|
|
354758
354921
|
function isIdentityStrategy(strategy) {
|
|
354759
354922
|
if (strategy.using === "accessibility id" || strategy.using === "id") return true;
|
|
354760
354923
|
if (strategy.using === "xpath") return isIdentityXpath(strategy.value);
|
|
354924
|
+
if (strategy.using === "-ios predicate string") return isIdentityIosPredicate(strategy.value);
|
|
354761
354925
|
return false;
|
|
354762
354926
|
}
|
|
354763
354927
|
function buildIdentityLocatorStrategies(target, platform3) {
|
|
@@ -354767,15 +354931,18 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354767
354931
|
if (bundle) {
|
|
354768
354932
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354769
354933
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354770
|
-
|
|
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
|
+
]) {
|
|
354771
354939
|
const trimmed = value?.trim();
|
|
354772
|
-
if (trimmed) strategies.push(
|
|
354940
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354773
354941
|
}
|
|
354774
|
-
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354775
354942
|
}
|
|
354776
354943
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354777
354944
|
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
354778
|
-
for (const strategy of locatorStrategyForValue(value)) {
|
|
354945
|
+
for (const strategy of locatorStrategyForValue(value, platform3)) {
|
|
354779
354946
|
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
354780
354947
|
}
|
|
354781
354948
|
}
|
|
@@ -355770,8 +355937,7 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355770
355937
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355771
355938
|
if (platform3 === "IOS") {
|
|
355772
355939
|
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355773
|
-
|
|
355774
|
-
if (text) return identityTextXpath2(text);
|
|
355940
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355775
355941
|
return "//*";
|
|
355776
355942
|
}
|
|
355777
355943
|
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
@@ -355781,14 +355947,15 @@ function harvestFallbackXpath(platform3, attrs) {
|
|
|
355781
355947
|
}
|
|
355782
355948
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
355783
355949
|
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
355950
|
+
const volatileText = isEditableMobileClassName(className) || isStateValueMobileClassName(className);
|
|
355784
355951
|
if (platform3 === "IOS") {
|
|
355785
355952
|
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
355786
355953
|
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
355787
355954
|
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
355788
|
-
const text2 = label ?? value;
|
|
355955
|
+
const text2 = label ?? (volatileText ? void 0 : value);
|
|
355789
355956
|
if (!name && !text2) return null;
|
|
355790
355957
|
const bundle2 = {
|
|
355791
|
-
xpath: harvestFallbackXpath("IOS", { name,
|
|
355958
|
+
xpath: harvestFallbackXpath("IOS", { name, text: text2 }),
|
|
355792
355959
|
className
|
|
355793
355960
|
};
|
|
355794
355961
|
if (name) bundle2.accessibilityId = name;
|
|
@@ -355797,7 +355964,8 @@ async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
|
355797
355964
|
}
|
|
355798
355965
|
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
355799
355966
|
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
355800
|
-
const
|
|
355967
|
+
const rawText = await readElementAttribute(sessionId, elementId, "text");
|
|
355968
|
+
const text = volatileText ? void 0 : rawText;
|
|
355801
355969
|
if (!resourceId && !contentDesc && !text) return null;
|
|
355802
355970
|
const bundle = {
|
|
355803
355971
|
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
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
|
|
@@ -2258,7 +2319,9 @@ function mobileLocatorBundleHasIdentity(bundle) {
|
|
|
2258
2319
|
return false;
|
|
2259
2320
|
if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
|
|
2260
2321
|
return true;
|
|
2261
|
-
if (nonEmpty(bundle.text)
|
|
2322
|
+
if (nonEmpty(bundle.text) && !isStateValueMobileClassName(bundle.className))
|
|
2323
|
+
return true;
|
|
2324
|
+
if (nonEmpty(bundle.contentDesc))
|
|
2262
2325
|
return true;
|
|
2263
2326
|
const xpath = nonEmpty(bundle.xpath);
|
|
2264
2327
|
return !!xpath && isIdentityMobileXpath(xpath);
|
|
@@ -2279,23 +2342,27 @@ function selectorCandidatesForStep(step) {
|
|
|
2279
2342
|
candidates.push(`~${bundle.accessibilityId}`);
|
|
2280
2343
|
if (nonEmpty(bundle.resourceId))
|
|
2281
2344
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2345
|
+
if (nonEmpty(bundle.xpath))
|
|
2346
|
+
candidates.push(bundle.xpath);
|
|
2282
2347
|
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2283
|
-
const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2348
|
+
const textIdentity = (isGateStep || !isEditableMobileClassName(bundle.className)) && !isStateValueMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2284
2349
|
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2285
2350
|
const trimmed = nonEmpty(value);
|
|
2286
2351
|
if (trimmed)
|
|
2287
2352
|
candidates.push(identityTextXpath(trimmed));
|
|
2288
2353
|
}
|
|
2289
|
-
if (nonEmpty(bundle.xpath))
|
|
2290
|
-
candidates.push(bundle.xpath);
|
|
2291
2354
|
}
|
|
2292
2355
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2356
|
+
const bundleContributedIdentity = candidates.some((value) => !(value.startsWith("/") || value.startsWith("(//")) || isIdentityMobileXpath(value));
|
|
2293
2357
|
const pushValueCandidates = (value) => {
|
|
2294
2358
|
candidates.push(selectorForValue(value));
|
|
2295
2359
|
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2296
2360
|
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2297
2361
|
candidates.push(`id=${value}`);
|
|
2298
2362
|
}
|
|
2363
|
+
if (!bundleContributedIdentity && !/^(~|id=|css=|\/|\()/.test(value)) {
|
|
2364
|
+
candidates.push(identityTextXpath(value));
|
|
2365
|
+
}
|
|
2299
2366
|
};
|
|
2300
2367
|
if (nonEmpty(target.primary))
|
|
2301
2368
|
pushValueCandidates(target.primary);
|
|
@@ -2990,7 +3057,8 @@ function isWebViewStep(step) {
|
|
|
2990
3057
|
function hasCrossPlatformIdentity(bundle) {
|
|
2991
3058
|
if (!bundle)
|
|
2992
3059
|
return false;
|
|
2993
|
-
|
|
3060
|
+
const durableText = isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text;
|
|
3061
|
+
return Boolean(durableText?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
2994
3062
|
}
|
|
2995
3063
|
function stepIsDivergentForPlatform(step, platform3) {
|
|
2996
3064
|
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
@@ -3072,7 +3140,8 @@ function summarizeMobileReadiness(steps, authoringPlatform) {
|
|
|
3072
3140
|
function hasStableNativeIdentity(bundle) {
|
|
3073
3141
|
if (!bundle)
|
|
3074
3142
|
return false;
|
|
3075
|
-
|
|
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));
|
|
3076
3145
|
}
|
|
3077
3146
|
function isPositionalXpath(xpath) {
|
|
3078
3147
|
const value = xpath?.trim();
|
|
@@ -3152,6 +3221,7 @@ var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS, XPATH_ATTRIBUTE_EQUALITY_RE;
|
|
|
3152
3221
|
var init_mobile_cross_platform = __esm({
|
|
3153
3222
|
"../shared/dist/mobile-cross-platform.js"() {
|
|
3154
3223
|
"use strict";
|
|
3224
|
+
init_mobile_test_generation();
|
|
3155
3225
|
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
3156
3226
|
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
3157
3227
|
"tap",
|
|
@@ -6154,6 +6224,7 @@ __export(dist_exports, {
|
|
|
6154
6224
|
isEditableMobileClassName: () => isEditableMobileClassName,
|
|
6155
6225
|
isEmailFromIdentity: () => isEmailFromIdentity,
|
|
6156
6226
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
6227
|
+
isIdentityMobileXpath: () => isIdentityMobileXpath,
|
|
6157
6228
|
isMobilePlatform: () => isMobilePlatform,
|
|
6158
6229
|
isPricingPlan: () => isPricingPlan,
|
|
6159
6230
|
isRunnableStep: () => isRunnableStep,
|
|
@@ -6161,6 +6232,7 @@ __export(dist_exports, {
|
|
|
6161
6232
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
6162
6233
|
isSensitiveQueryKey: () => isSensitiveQueryKey,
|
|
6163
6234
|
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
6235
|
+
isStateValueMobileClassName: () => isStateValueMobileClassName,
|
|
6164
6236
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
6165
6237
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6166
6238
|
isTrialExpired: () => isTrialExpired,
|
|
@@ -7068,6 +7140,7 @@ var require_mobile_observation = __commonJS({
|
|
|
7068
7140
|
exports.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
|
|
7069
7141
|
exports.extractNativeMobileLandmarks = extractNativeMobileLandmarks;
|
|
7070
7142
|
exports.safeMobileElementLabel = safeMobileElementLabel;
|
|
7143
|
+
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
7071
7144
|
var DEFAULT_MAX_ELEMENTS = 60;
|
|
7072
7145
|
var MAX_LABEL_LENGTH = 80;
|
|
7073
7146
|
var EVIDENCE_LABEL_LIMIT = 40;
|
|
@@ -7158,7 +7231,8 @@ var require_mobile_observation = __commonJS({
|
|
|
7158
7231
|
const locators = [];
|
|
7159
7232
|
if (stableId)
|
|
7160
7233
|
locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
|
|
7161
|
-
const
|
|
7234
|
+
const platform3 = element.role.startsWith("XCUIElementType") ? "IOS" : "ANDROID";
|
|
7235
|
+
const xpath = (0, mobile_source_parser_js_1.buildLocatorBundle)(element, platform3).xpath?.trim();
|
|
7162
7236
|
if (xpath)
|
|
7163
7237
|
locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
|
|
7164
7238
|
const collected = {
|
|
@@ -78407,6 +78481,10 @@ var require_discover_ai_planner = __commonJS({
|
|
|
78407
78481
|
if (context.appBrief) {
|
|
78408
78482
|
sections.push(`## Application Brief
|
|
78409
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)}`);
|
|
78410
78488
|
}
|
|
78411
78489
|
sections.push(`## Base URL
|
|
78412
78490
|
${compressed.baseUrl}`);
|
|
@@ -92288,6 +92366,15 @@ var require_agentic_discovery_prompts = __commonJS({
|
|
|
92288
92366
|
lines.push(input.appBrief.trim());
|
|
92289
92367
|
lines.push("");
|
|
92290
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
|
+
}
|
|
92291
92378
|
if (input.credentials && input.credentials.length > 0) {
|
|
92292
92379
|
lines.push("## Test credentials (throwaway accounts)");
|
|
92293
92380
|
lines.push("");
|
|
@@ -328955,6 +329042,15 @@ var require_agentic_survey_prompts = __commonJS({
|
|
|
328955
329042
|
lines.push(input.appBrief.trim());
|
|
328956
329043
|
lines.push("");
|
|
328957
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
|
+
}
|
|
328958
329054
|
if (input.credentials && input.credentials.length > 0) {
|
|
328959
329055
|
lines.push("## Test credentials (throwaway accounts)");
|
|
328960
329056
|
lines.push("");
|
|
@@ -329582,6 +329678,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
329582
329678
|
const briefInput = {
|
|
329583
329679
|
baseUrl: ctx.baseUrl,
|
|
329584
329680
|
appBrief: ctx.appBrief,
|
|
329681
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
329585
329682
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
329586
329683
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
329587
329684
|
focusAreas: ctx.focusAreas,
|
|
@@ -331277,6 +331374,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
331277
331374
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
331278
331375
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
331279
331376
|
appBrief: ctx.appBrief,
|
|
331377
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
331280
331378
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
331281
331379
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
331282
331380
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -332214,6 +332312,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
332214
332312
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
332215
332313
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
332216
332314
|
appBrief: ctx.appBrief,
|
|
332315
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
332217
332316
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
332218
332317
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
332219
332318
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -333305,6 +333404,7 @@ ${preview}`);
|
|
|
333305
333404
|
const briefInput = {
|
|
333306
333405
|
baseUrl: ctx.baseUrl,
|
|
333307
333406
|
appBrief: ctx.appBrief,
|
|
333407
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
333308
333408
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
333309
333409
|
existingFeatureNames: ctx.existingFeatureNames ?? [],
|
|
333310
333410
|
focusAreas: ctx.focusAreas,
|
|
@@ -334164,6 +334264,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
334164
334264
|
featureObservations: ctx.featureContext?.flowIntel,
|
|
334165
334265
|
notesForTestAuthor: ctx.featureContext?.priorInsight,
|
|
334166
334266
|
appBrief: ctx.appBrief,
|
|
334267
|
+
knownFactsBlock: ctx.knowledgeContext,
|
|
334167
334268
|
credentials: credentials.length > 0 ? credentials : void 0,
|
|
334168
334269
|
testVariableKeys: testVariableKeys.length > 0 ? testVariableKeys : void 0,
|
|
334169
334270
|
existingTestNames: ctx.existingTestNames ?? [],
|
|
@@ -336018,7 +336119,8 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
336018
336119
|
// scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
|
|
336019
336120
|
// auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
|
|
336020
336121
|
emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
|
|
336021
|
-
testBudget
|
|
336122
|
+
testBudget,
|
|
336123
|
+
knowledgeContext: ctx.knowledgeContext
|
|
336022
336124
|
}, {
|
|
336023
336125
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
336024
336126
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -336130,6 +336232,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
336130
336232
|
interactionLog: exploreResult.interactionLog,
|
|
336131
336233
|
modelOverride: ctx.generateModel,
|
|
336132
336234
|
appBrief: ctx.appBrief,
|
|
336235
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
336133
336236
|
onAICall,
|
|
336134
336237
|
onProgress,
|
|
336135
336238
|
mcpClient: mcpClient ?? void 0,
|
|
@@ -336609,7 +336712,8 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
336609
336712
|
// configured inbox), so the planner never promises what generation can't do.
|
|
336610
336713
|
// Scoped to AUTH features: a non-auth feature has no signup surface to test.
|
|
336611
336714
|
emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
|
|
336612
|
-
testBudget: featureTestBudget
|
|
336715
|
+
testBudget: featureTestBudget,
|
|
336716
|
+
knowledgeContext: ctx.knowledgeContext
|
|
336613
336717
|
}, {
|
|
336614
336718
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
336615
336719
|
gateModelOverride: ctx.planGateModel,
|
|
@@ -336655,6 +336759,7 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
336655
336759
|
interactionLog: exploreResult.interactionLog,
|
|
336656
336760
|
modelOverride: ctx.generateModel,
|
|
336657
336761
|
appBrief: ctx.appBrief,
|
|
336762
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
336658
336763
|
onAICall,
|
|
336659
336764
|
mcpClient: mcpClient ?? void 0,
|
|
336660
336765
|
// Pooled browser for parallel scenario generation (Fargate opt-in via
|
|
@@ -347868,6 +347973,12 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347868
347973
|
"Use {{TEST_USER_EMAIL}}/{{TEST_USER_PASSWORD}} for credentials. Include",
|
|
347869
347974
|
"assertVisible or assertText unless tagging @needs-review. When an incremental",
|
|
347870
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.",
|
|
347871
347982
|
"Write strictly valid JSON."
|
|
347872
347983
|
].join("\n");
|
|
347873
347984
|
}
|
|
@@ -347885,6 +347996,32 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347885
347996
|
function isPlainObject2(value) {
|
|
347886
347997
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
347887
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
|
+
}
|
|
347888
348025
|
function isValidStep(value) {
|
|
347889
348026
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
347890
348027
|
return false;
|
|
@@ -347936,6 +348073,13 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347936
348073
|
if (!o.steps.every(isValidStep)) {
|
|
347937
348074
|
return { ok: false, reason: "steps: invalid step shape" };
|
|
347938
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
|
+
}
|
|
347939
348083
|
const steps = o.steps.map((step, order) => ({
|
|
347940
348084
|
...step,
|
|
347941
348085
|
order,
|
|
@@ -351229,7 +351373,7 @@ var require_package4 = __commonJS({
|
|
|
351229
351373
|
"package.json"(exports, module) {
|
|
351230
351374
|
module.exports = {
|
|
351231
351375
|
name: "@validate.qa/runner",
|
|
351232
|
-
version: "1.0.
|
|
351376
|
+
version: "1.0.68",
|
|
351233
351377
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351234
351378
|
bin: {
|
|
351235
351379
|
"validate-runner": "dist/cli.js"
|
|
@@ -354700,7 +354844,7 @@ function boundsForRun(step, platform3, isCrossPlatform) {
|
|
|
354700
354844
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
354701
354845
|
return boundsFromStep(step);
|
|
354702
354846
|
}
|
|
354703
|
-
function locatorStrategyForValue(value) {
|
|
354847
|
+
function locatorStrategyForValue(value, platform3) {
|
|
354704
354848
|
const trimmed = value?.trim();
|
|
354705
354849
|
if (!trimmed) {
|
|
354706
354850
|
return [];
|
|
@@ -354716,7 +354860,8 @@ function locatorStrategyForValue(value) {
|
|
|
354716
354860
|
}
|
|
354717
354861
|
return [
|
|
354718
354862
|
{ using: "accessibility id", value: trimmed },
|
|
354719
|
-
{ using: "id", value: trimmed }
|
|
354863
|
+
{ using: "id", value: trimmed },
|
|
354864
|
+
...textIdentityStrategies(trimmed, platform3)
|
|
354720
354865
|
];
|
|
354721
354866
|
}
|
|
354722
354867
|
function buildLocatorStrategies(target, platform3) {
|
|
@@ -354726,19 +354871,19 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354726
354871
|
if (bundle) {
|
|
354727
354872
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354728
354873
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354874
|
+
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354729
354875
|
for (const value of [
|
|
354730
|
-
isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354876
|
+
isEditableMobileClassName(bundle.className) || isStateValueMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354731
354877
|
bundle.contentDesc
|
|
354732
354878
|
]) {
|
|
354733
354879
|
const trimmed = value?.trim();
|
|
354734
|
-
if (trimmed) strategies.push(
|
|
354880
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354735
354881
|
}
|
|
354736
|
-
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354737
354882
|
}
|
|
354738
354883
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354739
|
-
strategies.push(...locatorStrategyForValue(target.primary));
|
|
354884
|
+
strategies.push(...locatorStrategyForValue(target.primary, platform3));
|
|
354740
354885
|
for (const fallback of target.fallbacks ?? []) {
|
|
354741
|
-
strategies.push(...locatorStrategyForValue(fallback));
|
|
354886
|
+
strategies.push(...locatorStrategyForValue(fallback, platform3));
|
|
354742
354887
|
}
|
|
354743
354888
|
}
|
|
354744
354889
|
return strategies.filter((strategy) => strategy.using !== "xpath" || isIdentityXpath(strategy.value)).filter(
|
|
@@ -354760,12 +354905,31 @@ function identityTextXpath2(value) {
|
|
|
354760
354905
|
const lit = xpathLiteral3(value);
|
|
354761
354906
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354762
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
|
+
}
|
|
354763
354926
|
function isIdentityXpath(value) {
|
|
354764
354927
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
354765
354928
|
}
|
|
354766
354929
|
function isIdentityStrategy(strategy) {
|
|
354767
354930
|
if (strategy.using === "accessibility id" || strategy.using === "id") return true;
|
|
354768
354931
|
if (strategy.using === "xpath") return isIdentityXpath(strategy.value);
|
|
354932
|
+
if (strategy.using === "-ios predicate string") return isIdentityIosPredicate(strategy.value);
|
|
354769
354933
|
return false;
|
|
354770
354934
|
}
|
|
354771
354935
|
function buildIdentityLocatorStrategies(target, platform3) {
|
|
@@ -354775,15 +354939,18 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354775
354939
|
if (bundle) {
|
|
354776
354940
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354777
354941
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354778
|
-
|
|
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
|
+
]) {
|
|
354779
354947
|
const trimmed = value?.trim();
|
|
354780
|
-
if (trimmed) strategies.push(
|
|
354948
|
+
if (trimmed) strategies.push(...textIdentityStrategies(trimmed, platform3));
|
|
354781
354949
|
}
|
|
354782
|
-
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354783
354950
|
}
|
|
354784
354951
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
354785
354952
|
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
354786
|
-
for (const strategy of locatorStrategyForValue(value)) {
|
|
354953
|
+
for (const strategy of locatorStrategyForValue(value, platform3)) {
|
|
354787
354954
|
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
354788
354955
|
}
|
|
354789
354956
|
}
|
|
@@ -355778,8 +355945,7 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355778
355945
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355779
355946
|
if (platform3 === "IOS") {
|
|
355780
355947
|
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355781
|
-
|
|
355782
|
-
if (text) return identityTextXpath2(text);
|
|
355948
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355783
355949
|
return "//*";
|
|
355784
355950
|
}
|
|
355785
355951
|
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
@@ -355789,14 +355955,15 @@ function harvestFallbackXpath(platform3, attrs) {
|
|
|
355789
355955
|
}
|
|
355790
355956
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
355791
355957
|
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
355958
|
+
const volatileText = isEditableMobileClassName(className) || isStateValueMobileClassName(className);
|
|
355792
355959
|
if (platform3 === "IOS") {
|
|
355793
355960
|
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
355794
355961
|
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
355795
355962
|
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
355796
|
-
const text2 = label ?? value;
|
|
355963
|
+
const text2 = label ?? (volatileText ? void 0 : value);
|
|
355797
355964
|
if (!name && !text2) return null;
|
|
355798
355965
|
const bundle2 = {
|
|
355799
|
-
xpath: harvestFallbackXpath("IOS", { name,
|
|
355966
|
+
xpath: harvestFallbackXpath("IOS", { name, text: text2 }),
|
|
355800
355967
|
className
|
|
355801
355968
|
};
|
|
355802
355969
|
if (name) bundle2.accessibilityId = name;
|
|
@@ -355805,7 +355972,8 @@ async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
|
355805
355972
|
}
|
|
355806
355973
|
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
355807
355974
|
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
355808
|
-
const
|
|
355975
|
+
const rawText = await readElementAttribute(sessionId, elementId, "text");
|
|
355976
|
+
const text = volatileText ? void 0 : rawText;
|
|
355809
355977
|
if (!resourceId && !contentDesc && !text) return null;
|
|
355810
355978
|
const bundle = {
|
|
355811
355979
|
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
package/package.json
CHANGED