@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.
Files changed (3) hide show
  1. package/dist/cli.js +221 -53
  2. package/dist/cli.mjs +221 -53
  3. 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 = value && value !== label ? value : labelAttr && labelAttr !== label ? labelAttr : void 0;
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
- if (a11y) {
1265
- return platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]`;
1266
- }
1267
- const rid = nonBlank(el.resourceId);
1268
- if (platform3 === "ANDROID" && rid)
1269
- return `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]`;
1270
- const text = nonBlank(el.text);
1271
- if (text && !isEditableElement(el)) {
1272
- return platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]`;
1273
- }
1274
- return void 0;
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
- if (el.accessibilityId)
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 (platform3 === "ANDROID" && el.resourceId)
1344
+ if (includeRid && el.resourceId)
1292
1345
  bundle.resourceId = el.resourceId;
1293
- if (el.text && !isEditableElement(el))
1294
- bundle.text = el.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
- if (el.accessibilityId)
1301
- return el.accessibilityId;
1302
- if (el.resourceId)
1303
- return el.resourceId;
1304
- if (el.text && !isEditableElement(el))
1305
- return el.text;
1306
- return el.xpath;
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). Static
2099
- // elements keep their text fallback.
2100
- isEditableMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
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) || nonEmpty(bundle.contentDesc))
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
- return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
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
- return Boolean(bundle.accessibilityId?.trim() || bundle.resourceId?.trim() || bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.xpath?.trim() && XPATH_ATTRIBUTE_EQUALITY_RE.test(bundle.xpath) && !/(?:contains|starts-with)\s*\(/i.test(bundle.xpath));
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 xpath = element.xpath?.trim();
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.67",
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({ using: "xpath", value: identityTextXpath2(trimmed) });
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
- for (const value of [bundle.text, bundle.contentDesc]) {
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({ using: "xpath", value: identityTextXpath2(trimmed) });
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
- const text = attrs.label ?? attrs.value;
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, label, value }),
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 text = await readElementAttribute(sessionId, elementId, "text");
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 = value && value !== label ? value : labelAttr && labelAttr !== label ? labelAttr : void 0;
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
- if (a11y) {
1270
- return platform3 === "ANDROID" ? `//${cls}[@content-desc=${xpathAttrLiteral(a11y)}]` : `//${cls}[@name=${xpathAttrLiteral(a11y)}]`;
1271
- }
1272
- const rid = nonBlank(el.resourceId);
1273
- if (platform3 === "ANDROID" && rid)
1274
- return `//${cls}[@resource-id=${xpathAttrLiteral(rid)}]`;
1275
- const text = nonBlank(el.text);
1276
- if (text && !isEditableElement(el)) {
1277
- return platform3 === "ANDROID" ? `//${cls}[@text=${xpathAttrLiteral(text)}]` : `//${cls}[@label=${xpathAttrLiteral(text)}]`;
1278
- }
1279
- return void 0;
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
- if (el.accessibilityId)
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 (platform3 === "ANDROID" && el.resourceId)
1349
+ if (includeRid && el.resourceId)
1297
1350
  bundle.resourceId = el.resourceId;
1298
- if (el.text && !isEditableElement(el))
1299
- bundle.text = el.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
- if (el.accessibilityId)
1306
- return el.accessibilityId;
1307
- if (el.resourceId)
1308
- return el.resourceId;
1309
- if (el.text && !isEditableElement(el))
1310
- return el.text;
1311
- return el.xpath;
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). Static
2104
- // elements keep their text fallback.
2105
- isEditableMobileClassName(locatorBundle.className) ? void 0 : locatorBundle.text,
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) || nonEmpty(bundle.contentDesc))
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
- return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
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
- return Boolean(bundle.accessibilityId?.trim() || bundle.resourceId?.trim() || bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.xpath?.trim() && XPATH_ATTRIBUTE_EQUALITY_RE.test(bundle.xpath) && !/(?:contains|starts-with)\s*\(/i.test(bundle.xpath));
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 xpath = element.xpath?.trim();
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.67",
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({ using: "xpath", value: identityTextXpath2(trimmed) });
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
- for (const value of [bundle.text, bundle.contentDesc]) {
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({ using: "xpath", value: identityTextXpath2(trimmed) });
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
- const text = attrs.label ?? attrs.value;
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, label, value }),
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 text = await readElementAttribute(sessionId, elementId, "text");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
4
4
  "description": "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
5
5
  "bin": {
6
6
  "validate-runner": "dist/cli.js"