@validate.qa/runner 1.0.83 → 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.
- package/dist/cli.js +209 -34
- package/dist/cli.mjs +209 -34
- 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
|
|
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 =
|
|
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).
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
83249
|
-
stableAnchors.sort((a, b) => {
|
|
83250
|
-
const
|
|
83251
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
354207
|
-
//
|
|
354208
|
-
minElements:
|
|
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}/
|
|
354521
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
354360
354522
|
method: "POST",
|
|
354361
|
-
body: JSON.stringify({
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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).
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
83254
|
-
stableAnchors.sort((a, b) => {
|
|
83255
|
-
const
|
|
83256
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
354212
|
-
//
|
|
354213
|
-
minElements:
|
|
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}/
|
|
354526
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
354365
354527
|
method: "POST",
|
|
354366
|
-
body: JSON.stringify({
|
|
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
|
-
|
|
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