@validate.qa/runner 1.0.84 → 1.0.85

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 +209 -34
  2. package/dist/cli.mjs +209 -34
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1019,7 +1019,8 @@ var require_mobile_source_parser = __commonJS({
1019
1019
  const isGenericView = /android\.view\.View$/i.test(role);
1020
1020
  const moderateHitTarget = hasArea && bounds.width >= 24 && bounds.height >= 24 && bounds.width <= 2e3 && bounds.height <= 1800;
1021
1021
  const webHybridLabelledControl = isGenericView && hasHumanIdentity && moderateHitTarget && (focusable || clickable || longClickable || Boolean(contentDesc));
1022
- const actionable = enabled && hasArea && (clickable || longClickable || classActionable || focusable && classActionable || focusable && hasHumanIdentity && !isGenericView || webHybridLabelledControl);
1022
+ const isAnrChrome = typeof resourceId === "string" && /(?:android:id\/)?aerr_/i.test(resourceId) || typeof text === "string" && /isn['’]?t responding/i.test(text) || typeof label === "string" && /isn['’]?t responding/i.test(label);
1023
+ const actionable = !isAnrChrome && enabled && hasArea && (clickable || longClickable || classActionable || focusable && classActionable || focusable && hasHumanIdentity && !isGenericView || webHybridLabelledControl);
1023
1024
  return {
1024
1025
  role,
1025
1026
  label,
@@ -1197,6 +1198,7 @@ var require_mobile_source_parser = __commonJS({
1197
1198
  element.secure = fields.secure;
1198
1199
  if (fields.enabled !== void 0)
1199
1200
  element.enabled = fields.enabled;
1201
+ element.visible = fields.visible;
1200
1202
  if (isCredibleStructuralNavigationTarget(element) && chainHasNativeNavigationSemantics(chain, platform3)) {
1201
1203
  markNavigation(element, "native-navigation-semantics");
1202
1204
  }
@@ -1571,6 +1573,8 @@ var require_mobile_boundary = __commonJS({
1571
1573
  "../runner-core/dist/services/mobile/mobile-boundary.js"(exports2) {
1572
1574
  "use strict";
1573
1575
  Object.defineProperty(exports2, "__esModule", { value: true });
1576
+ exports2.isAndroidAnrSystemResourceId = isAndroidAnrSystemResourceId;
1577
+ exports2.looksLikeAndroidAnrUi = looksLikeAndroidAnrUi;
1574
1578
  exports2.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
1575
1579
  exports2.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
1576
1580
  exports2.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
@@ -1588,6 +1592,28 @@ var require_mobile_boundary = __commonJS({
1588
1592
  "com.miui.securitycenter",
1589
1593
  "com.samsung.android.permissioncontroller"
1590
1594
  ]);
1595
+ var ANDROID_ANR_RESOURCE_ID_RE = /^(?:android:id\/)?(?:aerr_(?:wait|close|close_app|mute|report|restart)|alertTitle|button1|button2|button3|parentPanel|topPanel|contentPanel|customPanel|buttonPanel|scrollView|textSpacerNoButtons|textSpacerNoTitle)$/i;
1596
+ var ANDROID_ANR_TEXT_RE = /isn['’]?t responding|isn['’]?t responding\.|anr|close app|wait/i;
1597
+ function isAndroidAnrSystemResourceId(resourceId) {
1598
+ const rid = resourceId?.trim();
1599
+ if (!rid)
1600
+ return false;
1601
+ if (/(?:android:id\/)?aerr_/i.test(rid))
1602
+ return true;
1603
+ return ANDROID_ANR_RESOURCE_ID_RE.test(rid) && /aerr_/i.test(rid);
1604
+ }
1605
+ function looksLikeAndroidAnrUi(input) {
1606
+ if (isAndroidAnrSystemResourceId(input.resourceId))
1607
+ return true;
1608
+ const rid = input.resourceId?.trim() ?? "";
1609
+ const blob = [input.text, input.label, input.contentDesc].filter(Boolean).join(" ");
1610
+ if (/(?:android:id\/)?(?:alertTitle|aerr_)/i.test(rid) && ANDROID_ANR_TEXT_RE.test(blob)) {
1611
+ return true;
1612
+ }
1613
+ if (/isn['’]?t responding/i.test(blob))
1614
+ return true;
1615
+ return false;
1616
+ }
1591
1617
  var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
1592
1618
  "com.apple.springboard",
1593
1619
  "com.apple.authenticationservicesui",
@@ -1745,7 +1771,7 @@ var init_constants = __esm({
1745
1771
  MOBILE_APP_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,253}[A-Za-z0-9])?$/;
1746
1772
  MOBILE_FOCUS_SETTLE_MS = 150;
1747
1773
  MOBILE_POST_ACTION_SETTLE_MS = 1800;
1748
- MOBILE_COLD_BOOT_SETTLE_MS = 12e3;
1774
+ MOBILE_COLD_BOOT_SETTLE_MS = 25e3;
1749
1775
  MOBILE_DEFAULT_SWIPE_DISTANCE = 500;
1750
1776
  MOBILE_MAX_SWIPE_DISTANCE = 2e3;
1751
1777
  SELECTOR_PRIORITIES = [
@@ -74887,7 +74913,7 @@ var require_mobile_explorer = __commonJS({
74887
74913
  type: "function",
74888
74914
  function: {
74889
74915
  name: "mobile_relaunch",
74890
- description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds). Use between exploration episodes when you want to start fresh. Re-snapshot afterward.",
74916
+ description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds). Only for CLEAR_APP_DATA / cold flows. Blocked when app data is being preserved (PRESERVE_APP_DATA or survey AUTO). Do not use on a working product screen. Re-snapshot afterward.",
74891
74917
  parameters: { type: "object", properties: {} }
74892
74918
  }
74893
74919
  };
@@ -75687,11 +75713,11 @@ Rules:
75687
75713
  **Phase 2 \u2014 Logged-in breadth first (~35% of budget).** Once signed in, find the primary navigation (bottom tab bar, drawer, or menu) and visit EVERY top-level destination ONCE, observing each screen, BEFORE drilling into any of them. A survey that maps one tab deeply but never opens the other tabs has failed \u2014 breadth across all tabs/drawer entries is mandatory.
75688
75714
  **Phase 3 \u2014 Depth (remaining budget).** Return to each observed tab and explore its sub-screens: open ONE list row per list (rows share a template), open menus/sheets/dialogs, observe detail screens, and mobile_swipe up/down to reveal off-screen content. record_transition whenever you move between screens.
75689
75715
 
75690
- Rules that apply throughout: snapshots and post-action observations capture screen evidence automatically. Use capture_screen sparingly to name/classify important stable destinations (use screenType "LANDING" for welcome/onboarding/intro and "AUTH" for sign-in/sign-up), but never spend turns renaming already-observed structural variants. Record transitions on every move; mobile_back to return; mobile_relaunch only for a clean restart between episodes. When no new screens or safe navigation controls remain, call finish_exploration.
75716
+ Rules that apply throughout: snapshots and post-action observations capture screen evidence automatically. Use capture_screen sparingly to name/classify important stable destinations (use screenType "LANDING" for welcome/onboarding/intro and "AUTH" for sign-in/sign-up), but never spend turns renaming already-observed structural variants. Record transitions on every move; mobile_back to return. Prefer staying on the current app instance \u2014 do not cold-relaunch unless the product UI is truly stuck after a long wait. When no new screens or safe navigation controls remain, call finish_exploration.
75691
75717
 
75692
75718
  ## Boundaries (survey = map, do not mutate)
75693
75719
  - Do NOT submit forms, save, delete, purchase, or send anything. You MAY open modals/menus to catalog them, then dismiss.
75694
- - Stay inside this app.${targetLine} Do not leave to the system home screen or other apps (mobile_home backgrounds \u2014 avoid it). If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back or mobile_relaunch.
75720
+ - Stay inside this app.${targetLine} Do not leave to the system home screen or other apps (mobile_home backgrounds \u2014 avoid it). If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back (not a cold relaunch).
75695
75721
  - If you hit a login wall and have no credentials, capture the auth screen and explore what is reachable.
75696
75722
 
75697
75723
  Max iterations: ${maxIterations}. You will be warned near the end.`;
@@ -76102,6 +76128,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
76102
76128
  const kickoff = buildKickoffMessage(mode);
76103
76129
  const recentExchanges = [];
76104
76130
  const transientDirectives = [];
76131
+ const resetPolicyForTools = ctx.target?.resetPolicy ?? "AUTO";
76132
+ const preserveAppDataForTools = resetPolicyForTools === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicyForTools === "AUTO";
76105
76133
  const tools = [
76106
76134
  MOBILE_SNAPSHOT_TOOL,
76107
76135
  MOBILE_TAP_TOOL,
@@ -76111,7 +76139,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
76111
76139
  MOBILE_SWIPE_TOOL,
76112
76140
  MOBILE_BACK_TOOL,
76113
76141
  MOBILE_HOME_TOOL,
76114
- MOBILE_RELAUNCH_TOOL,
76142
+ // Omit relaunch under preserve — model otherwise cold-restarts RN into splash.
76143
+ ...preserveAppDataForTools ? [] : [MOBILE_RELAUNCH_TOOL],
76115
76144
  MOBILE_OPEN_DEEPLINK_TOOL,
76116
76145
  CAPTURE_SCREEN_TOOL,
76117
76146
  RECORD_TRANSITION_TOOL,
@@ -76467,7 +76496,8 @@ Exploration complete: ${summary}`);
76467
76496
  credentials: credentialSecrets,
76468
76497
  redactionCredentials: credentialValuesForRedaction,
76469
76498
  authTracker,
76470
- inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null
76499
+ inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null,
76500
+ allowRelaunch: !preserveAppDataForTools
76471
76501
  });
76472
76502
  pushResult(handled);
76473
76503
  const handledRecord = handled && typeof handled === "object" ? handled : null;
@@ -76560,7 +76590,7 @@ Exploration complete: ${summary}`);
76560
76590
  return result;
76561
76591
  }
76562
76592
  async function dispatchMobileTool(args) {
76563
- const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox } = args;
76593
+ const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox, allowRelaunch } = args;
76564
76594
  const absorbActionResult = (actionResult, source, preferCredentialSecondary = false) => {
76565
76595
  (0, mobile_driver_errors_js_1.throwIfTerminalMobileActionFailure)(actionResult, source);
76566
76596
  const resolved = buildResolved(actionResult.source, latest()?.screen.windowSize ?? null, actionResult.screenSignal);
@@ -76963,6 +76993,13 @@ Exploration complete: ${summary}`);
76963
76993
  }
76964
76994
  // ----- Relaunch (hermetic reset) -----
76965
76995
  case "mobile_relaunch": {
76996
+ if (!allowRelaunch) {
76997
+ return {
76998
+ ok: false,
76999
+ error: "mobile_relaunch is blocked while app data is being preserved. Use mobile_back / mobile_snapshot to continue mapping; request CLEAR_APP_DATA for a true cold restart.",
77000
+ note: "Relaunch refused under preserve policy."
77001
+ };
77002
+ }
76966
77003
  authTracker?.noteInterveningMutation(iteration);
76967
77004
  await driver.relaunch();
76968
77005
  log2(" Relaunched app for hermetic reset.");
@@ -80255,7 +80292,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
80255
80292
  5. CRUD screens for the same entity belong together
80256
80293
  6. Do NOT merge screens into one feature just because they serve a similar broader goal. Treat areas as separate features when the evidence shows separate primary navigation destinations (e.g. separate tab-bar items or drawer links), separate screen families for distinct entities, or different CRUD entities/forms. Keep screens together when they share one navigational area and operate on the same entity (e.g. a list screen, its filter/sort sheet, and its detail screen all belong together).
80257
80294
  7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
80258
- 8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature.
80295
+ 8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature. Never name a feature after a scrub/placeholder token such as "<number>", "<identity>", "<email>", or a bare number \u2014 those are privacy redactions of dynamic content, not product features. Attach those screens to the nearest real product feature instead.
80259
80296
  9. COVER EVERY PRODUCT SCREEN: every explored product screen except generic error / permission / empty / loading / intermediate screens MUST appear in exactly one feature group's routes. Do not leave usable screens ungrouped \u2014 a screen the explorer reached is part of SOME feature. Attach an ambiguous usable screen to its most closely related feature and note the ambiguity in that feature's description. Sign-in / sign-up / forgot-password screens form an "Authentication" group even when they were only reached via links from other screens.
80260
80297
  10. LOGGED-OUT vs LOGGED-IN SEPARATION: screens shown BEFORE the user is signed in (splash, landing/welcome, theme or language pickers, walkthroughs, sign-up, sign-in, forgot-password, demo/preview screens) must NEVER share a feature group with the signed-in app. Put sign-in / sign-up / forgot-password in "Authentication" (rule 4) and the remaining pre-login screens in an "Onboarding" group. Features named after signed-in areas (feeds, dashboards, tabs, settings) must contain ONLY screens reached after authentication. A pre-login flow is a linear sequence, not a navigation destination \u2014 do not let it swallow the app's main features.
80261
80298
  11. OUTPUT SIZE: screen-ids are long opaque values. List each one exactly once in the routes array, but do NOT repeat them in pages or flows. Omit pages and flows; the harness deterministically reconstructs those fields from the captured graph. This is mandatory for large maps so the JSON is not truncated.
@@ -80300,6 +80337,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
80300
80337
  Example - GOOD happy path (generic, async): \`["Launch app and sign in if needed", "Navigate to the feature screen", "Note the current count of rows in the list", "Tap the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the row count increased by 1", "Open the newly created row", "Verify its content is non-empty and contains the input I provided"]\`
80301
80338
  Example - GOOD happy path (sync CRUD): \`["Launch app and sign in if needed", "Navigate to the list screen", "Tap New", "Fill required fields", "Tap Save", "Verify the new record appears in the list", "Tap the record and verify its detail screen shows the values I entered"]\`
80302
80339
  13. **Auth happy paths require concrete session proof.** A sign-in HAPPY_PATH is incomplete if it only proves "user reaches the home screen" or "an authenticated screen is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: the account/profile tab opens, Sign out is available, the observed display name/email/avatar is present, an authenticated screen remains reachable after a relaunch with the session restored, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Sign out", "Account", "Profile", or the user's own displayed name/email) over generic phrases like "personalized content." If the explorer only observed a screen transition and no signed-in signal, keep the screen outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
80340
+ 13b. **Session-mode match \u2014 no guest-wall scenarios when the explore session was authenticated.** If the explorer evidence shows authenticated product screens (requiresAuth / post-login provenance / preserved-session product), do NOT plan scenarios that expect a guest-only gate after a product action (e.g. "Like prompts Sign In", "Add to playlist requires Log In", "Download requires Sign In"). Those gates do not appear for the signed-in session the runner will use to re-execute. Prefer the authenticated outcome actually observed (action succeeds, toggle flips to Unlike, sheet/picker opens, share sheet appears). Guest-wall / sign-in-prompt scenarios are valid ONLY when the explorer evidence for THAT action was recorded while signed out, or the scenario is an Authentication feature flow.
80303
80341
  14. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and against an app launch state that may be freshly cleared OR may preserve a previous session depending on the device/reset policy. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
80304
80342
  15. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation already in the account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the home screen header is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Tap New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
80305
80343
  16. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first row", "the latest message", "the last cell", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs against a fresh session or a different account.
@@ -80454,10 +80492,26 @@ Each entry is exactly one of:
80454
80492
  function screenDisplayName(state2) {
80455
80493
  return state2.screenName?.trim() || state2.screenId;
80456
80494
  }
80495
+ function isScrubbedPlaceholderName(value) {
80496
+ const trimmed = value.trim();
80497
+ if (!trimmed)
80498
+ return false;
80499
+ if (/^<[^<>\s]+>$/.test(trimmed))
80500
+ return true;
80501
+ if (/^(number|identity|email|id|url|token|value|secret|date|time)$/i.test(trimmed)) {
80502
+ return true;
80503
+ }
80504
+ const stripped = trimmed.replace(/<[^<>\s]+>/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
80505
+ if (!stripped)
80506
+ return true;
80507
+ return /^(number|identity|email|id|url|token|value|secret|date|time)(\s+(number|identity|email|id|url|token|value|secret|date|time))*$/i.test(stripped);
80508
+ }
80457
80509
  function isOpaqueMobileScreenName(value, appId) {
80458
80510
  const trimmed = value.trim();
80459
80511
  if (!trimmed)
80460
80512
  return false;
80513
+ if (isScrubbedPlaceholderName(trimmed))
80514
+ return true;
80461
80515
  const lower = trimmed.toLowerCase();
80462
80516
  const lowerAppId = appId?.trim().toLowerCase();
80463
80517
  if (lowerAppId && (lower === lowerAppId || lower.startsWith(`${lowerAppId}#`) || lower.startsWith(`${lowerAppId}.`))) {
@@ -80479,11 +80533,12 @@ Each entry is exactly one of:
80479
80533
  }
80480
80534
  function nonGenericScreenName(state2, appId) {
80481
80535
  const raw = state2.screenName?.trim();
80482
- if (!raw || isOpaqueMobileScreenName(raw, appId))
80536
+ if (!raw || isOpaqueMobileScreenName(raw, appId) || isScrubbedPlaceholderName(raw))
80483
80537
  return null;
80484
80538
  const human = humanizeToken(raw.replace(/(?:Activity|ViewController|Fragment|Screen|Page)$/i, ""));
80485
- if (!human || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human))
80539
+ if (!human || isScrubbedPlaceholderName(human) || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human)) {
80486
80540
  return null;
80541
+ }
80487
80542
  return human;
80488
80543
  }
80489
80544
  function inferMobileFeatureGroupName(states, appId) {
@@ -81212,6 +81267,45 @@ ${constraints.join("\n")}`);
81212
81267
  return finalGroups;
81213
81268
  }
81214
81269
  var WEB_URL_PREFIX_FALLBACK_LOG_RE = /\bFallback:\s*\d+\s+feature groups? from URL-prefix grouping\b/i;
81270
+ function graphHasAuthenticatedProduct(graph) {
81271
+ for (const state2 of graph.screens.values()) {
81272
+ if (state2.requiresAuth === true)
81273
+ return true;
81274
+ if (state2.authProvenance === "post-submit-authenticated" || state2.authProvenance === "preserved-session-product") {
81275
+ return true;
81276
+ }
81277
+ }
81278
+ return false;
81279
+ }
81280
+ function isGuestWallScenario(scenario) {
81281
+ const feature = (scenario.featureGroup ?? "").toLowerCase();
81282
+ if (/auth|sign[\s-]?in|log[\s-]?in|onboarding/.test(feature))
81283
+ return false;
81284
+ const stepText = (scenario.steps ?? []).map((step) => typeof step === "string" ? step : step.action ?? "").join(" ");
81285
+ const outcomeText = (scenario.expectedOutcomes ?? []).map((outcome) => outcome.description ?? "").join(" ");
81286
+ const blob = [scenario.name, scenario.goal, stepText, outcomeText].filter(Boolean).join("\n").toLowerCase();
81287
+ if (/prompt(?:s)?\s+sign[\s-]?in|sign[\s-]?in\s+to\s+\w+|requires?\s+(?:log|sign)[\s-]?in|must\s+(?:log|sign)[\s-]?in|guest\s+(?:only|wall|gate|mode)|log[\s-]?in\s+to\s+(?:continue|like|save|download|comment|follow|share)/i.test(blob)) {
81288
+ return true;
81289
+ }
81290
+ const productAction = /\b(like|unlike|download|playlist|comment|follow|share|clip|save|favorite|bookmark|subscribe|purchase|buy|checkout)\b/i.test(blob);
81291
+ const assertsLoginGate = /\b(?:assert|verify|expect|see|show|display|prompts?)\b[\s\S]{0,40}\b(?:log[\s-]?in|sign[\s-]?in)\b/i.test(blob) || /\b(?:log[\s-]?in|sign[\s-]?in)\b[\s\S]{0,40}\b(?:dialog|sheet|modal|prompt|gate|wall|required)\b/i.test(blob);
81292
+ return productAction && assertsLoginGate;
81293
+ }
81294
+ function filterGuestWallScenariosForAuthSession(scenarios, graph) {
81295
+ if (scenarios.length === 0 || !graphHasAuthenticatedProduct(graph)) {
81296
+ return { scenarios, dropped: 0 };
81297
+ }
81298
+ const kept = [];
81299
+ let dropped = 0;
81300
+ for (const scenario of scenarios) {
81301
+ if (isGuestWallScenario(scenario)) {
81302
+ dropped += 1;
81303
+ continue;
81304
+ }
81305
+ kept.push(scenario);
81306
+ }
81307
+ return { scenarios: kept, dropped };
81308
+ }
81215
81309
  function selectMobilePlannerFeatureGroups(result, graph, appId, mode, existingFeatureNames = []) {
81216
81310
  if (mode !== "survey") {
81217
81311
  return { accepted: result.featureGroups, rejected: [] };
@@ -81325,7 +81419,12 @@ ${constraints.join("\n")}`);
81325
81419
  }
81326
81420
  const plannerFeatureGroups = filterFeatureGroupsToSiteMap(plannerGroupsForFiltering, siteMap, input.graph, input.appId, input.mode === "survey");
81327
81421
  const mobileFeatureGroups = plannerFeatureGroups.length > 0 ? plannerFeatureGroups : buildMobileFeatureGroupsFromGraph(input.graph, input.appId);
81328
- if (input.mode !== "survey" && result.scenarios.length === 0 && input.graph.screens.size > 0) {
81422
+ const guestWallFilter = input.mode !== "survey" ? filterGuestWallScenariosForAuthSession(result.scenarios, input.graph) : { scenarios: result.scenarios, dropped: 0 };
81423
+ if (guestWallFilter.dropped > 0) {
81424
+ mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
81425
+ }
81426
+ const plannedScenarios = guestWallFilter.scenarios;
81427
+ if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
81329
81428
  const fallbackScenarios = buildMobileFallbackScenarios(input);
81330
81429
  if (fallbackScenarios.length > 0) {
81331
81430
  return {
@@ -81344,6 +81443,7 @@ ${constraints.join("\n")}`);
81344
81443
  if (input.featureName?.trim()) {
81345
81444
  return {
81346
81445
  ...result,
81446
+ scenarios: plannedScenarios,
81347
81447
  featureGroups: mobileFeatureGroups,
81348
81448
  logs: [...result.logs, ...mobileAdaptationLogs],
81349
81449
  warning: [result.warning, `Deep discovery could not plan any scenarios for "${input.featureName}" \u2014 the explorer did not reach that feature's screens this run. If the feature requires a signed-in session, verify TEST_USER_EMAIL / TEST_USER_PASSWORD are valid for this app, then re-run.`].filter(Boolean).join(" | "),
@@ -81380,6 +81480,7 @@ ${constraints.join("\n")}`);
81380
81480
  }
81381
81481
  return {
81382
81482
  ...result,
81483
+ scenarios: plannedScenarios,
81383
81484
  featureGroups: mobileFeatureGroups,
81384
81485
  logs: [...result.logs, ...mobileAdaptationLogs]
81385
81486
  };
@@ -82025,7 +82126,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
82025
82126
  }
82026
82127
  }
82027
82128
  async function runScenarioLoop(args) {
82028
- const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog } = args;
82129
+ const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
82029
82130
  const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
82030
82131
  const actions = [];
82031
82132
  let assertCount = 0;
@@ -82599,6 +82700,7 @@ ${statePacket}`, redactionCredentials) },
82599
82700
  setLatest,
82600
82701
  credentials,
82601
82702
  screenName: () => lastScreenName || "UnknownScreen",
82703
+ allowRelaunch,
82602
82704
  recordAction: (action) => {
82603
82705
  const screenHash = getLatest()?.signal.screenHash;
82604
82706
  const structuralKey = structuralScreenKey(getLatest());
@@ -82633,7 +82735,7 @@ ${statePacket}`, redactionCredentials) },
82633
82735
  return { actions, assertCount, finish, lastSnapshot: lastTargetSnapshot, lastScreenName: lastTargetScreenName };
82634
82736
  }
82635
82737
  async function dispatchGenerateTool(args) {
82636
- const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
82738
+ const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, allowRelaunch, recordAction, recordedActions, log: log2 } = args;
82637
82739
  const unknownRef = (ref) => ({
82638
82740
  ok: false,
82639
82741
  error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
@@ -83023,6 +83125,16 @@ ${statePacket}`, redactionCredentials) },
83023
83125
  return actionPayload(result, absorbed);
83024
83126
  }
83025
83127
  case "mobile_relaunch": {
83128
+ if (!allowRelaunch) {
83129
+ log2(" relaunch refused under PRESERVE_APP_DATA \u2014 staying on current app instance.");
83130
+ const snap2 = await driver.snapshot();
83131
+ const absorbed2 = await absorbObservation(snap2.xml, snap2.screenSignal, snap2.windowSize, snap2.screenshot, "mobile_relaunch_blocked");
83132
+ return {
83133
+ ok: false,
83134
+ error: "mobile_relaunch is blocked while app data is preserved. Continue from the current screen.",
83135
+ observation: absorbed2.observation
83136
+ };
83137
+ }
83026
83138
  await driver.relaunch();
83027
83139
  const snap = await driver.snapshot();
83028
83140
  const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
@@ -83241,14 +83353,48 @@ ${statePacket}`, redactionCredentials) },
83241
83353
  });
83242
83354
  return { ok: true, label: metadata.targetLabel ?? accessibilityId ?? text ?? "element" };
83243
83355
  }
83356
+ var AMBIENT_CHROME_LABEL_RE = /^(your\s*profile|you|home|search|profile|more|settings|notifications?|tab\s*\d+(\s*of\s*\d+)?|you,\s*tab.*)$/i;
83357
+ function isDisplayableLandingCandidate(el, windowSize) {
83358
+ if (el.visible === false)
83359
+ return false;
83360
+ const { x, y, width, height } = el.bounds;
83361
+ if (!(width > 0 && height > 0))
83362
+ return false;
83363
+ if (width < 8 || height < 8)
83364
+ return false;
83365
+ if (windowSize && windowSize.width > 0 && windowSize.height > 0) {
83366
+ const centerX = x + width / 2;
83367
+ const centerY = y + height / 2;
83368
+ if (centerX < 0 || centerY < 0 || centerX > windowSize.width || centerY > windowSize.height) {
83369
+ return false;
83370
+ }
83371
+ if (width >= windowSize.width * 0.92 && height >= windowSize.height * 0.55) {
83372
+ return false;
83373
+ }
83374
+ }
83375
+ return true;
83376
+ }
83377
+ function landingAnchorSortKey(el) {
83378
+ const label = (nonEmpty3(el.label) || nonEmpty3(el.text) || "").trim();
83379
+ const ambient = AMBIENT_CHROME_LABEL_RE.test(label) ? 1 : 0;
83380
+ const labelled = nonEmpty3(el.text) || nonEmpty3(el.label) ? 0 : 1;
83381
+ const area = Math.max(1, el.bounds.width * el.bounds.height);
83382
+ const areaBucket = area < 8e4 ? 0 : area < 25e4 ? 1 : 2;
83383
+ return [ambient, labelled, areaBucket];
83384
+ }
83244
83385
  function pickLandingAnchor(snapshot) {
83245
83386
  if (!snapshot || snapshot.screen.elements.length === 0)
83246
83387
  return null;
83247
83388
  const elements = snapshot.screen.elements;
83248
- const stableAnchors = elements.filter((el) => hasStableId(el) && !weakAssertionTargetReason(el));
83249
- stableAnchors.sort((a, b) => {
83250
- const labelled2 = (el) => nonEmpty3(el.text) || nonEmpty3(el.label) ? 0 : 1;
83251
- return labelled2(a) - labelled2(b);
83389
+ const windowSize = snapshot.screen.windowSize ?? null;
83390
+ const stableAnchors = elements.filter((el) => hasStableId(el) && !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)).sort((a, b) => {
83391
+ const ka = landingAnchorSortKey(a);
83392
+ const kb = landingAnchorSortKey(b);
83393
+ for (let i = 0; i < ka.length; i += 1) {
83394
+ if (ka[i] !== kb[i])
83395
+ return ka[i] - kb[i];
83396
+ }
83397
+ return 0;
83252
83398
  });
83253
83399
  const contentfulNonButton = stableAnchors.find((el) => !/button/i.test(el.role) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
83254
83400
  if (contentfulNonButton)
@@ -83258,8 +83404,10 @@ ${statePacket}`, redactionCredentials) },
83258
83404
  return nonButton;
83259
83405
  if (stableAnchors.length > 0)
83260
83406
  return stableAnchors[0];
83261
- const labelled = elements.find((el) => !weakAssertionTargetReason(el) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
83262
- return labelled ?? elements.find((el) => !weakAssertionTargetReason(el)) ?? null;
83407
+ const labelled = elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
83408
+ if (labelled)
83409
+ return labelled;
83410
+ return elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)) ?? null;
83263
83411
  }
83264
83412
  var ALWAYS_PRESENT_CONTAINER_ROLES = /^(XCUIElementTypeApplication|XCUIElementTypeWindow)$/;
83265
83413
  var ALWAYS_PRESENT_CONTAINER_ID_NAMES = /* @__PURE__ */ new Set([
@@ -83357,10 +83505,19 @@ ${statePacket}`, redactionCredentials) },
83357
83505
  const anchor = pickLandingAnchor(recording.lastSnapshot);
83358
83506
  if (!anchor)
83359
83507
  return false;
83508
+ const modelAssertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
83509
+ const anchorLabel = (nonEmpty3(anchor.label) || nonEmpty3(anchor.text) || "").trim();
83510
+ if (modelAssertCount > 0 && AMBIENT_CHROME_LABEL_RE.test(anchorLabel)) {
83511
+ return false;
83512
+ }
83513
+ const anchorSelector = (0, mobile_source_parser_js_2.preferredLocator)(anchor);
83514
+ const alreadyAsserted = actions.some((a) => (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") && typeof a.selector === "string" && a.selector === anchorSelector);
83515
+ if (alreadyAsserted)
83516
+ return false;
83360
83517
  const screen = recording.lastScreenName || "LandingScreen";
83361
83518
  actions.push(finalizeAction({
83362
83519
  type: "ASSERT_VISIBLE",
83363
- selector: (0, mobile_source_parser_js_2.preferredLocator)(anchor),
83520
+ selector: anchorSelector,
83364
83521
  metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
83365
83522
  }));
83366
83523
  return true;
@@ -84236,7 +84393,8 @@ ${statePacket}`, redactionCredentials) },
84236
84393
  targetAppId,
84237
84394
  credentials: credentialSecrets,
84238
84395
  redactionCredentials: credentialValuesForRedaction,
84239
- log: log2
84396
+ log: log2,
84397
+ allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA"
84240
84398
  });
84241
84399
  consecutiveInfraFailures = 0;
84242
84400
  const test = assembleTest({
@@ -337169,10 +337327,11 @@ Fatal error: ${msg}`);
337169
337327
  const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
337170
337328
  if (shouldPreserveAppData) {
337171
337329
  try {
337172
- await driver.relaunch();
337330
+ await driver.launchApp();
337331
+ log2("App activated with preserved data (no terminate/clear).");
337173
337332
  } catch (err) {
337174
337333
  (0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
337175
- log2(`App relaunch failed while preserving app data: ${err instanceof Error ? err.message : String(err)}`);
337334
+ log2(`App activate failed while preserving app data: ${err instanceof Error ? err.message : String(err)}`);
337176
337335
  }
337177
337336
  return false;
337178
337337
  }
@@ -353337,7 +353496,7 @@ var require_package4 = __commonJS({
353337
353496
  "package.json"(exports2, module2) {
353338
353497
  module2.exports = {
353339
353498
  name: "@validate.qa/runner",
353340
- version: "1.0.84",
353499
+ version: "1.0.85",
353341
353500
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353342
353501
  bin: {
353343
353502
  "validate-runner": "dist/cli.js"
@@ -354203,9 +354362,9 @@ async function waitForColdBootSettle(sessionPath) {
354203
354362
  timeoutMs: MOBILE_COLD_BOOT_SETTLE_MS,
354204
354363
  minStableFrames: 2,
354205
354364
  pollMs: 400,
354206
- // WebView native shell alone is ~5–8 nodes; real first screens flatten to
354207
- // dozens. Require enough nodes that the DOM has painted (mirrors executor).
354208
- minElements: 12
354365
+ // Native splash / WebView shell is ~5–8 nodes; RN product screens and rich
354366
+ // onboarding typically flatten to 15+ once JS has painted.
354367
+ minElements: 15
354209
354368
  });
354210
354369
  }
354211
354370
  async function getWindowSize(sessionPath) {
@@ -354354,20 +354513,33 @@ async function sendKeysToFocusedInput(sessionPath, value) {
354354
354513
  }
354355
354514
  async function typeText(sessionPath, value, bounds) {
354356
354515
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
354516
+ if (!elementId && bounds === void 0) {
354517
+ throw new Error("No focused element available for TYPE. Tap an input field first.");
354518
+ }
354357
354519
  if (elementId) {
354358
354520
  try {
354359
- await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
354521
+ await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
354360
354522
  method: "POST",
354361
- body: JSON.stringify({ text: value })
354523
+ body: JSON.stringify({})
354362
354524
  });
354363
- return;
354364
354525
  } catch (err) {
354365
354526
  if (isAppiumDriverDeadError(err)) throw err;
354366
354527
  }
354367
- } else if (bounds === void 0) {
354368
- throw new Error("No focused element available for TYPE. Tap an input field first.");
354369
354528
  }
354370
- await sendKeysToFocusedInput(sessionPath, value);
354529
+ try {
354530
+ await sendKeysToFocusedInput(sessionPath, value);
354531
+ return;
354532
+ } catch (err) {
354533
+ if (isAppiumDriverDeadError(err)) throw err;
354534
+ }
354535
+ if (elementId) {
354536
+ await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
354537
+ method: "POST",
354538
+ body: JSON.stringify({ text: value })
354539
+ });
354540
+ return;
354541
+ }
354542
+ throw new Error("TYPE failed: could not send keys to the focused input.");
354371
354543
  }
354372
354544
  async function submitKeyboard(sessionPath, bounds) {
354373
354545
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
@@ -359512,6 +359684,9 @@ async function startSession(validated, res) {
359512
359684
  ...deviceId ? { "appium:udid": deviceId } : {},
359513
359685
  ...platformVersion ? { "appium:platformVersion": platformVersion } : {},
359514
359686
  "appium:noReset": true,
359687
+ // Keep pace with executor createSession: Expo "Open in App?" sheets and
359688
+ // permission dialogs otherwise freeze WDA for recording/mirror sessions.
359689
+ ...process.env.APPIUM_IOS_AUTO_ACCEPT_ALERTS !== "false" ? { "appium:autoAcceptAlerts": true } : {},
359515
359690
  // Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
359516
359691
  // Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
359517
359692
  // "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
package/dist/cli.mjs CHANGED
@@ -1024,7 +1024,8 @@ var require_mobile_source_parser = __commonJS({
1024
1024
  const isGenericView = /android\.view\.View$/i.test(role);
1025
1025
  const moderateHitTarget = hasArea && bounds.width >= 24 && bounds.height >= 24 && bounds.width <= 2e3 && bounds.height <= 1800;
1026
1026
  const webHybridLabelledControl = isGenericView && hasHumanIdentity && moderateHitTarget && (focusable || clickable || longClickable || Boolean(contentDesc));
1027
- const actionable = enabled && hasArea && (clickable || longClickable || classActionable || focusable && classActionable || focusable && hasHumanIdentity && !isGenericView || webHybridLabelledControl);
1027
+ const isAnrChrome = typeof resourceId === "string" && /(?:android:id\/)?aerr_/i.test(resourceId) || typeof text === "string" && /isn['’]?t responding/i.test(text) || typeof label === "string" && /isn['’]?t responding/i.test(label);
1028
+ const actionable = !isAnrChrome && enabled && hasArea && (clickable || longClickable || classActionable || focusable && classActionable || focusable && hasHumanIdentity && !isGenericView || webHybridLabelledControl);
1028
1029
  return {
1029
1030
  role,
1030
1031
  label,
@@ -1202,6 +1203,7 @@ var require_mobile_source_parser = __commonJS({
1202
1203
  element.secure = fields.secure;
1203
1204
  if (fields.enabled !== void 0)
1204
1205
  element.enabled = fields.enabled;
1206
+ element.visible = fields.visible;
1205
1207
  if (isCredibleStructuralNavigationTarget(element) && chainHasNativeNavigationSemantics(chain, platform3)) {
1206
1208
  markNavigation(element, "native-navigation-semantics");
1207
1209
  }
@@ -1576,6 +1578,8 @@ var require_mobile_boundary = __commonJS({
1576
1578
  "../runner-core/dist/services/mobile/mobile-boundary.js"(exports) {
1577
1579
  "use strict";
1578
1580
  Object.defineProperty(exports, "__esModule", { value: true });
1581
+ exports.isAndroidAnrSystemResourceId = isAndroidAnrSystemResourceId;
1582
+ exports.looksLikeAndroidAnrUi = looksLikeAndroidAnrUi;
1579
1583
  exports.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
1580
1584
  exports.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
1581
1585
  exports.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
@@ -1593,6 +1597,28 @@ var require_mobile_boundary = __commonJS({
1593
1597
  "com.miui.securitycenter",
1594
1598
  "com.samsung.android.permissioncontroller"
1595
1599
  ]);
1600
+ var ANDROID_ANR_RESOURCE_ID_RE = /^(?:android:id\/)?(?:aerr_(?:wait|close|close_app|mute|report|restart)|alertTitle|button1|button2|button3|parentPanel|topPanel|contentPanel|customPanel|buttonPanel|scrollView|textSpacerNoButtons|textSpacerNoTitle)$/i;
1601
+ var ANDROID_ANR_TEXT_RE = /isn['’]?t responding|isn['’]?t responding\.|anr|close app|wait/i;
1602
+ function isAndroidAnrSystemResourceId(resourceId) {
1603
+ const rid = resourceId?.trim();
1604
+ if (!rid)
1605
+ return false;
1606
+ if (/(?:android:id\/)?aerr_/i.test(rid))
1607
+ return true;
1608
+ return ANDROID_ANR_RESOURCE_ID_RE.test(rid) && /aerr_/i.test(rid);
1609
+ }
1610
+ function looksLikeAndroidAnrUi(input) {
1611
+ if (isAndroidAnrSystemResourceId(input.resourceId))
1612
+ return true;
1613
+ const rid = input.resourceId?.trim() ?? "";
1614
+ const blob = [input.text, input.label, input.contentDesc].filter(Boolean).join(" ");
1615
+ if (/(?:android:id\/)?(?:alertTitle|aerr_)/i.test(rid) && ANDROID_ANR_TEXT_RE.test(blob)) {
1616
+ return true;
1617
+ }
1618
+ if (/isn['’]?t responding/i.test(blob))
1619
+ return true;
1620
+ return false;
1621
+ }
1596
1622
  var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
1597
1623
  "com.apple.springboard",
1598
1624
  "com.apple.authenticationservicesui",
@@ -1750,7 +1776,7 @@ var init_constants = __esm({
1750
1776
  MOBILE_APP_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,253}[A-Za-z0-9])?$/;
1751
1777
  MOBILE_FOCUS_SETTLE_MS = 150;
1752
1778
  MOBILE_POST_ACTION_SETTLE_MS = 1800;
1753
- MOBILE_COLD_BOOT_SETTLE_MS = 12e3;
1779
+ MOBILE_COLD_BOOT_SETTLE_MS = 25e3;
1754
1780
  MOBILE_DEFAULT_SWIPE_DISTANCE = 500;
1755
1781
  MOBILE_MAX_SWIPE_DISTANCE = 2e3;
1756
1782
  SELECTOR_PRIORITIES = [
@@ -74892,7 +74918,7 @@ var require_mobile_explorer = __commonJS({
74892
74918
  type: "function",
74893
74919
  function: {
74894
74920
  name: "mobile_relaunch",
74895
- description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds). Use between exploration episodes when you want to start fresh. Re-snapshot afterward.",
74921
+ description: "Terminate and relaunch the app for a clean, hermetic state (sessions are noReset=true, so Home only backgrounds). Only for CLEAR_APP_DATA / cold flows. Blocked when app data is being preserved (PRESERVE_APP_DATA or survey AUTO). Do not use on a working product screen. Re-snapshot afterward.",
74896
74922
  parameters: { type: "object", properties: {} }
74897
74923
  }
74898
74924
  };
@@ -75692,11 +75718,11 @@ Rules:
75692
75718
  **Phase 2 \u2014 Logged-in breadth first (~35% of budget).** Once signed in, find the primary navigation (bottom tab bar, drawer, or menu) and visit EVERY top-level destination ONCE, observing each screen, BEFORE drilling into any of them. A survey that maps one tab deeply but never opens the other tabs has failed \u2014 breadth across all tabs/drawer entries is mandatory.
75693
75719
  **Phase 3 \u2014 Depth (remaining budget).** Return to each observed tab and explore its sub-screens: open ONE list row per list (rows share a template), open menus/sheets/dialogs, observe detail screens, and mobile_swipe up/down to reveal off-screen content. record_transition whenever you move between screens.
75694
75720
 
75695
- Rules that apply throughout: snapshots and post-action observations capture screen evidence automatically. Use capture_screen sparingly to name/classify important stable destinations (use screenType "LANDING" for welcome/onboarding/intro and "AUTH" for sign-in/sign-up), but never spend turns renaming already-observed structural variants. Record transitions on every move; mobile_back to return; mobile_relaunch only for a clean restart between episodes. When no new screens or safe navigation controls remain, call finish_exploration.
75721
+ Rules that apply throughout: snapshots and post-action observations capture screen evidence automatically. Use capture_screen sparingly to name/classify important stable destinations (use screenType "LANDING" for welcome/onboarding/intro and "AUTH" for sign-in/sign-up), but never spend turns renaming already-observed structural variants. Record transitions on every move; mobile_back to return. Prefer staying on the current app instance \u2014 do not cold-relaunch unless the product UI is truly stuck after a long wait. When no new screens or safe navigation controls remain, call finish_exploration.
75696
75722
 
75697
75723
  ## Boundaries (survey = map, do not mutate)
75698
75724
  - Do NOT submit forms, save, delete, purchase, or send anything. You MAY open modals/menus to catalog them, then dismiss.
75699
- - Stay inside this app.${targetLine} Do not leave to the system home screen or other apps (mobile_home backgrounds \u2014 avoid it). If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back or mobile_relaunch.
75725
+ - Stay inside this app.${targetLine} Do not leave to the system home screen or other apps (mobile_home backgrounds \u2014 avoid it). If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back (not a cold relaunch).
75700
75726
  - If you hit a login wall and have no credentials, capture the auth screen and explore what is reachable.
75701
75727
 
75702
75728
  Max iterations: ${maxIterations}. You will be warned near the end.`;
@@ -76107,6 +76133,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
76107
76133
  const kickoff = buildKickoffMessage(mode);
76108
76134
  const recentExchanges = [];
76109
76135
  const transientDirectives = [];
76136
+ const resetPolicyForTools = ctx.target?.resetPolicy ?? "AUTO";
76137
+ const preserveAppDataForTools = resetPolicyForTools === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicyForTools === "AUTO";
76110
76138
  const tools = [
76111
76139
  MOBILE_SNAPSHOT_TOOL,
76112
76140
  MOBILE_TAP_TOOL,
@@ -76116,7 +76144,8 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
76116
76144
  MOBILE_SWIPE_TOOL,
76117
76145
  MOBILE_BACK_TOOL,
76118
76146
  MOBILE_HOME_TOOL,
76119
- MOBILE_RELAUNCH_TOOL,
76147
+ // Omit relaunch under preserve — model otherwise cold-restarts RN into splash.
76148
+ ...preserveAppDataForTools ? [] : [MOBILE_RELAUNCH_TOOL],
76120
76149
  MOBILE_OPEN_DEEPLINK_TOOL,
76121
76150
  CAPTURE_SCREEN_TOOL,
76122
76151
  RECORD_TRANSITION_TOOL,
@@ -76472,7 +76501,8 @@ Exploration complete: ${summary}`);
76472
76501
  credentials: credentialSecrets,
76473
76502
  redactionCredentials: credentialValuesForRedaction,
76474
76503
  authTracker,
76475
- inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null
76504
+ inbox: inboxEnabled && inboxState && inboxReader ? { state: inboxState, reader: inboxReader } : null,
76505
+ allowRelaunch: !preserveAppDataForTools
76476
76506
  });
76477
76507
  pushResult(handled);
76478
76508
  const handledRecord = handled && typeof handled === "object" ? handled : null;
@@ -76565,7 +76595,7 @@ Exploration complete: ${summary}`);
76565
76595
  return result;
76566
76596
  }
76567
76597
  async function dispatchMobileTool(args) {
76568
- const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox } = args;
76598
+ const { toolName, toolArgs, iteration, driver, state: state2, enableRecordJourney, latest, buildResolved, absorbResolvedObservation, resolveRef, resolveHybridRef, currentBoundary, collectedTransitions, counters, journeys, log: log2, credentials, redactionCredentials, authTracker, inbox, allowRelaunch } = args;
76569
76599
  const absorbActionResult = (actionResult, source, preferCredentialSecondary = false) => {
76570
76600
  (0, mobile_driver_errors_js_1.throwIfTerminalMobileActionFailure)(actionResult, source);
76571
76601
  const resolved = buildResolved(actionResult.source, latest()?.screen.windowSize ?? null, actionResult.screenSignal);
@@ -76968,6 +76998,13 @@ Exploration complete: ${summary}`);
76968
76998
  }
76969
76999
  // ----- Relaunch (hermetic reset) -----
76970
77000
  case "mobile_relaunch": {
77001
+ if (!allowRelaunch) {
77002
+ return {
77003
+ ok: false,
77004
+ error: "mobile_relaunch is blocked while app data is being preserved. Use mobile_back / mobile_snapshot to continue mapping; request CLEAR_APP_DATA for a true cold restart.",
77005
+ note: "Relaunch refused under preserve policy."
77006
+ };
77007
+ }
76971
77008
  authTracker?.noteInterveningMutation(iteration);
76972
77009
  await driver.relaunch();
76973
77010
  log2(" Relaunched app for hermetic reset.");
@@ -80260,7 +80297,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
80260
80297
  5. CRUD screens for the same entity belong together
80261
80298
  6. Do NOT merge screens into one feature just because they serve a similar broader goal. Treat areas as separate features when the evidence shows separate primary navigation destinations (e.g. separate tab-bar items or drawer links), separate screen families for distinct entities, or different CRUD entities/forms. Keep screens together when they share one navigational area and operate on the same entity (e.g. a list screen, its filter/sort sheet, and its detail screen all belong together).
80262
80299
  7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
80263
- 8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature.
80300
+ 8. Do NOT create standalone features for generic error / empty / loading / intermediate screens (network-error, permission-denied, blank WebView, spinner-only loading). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description. If a screen has no actionable controls and only says it is empty/loading/intermediate, omit it from featureGroups.routes unless it is the only evidence for a real product feature. Never name a feature after a scrub/placeholder token such as "<number>", "<identity>", "<email>", or a bare number \u2014 those are privacy redactions of dynamic content, not product features. Attach those screens to the nearest real product feature instead.
80264
80301
  9. COVER EVERY PRODUCT SCREEN: every explored product screen except generic error / permission / empty / loading / intermediate screens MUST appear in exactly one feature group's routes. Do not leave usable screens ungrouped \u2014 a screen the explorer reached is part of SOME feature. Attach an ambiguous usable screen to its most closely related feature and note the ambiguity in that feature's description. Sign-in / sign-up / forgot-password screens form an "Authentication" group even when they were only reached via links from other screens.
80265
80302
  10. LOGGED-OUT vs LOGGED-IN SEPARATION: screens shown BEFORE the user is signed in (splash, landing/welcome, theme or language pickers, walkthroughs, sign-up, sign-in, forgot-password, demo/preview screens) must NEVER share a feature group with the signed-in app. Put sign-in / sign-up / forgot-password in "Authentication" (rule 4) and the remaining pre-login screens in an "Onboarding" group. Features named after signed-in areas (feeds, dashboards, tabs, settings) must contain ONLY screens reached after authentication. A pre-login flow is a linear sequence, not a navigation destination \u2014 do not let it swallow the app's main features.
80266
80303
  11. OUTPUT SIZE: screen-ids are long opaque values. List each one exactly once in the routes array, but do NOT repeat them in pages or flows. Omit pages and flows; the harness deterministically reconstructs those fields from the captured graph. This is mandatory for large maps so the JSON is not truncated.
@@ -80305,6 +80342,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
80305
80342
  Example - GOOD happy path (generic, async): \`["Launch app and sign in if needed", "Navigate to the feature screen", "Note the current count of rows in the list", "Tap the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the row count increased by 1", "Open the newly created row", "Verify its content is non-empty and contains the input I provided"]\`
80306
80343
  Example - GOOD happy path (sync CRUD): \`["Launch app and sign in if needed", "Navigate to the list screen", "Tap New", "Fill required fields", "Tap Save", "Verify the new record appears in the list", "Tap the record and verify its detail screen shows the values I entered"]\`
80307
80344
  13. **Auth happy paths require concrete session proof.** A sign-in HAPPY_PATH is incomplete if it only proves "user reaches the home screen" or "an authenticated screen is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: the account/profile tab opens, Sign out is available, the observed display name/email/avatar is present, an authenticated screen remains reachable after a relaunch with the session restored, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Sign out", "Account", "Profile", or the user's own displayed name/email) over generic phrases like "personalized content." If the explorer only observed a screen transition and no signed-in signal, keep the screen outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
80345
+ 13b. **Session-mode match \u2014 no guest-wall scenarios when the explore session was authenticated.** If the explorer evidence shows authenticated product screens (requiresAuth / post-login provenance / preserved-session product), do NOT plan scenarios that expect a guest-only gate after a product action (e.g. "Like prompts Sign In", "Add to playlist requires Log In", "Download requires Sign In"). Those gates do not appear for the signed-in session the runner will use to re-execute. Prefer the authenticated outcome actually observed (action succeeds, toggle flips to Unlike, sheet/picker opens, share sheet appears). Guest-wall / sign-in-prompt scenarios are valid ONLY when the explorer evidence for THAT action was recorded while signed out, or the scenario is an Authentication feature flow.
80308
80346
  14. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and against an app launch state that may be freshly cleared OR may preserve a previous session depending on the device/reset policy. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
80309
80347
  15. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation already in the account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the home screen header is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Tap New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
80310
80348
  16. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first row", "the latest message", "the last cell", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs against a fresh session or a different account.
@@ -80459,10 +80497,26 @@ Each entry is exactly one of:
80459
80497
  function screenDisplayName(state2) {
80460
80498
  return state2.screenName?.trim() || state2.screenId;
80461
80499
  }
80500
+ function isScrubbedPlaceholderName(value) {
80501
+ const trimmed = value.trim();
80502
+ if (!trimmed)
80503
+ return false;
80504
+ if (/^<[^<>\s]+>$/.test(trimmed))
80505
+ return true;
80506
+ if (/^(number|identity|email|id|url|token|value|secret|date|time)$/i.test(trimmed)) {
80507
+ return true;
80508
+ }
80509
+ const stripped = trimmed.replace(/<[^<>\s]+>/g, " ").replace(/[^a-z0-9]+/gi, " ").trim();
80510
+ if (!stripped)
80511
+ return true;
80512
+ return /^(number|identity|email|id|url|token|value|secret|date|time)(\s+(number|identity|email|id|url|token|value|secret|date|time))*$/i.test(stripped);
80513
+ }
80462
80514
  function isOpaqueMobileScreenName(value, appId) {
80463
80515
  const trimmed = value.trim();
80464
80516
  if (!trimmed)
80465
80517
  return false;
80518
+ if (isScrubbedPlaceholderName(trimmed))
80519
+ return true;
80466
80520
  const lower = trimmed.toLowerCase();
80467
80521
  const lowerAppId = appId?.trim().toLowerCase();
80468
80522
  if (lowerAppId && (lower === lowerAppId || lower.startsWith(`${lowerAppId}#`) || lower.startsWith(`${lowerAppId}.`))) {
@@ -80484,11 +80538,12 @@ Each entry is exactly one of:
80484
80538
  }
80485
80539
  function nonGenericScreenName(state2, appId) {
80486
80540
  const raw = state2.screenName?.trim();
80487
- if (!raw || isOpaqueMobileScreenName(raw, appId))
80541
+ if (!raw || isOpaqueMobileScreenName(raw, appId) || isScrubbedPlaceholderName(raw))
80488
80542
  return null;
80489
80543
  const human = humanizeToken(raw.replace(/(?:Activity|ViewController|Fragment|Screen|Page)$/i, ""));
80490
- if (!human || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human))
80544
+ if (!human || isScrubbedPlaceholderName(human) || /^(main|root|launch|screen|page|activity|view controller)$/i.test(human)) {
80491
80545
  return null;
80546
+ }
80492
80547
  return human;
80493
80548
  }
80494
80549
  function inferMobileFeatureGroupName(states, appId) {
@@ -81217,6 +81272,45 @@ ${constraints.join("\n")}`);
81217
81272
  return finalGroups;
81218
81273
  }
81219
81274
  var WEB_URL_PREFIX_FALLBACK_LOG_RE = /\bFallback:\s*\d+\s+feature groups? from URL-prefix grouping\b/i;
81275
+ function graphHasAuthenticatedProduct(graph) {
81276
+ for (const state2 of graph.screens.values()) {
81277
+ if (state2.requiresAuth === true)
81278
+ return true;
81279
+ if (state2.authProvenance === "post-submit-authenticated" || state2.authProvenance === "preserved-session-product") {
81280
+ return true;
81281
+ }
81282
+ }
81283
+ return false;
81284
+ }
81285
+ function isGuestWallScenario(scenario) {
81286
+ const feature = (scenario.featureGroup ?? "").toLowerCase();
81287
+ if (/auth|sign[\s-]?in|log[\s-]?in|onboarding/.test(feature))
81288
+ return false;
81289
+ const stepText = (scenario.steps ?? []).map((step) => typeof step === "string" ? step : step.action ?? "").join(" ");
81290
+ const outcomeText = (scenario.expectedOutcomes ?? []).map((outcome) => outcome.description ?? "").join(" ");
81291
+ const blob = [scenario.name, scenario.goal, stepText, outcomeText].filter(Boolean).join("\n").toLowerCase();
81292
+ if (/prompt(?:s)?\s+sign[\s-]?in|sign[\s-]?in\s+to\s+\w+|requires?\s+(?:log|sign)[\s-]?in|must\s+(?:log|sign)[\s-]?in|guest\s+(?:only|wall|gate|mode)|log[\s-]?in\s+to\s+(?:continue|like|save|download|comment|follow|share)/i.test(blob)) {
81293
+ return true;
81294
+ }
81295
+ const productAction = /\b(like|unlike|download|playlist|comment|follow|share|clip|save|favorite|bookmark|subscribe|purchase|buy|checkout)\b/i.test(blob);
81296
+ const assertsLoginGate = /\b(?:assert|verify|expect|see|show|display|prompts?)\b[\s\S]{0,40}\b(?:log[\s-]?in|sign[\s-]?in)\b/i.test(blob) || /\b(?:log[\s-]?in|sign[\s-]?in)\b[\s\S]{0,40}\b(?:dialog|sheet|modal|prompt|gate|wall|required)\b/i.test(blob);
81297
+ return productAction && assertsLoginGate;
81298
+ }
81299
+ function filterGuestWallScenariosForAuthSession(scenarios, graph) {
81300
+ if (scenarios.length === 0 || !graphHasAuthenticatedProduct(graph)) {
81301
+ return { scenarios, dropped: 0 };
81302
+ }
81303
+ const kept = [];
81304
+ let dropped = 0;
81305
+ for (const scenario of scenarios) {
81306
+ if (isGuestWallScenario(scenario)) {
81307
+ dropped += 1;
81308
+ continue;
81309
+ }
81310
+ kept.push(scenario);
81311
+ }
81312
+ return { scenarios: kept, dropped };
81313
+ }
81220
81314
  function selectMobilePlannerFeatureGroups(result, graph, appId, mode, existingFeatureNames = []) {
81221
81315
  if (mode !== "survey") {
81222
81316
  return { accepted: result.featureGroups, rejected: [] };
@@ -81330,7 +81424,12 @@ ${constraints.join("\n")}`);
81330
81424
  }
81331
81425
  const plannerFeatureGroups = filterFeatureGroupsToSiteMap(plannerGroupsForFiltering, siteMap, input.graph, input.appId, input.mode === "survey");
81332
81426
  const mobileFeatureGroups = plannerFeatureGroups.length > 0 ? plannerFeatureGroups : buildMobileFeatureGroupsFromGraph(input.graph, input.appId);
81333
- if (input.mode !== "survey" && result.scenarios.length === 0 && input.graph.screens.size > 0) {
81427
+ const guestWallFilter = input.mode !== "survey" ? filterGuestWallScenariosForAuthSession(result.scenarios, input.graph) : { scenarios: result.scenarios, dropped: 0 };
81428
+ if (guestWallFilter.dropped > 0) {
81429
+ mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
81430
+ }
81431
+ const plannedScenarios = guestWallFilter.scenarios;
81432
+ if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
81334
81433
  const fallbackScenarios = buildMobileFallbackScenarios(input);
81335
81434
  if (fallbackScenarios.length > 0) {
81336
81435
  return {
@@ -81349,6 +81448,7 @@ ${constraints.join("\n")}`);
81349
81448
  if (input.featureName?.trim()) {
81350
81449
  return {
81351
81450
  ...result,
81451
+ scenarios: plannedScenarios,
81352
81452
  featureGroups: mobileFeatureGroups,
81353
81453
  logs: [...result.logs, ...mobileAdaptationLogs],
81354
81454
  warning: [result.warning, `Deep discovery could not plan any scenarios for "${input.featureName}" \u2014 the explorer did not reach that feature's screens this run. If the feature requires a signed-in session, verify TEST_USER_EMAIL / TEST_USER_PASSWORD are valid for this app, then re-run.`].filter(Boolean).join(" | "),
@@ -81385,6 +81485,7 @@ ${constraints.join("\n")}`);
81385
81485
  }
81386
81486
  return {
81387
81487
  ...result,
81488
+ scenarios: plannedScenarios,
81388
81489
  featureGroups: mobileFeatureGroups,
81389
81490
  logs: [...result.logs, ...mobileAdaptationLogs]
81390
81491
  };
@@ -82030,7 +82131,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
82030
82131
  }
82031
82132
  }
82032
82133
  async function runScenarioLoop(args) {
82033
- const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog } = args;
82134
+ const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
82034
82135
  const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
82035
82136
  const actions = [];
82036
82137
  let assertCount = 0;
@@ -82604,6 +82705,7 @@ ${statePacket}`, redactionCredentials) },
82604
82705
  setLatest,
82605
82706
  credentials,
82606
82707
  screenName: () => lastScreenName || "UnknownScreen",
82708
+ allowRelaunch,
82607
82709
  recordAction: (action) => {
82608
82710
  const screenHash = getLatest()?.signal.screenHash;
82609
82711
  const structuralKey = structuralScreenKey(getLatest());
@@ -82638,7 +82740,7 @@ ${statePacket}`, redactionCredentials) },
82638
82740
  return { actions, assertCount, finish, lastSnapshot: lastTargetSnapshot, lastScreenName: lastTargetScreenName };
82639
82741
  }
82640
82742
  async function dispatchGenerateTool(args) {
82641
- const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
82743
+ const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, allowRelaunch, recordAction, recordedActions, log: log2 } = args;
82642
82744
  const unknownRef = (ref) => ({
82643
82745
  ok: false,
82644
82746
  error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
@@ -83028,6 +83130,16 @@ ${statePacket}`, redactionCredentials) },
83028
83130
  return actionPayload(result, absorbed);
83029
83131
  }
83030
83132
  case "mobile_relaunch": {
83133
+ if (!allowRelaunch) {
83134
+ log2(" relaunch refused under PRESERVE_APP_DATA \u2014 staying on current app instance.");
83135
+ const snap2 = await driver.snapshot();
83136
+ const absorbed2 = await absorbObservation(snap2.xml, snap2.screenSignal, snap2.windowSize, snap2.screenshot, "mobile_relaunch_blocked");
83137
+ return {
83138
+ ok: false,
83139
+ error: "mobile_relaunch is blocked while app data is preserved. Continue from the current screen.",
83140
+ observation: absorbed2.observation
83141
+ };
83142
+ }
83031
83143
  await driver.relaunch();
83032
83144
  const snap = await driver.snapshot();
83033
83145
  const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
@@ -83246,14 +83358,48 @@ ${statePacket}`, redactionCredentials) },
83246
83358
  });
83247
83359
  return { ok: true, label: metadata.targetLabel ?? accessibilityId ?? text ?? "element" };
83248
83360
  }
83361
+ var AMBIENT_CHROME_LABEL_RE = /^(your\s*profile|you|home|search|profile|more|settings|notifications?|tab\s*\d+(\s*of\s*\d+)?|you,\s*tab.*)$/i;
83362
+ function isDisplayableLandingCandidate(el, windowSize) {
83363
+ if (el.visible === false)
83364
+ return false;
83365
+ const { x, y, width, height } = el.bounds;
83366
+ if (!(width > 0 && height > 0))
83367
+ return false;
83368
+ if (width < 8 || height < 8)
83369
+ return false;
83370
+ if (windowSize && windowSize.width > 0 && windowSize.height > 0) {
83371
+ const centerX = x + width / 2;
83372
+ const centerY = y + height / 2;
83373
+ if (centerX < 0 || centerY < 0 || centerX > windowSize.width || centerY > windowSize.height) {
83374
+ return false;
83375
+ }
83376
+ if (width >= windowSize.width * 0.92 && height >= windowSize.height * 0.55) {
83377
+ return false;
83378
+ }
83379
+ }
83380
+ return true;
83381
+ }
83382
+ function landingAnchorSortKey(el) {
83383
+ const label = (nonEmpty3(el.label) || nonEmpty3(el.text) || "").trim();
83384
+ const ambient = AMBIENT_CHROME_LABEL_RE.test(label) ? 1 : 0;
83385
+ const labelled = nonEmpty3(el.text) || nonEmpty3(el.label) ? 0 : 1;
83386
+ const area = Math.max(1, el.bounds.width * el.bounds.height);
83387
+ const areaBucket = area < 8e4 ? 0 : area < 25e4 ? 1 : 2;
83388
+ return [ambient, labelled, areaBucket];
83389
+ }
83249
83390
  function pickLandingAnchor(snapshot) {
83250
83391
  if (!snapshot || snapshot.screen.elements.length === 0)
83251
83392
  return null;
83252
83393
  const elements = snapshot.screen.elements;
83253
- const stableAnchors = elements.filter((el) => hasStableId(el) && !weakAssertionTargetReason(el));
83254
- stableAnchors.sort((a, b) => {
83255
- const labelled2 = (el) => nonEmpty3(el.text) || nonEmpty3(el.label) ? 0 : 1;
83256
- return labelled2(a) - labelled2(b);
83394
+ const windowSize = snapshot.screen.windowSize ?? null;
83395
+ const stableAnchors = elements.filter((el) => hasStableId(el) && !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)).sort((a, b) => {
83396
+ const ka = landingAnchorSortKey(a);
83397
+ const kb = landingAnchorSortKey(b);
83398
+ for (let i = 0; i < ka.length; i += 1) {
83399
+ if (ka[i] !== kb[i])
83400
+ return ka[i] - kb[i];
83401
+ }
83402
+ return 0;
83257
83403
  });
83258
83404
  const contentfulNonButton = stableAnchors.find((el) => !/button/i.test(el.role) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
83259
83405
  if (contentfulNonButton)
@@ -83263,8 +83409,10 @@ ${statePacket}`, redactionCredentials) },
83263
83409
  return nonButton;
83264
83410
  if (stableAnchors.length > 0)
83265
83411
  return stableAnchors[0];
83266
- const labelled = elements.find((el) => !weakAssertionTargetReason(el) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
83267
- return labelled ?? elements.find((el) => !weakAssertionTargetReason(el)) ?? null;
83412
+ const labelled = elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize) && (nonEmpty3(el.label) || nonEmpty3(el.text)));
83413
+ if (labelled)
83414
+ return labelled;
83415
+ return elements.find((el) => !weakAssertionTargetReason(el) && isDisplayableLandingCandidate(el, windowSize)) ?? null;
83268
83416
  }
83269
83417
  var ALWAYS_PRESENT_CONTAINER_ROLES = /^(XCUIElementTypeApplication|XCUIElementTypeWindow)$/;
83270
83418
  var ALWAYS_PRESENT_CONTAINER_ID_NAMES = /* @__PURE__ */ new Set([
@@ -83362,10 +83510,19 @@ ${statePacket}`, redactionCredentials) },
83362
83510
  const anchor = pickLandingAnchor(recording.lastSnapshot);
83363
83511
  if (!anchor)
83364
83512
  return false;
83513
+ const modelAssertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
83514
+ const anchorLabel = (nonEmpty3(anchor.label) || nonEmpty3(anchor.text) || "").trim();
83515
+ if (modelAssertCount > 0 && AMBIENT_CHROME_LABEL_RE.test(anchorLabel)) {
83516
+ return false;
83517
+ }
83518
+ const anchorSelector = (0, mobile_source_parser_js_2.preferredLocator)(anchor);
83519
+ const alreadyAsserted = actions.some((a) => (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT") && typeof a.selector === "string" && a.selector === anchorSelector);
83520
+ if (alreadyAsserted)
83521
+ return false;
83365
83522
  const screen = recording.lastScreenName || "LandingScreen";
83366
83523
  actions.push(finalizeAction({
83367
83524
  type: "ASSERT_VISIBLE",
83368
- selector: (0, mobile_source_parser_js_2.preferredLocator)(anchor),
83525
+ selector: anchorSelector,
83369
83526
  metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
83370
83527
  }));
83371
83528
  return true;
@@ -84241,7 +84398,8 @@ ${statePacket}`, redactionCredentials) },
84241
84398
  targetAppId,
84242
84399
  credentials: credentialSecrets,
84243
84400
  redactionCredentials: credentialValuesForRedaction,
84244
- log: log2
84401
+ log: log2,
84402
+ allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA"
84245
84403
  });
84246
84404
  consecutiveInfraFailures = 0;
84247
84405
  const test = assembleTest({
@@ -337174,10 +337332,11 @@ Fatal error: ${msg}`);
337174
337332
  const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
337175
337333
  if (shouldPreserveAppData) {
337176
337334
  try {
337177
- await driver.relaunch();
337335
+ await driver.launchApp();
337336
+ log2("App activated with preserved data (no terminate/clear).");
337178
337337
  } catch (err) {
337179
337338
  (0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
337180
- log2(`App relaunch failed while preserving app data: ${err instanceof Error ? err.message : String(err)}`);
337339
+ log2(`App activate failed while preserving app data: ${err instanceof Error ? err.message : String(err)}`);
337181
337340
  }
337182
337341
  return false;
337183
337342
  }
@@ -353342,7 +353501,7 @@ var require_package4 = __commonJS({
353342
353501
  "package.json"(exports, module) {
353343
353502
  module.exports = {
353344
353503
  name: "@validate.qa/runner",
353345
- version: "1.0.84",
353504
+ version: "1.0.85",
353346
353505
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353347
353506
  bin: {
353348
353507
  "validate-runner": "dist/cli.js"
@@ -354208,9 +354367,9 @@ async function waitForColdBootSettle(sessionPath) {
354208
354367
  timeoutMs: MOBILE_COLD_BOOT_SETTLE_MS,
354209
354368
  minStableFrames: 2,
354210
354369
  pollMs: 400,
354211
- // WebView native shell alone is ~5–8 nodes; real first screens flatten to
354212
- // dozens. Require enough nodes that the DOM has painted (mirrors executor).
354213
- minElements: 12
354370
+ // Native splash / WebView shell is ~5–8 nodes; RN product screens and rich
354371
+ // onboarding typically flatten to 15+ once JS has painted.
354372
+ minElements: 15
354214
354373
  });
354215
354374
  }
354216
354375
  async function getWindowSize(sessionPath) {
@@ -354359,20 +354518,33 @@ async function sendKeysToFocusedInput(sessionPath, value) {
354359
354518
  }
354360
354519
  async function typeText(sessionPath, value, bounds) {
354361
354520
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
354521
+ if (!elementId && bounds === void 0) {
354522
+ throw new Error("No focused element available for TYPE. Tap an input field first.");
354523
+ }
354362
354524
  if (elementId) {
354363
354525
  try {
354364
- await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
354526
+ await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
354365
354527
  method: "POST",
354366
- body: JSON.stringify({ text: value })
354528
+ body: JSON.stringify({})
354367
354529
  });
354368
- return;
354369
354530
  } catch (err) {
354370
354531
  if (isAppiumDriverDeadError(err)) throw err;
354371
354532
  }
354372
- } else if (bounds === void 0) {
354373
- throw new Error("No focused element available for TYPE. Tap an input field first.");
354374
354533
  }
354375
- await sendKeysToFocusedInput(sessionPath, value);
354534
+ try {
354535
+ await sendKeysToFocusedInput(sessionPath, value);
354536
+ return;
354537
+ } catch (err) {
354538
+ if (isAppiumDriverDeadError(err)) throw err;
354539
+ }
354540
+ if (elementId) {
354541
+ await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
354542
+ method: "POST",
354543
+ body: JSON.stringify({ text: value })
354544
+ });
354545
+ return;
354546
+ }
354547
+ throw new Error("TYPE failed: could not send keys to the focused input.");
354376
354548
  }
354377
354549
  async function submitKeyboard(sessionPath, bounds) {
354378
354550
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
@@ -359520,6 +359692,9 @@ async function startSession(validated, res) {
359520
359692
  ...deviceId ? { "appium:udid": deviceId } : {},
359521
359693
  ...platformVersion ? { "appium:platformVersion": platformVersion } : {},
359522
359694
  "appium:noReset": true,
359695
+ // Keep pace with executor createSession: Expo "Open in App?" sheets and
359696
+ // permission dialogs otherwise freeze WDA for recording/mirror sessions.
359697
+ ...process.env.APPIUM_IOS_AUTO_ACCEPT_ALERTS !== "false" ? { "appium:autoAcceptAlerts": true } : {},
359523
359698
  // Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
359524
359699
  // Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
359525
359700
  // "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.84",
3
+ "version": "1.0.85",
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"