@validate.qa/runner 1.0.32 → 1.0.33

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 +441 -155
  2. package/dist/cli.mjs +441 -155
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -62290,7 +62290,7 @@ var require_mobile_explorer = __commonJS({
62290
62290
  return { screen, signal, refMap, observation };
62291
62291
  }
62292
62292
  function buildStatePacket(input) {
62293
- const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
62293
+ const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
62294
62294
  const sections = [];
62295
62295
  sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
62296
62296
  sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
@@ -62303,10 +62303,46 @@ var require_mobile_explorer = __commonJS({
62303
62303
  sections.push("");
62304
62304
  if (mode === "survey") {
62305
62305
  sections.push("**Mission:** Map every reachable screen. For each new screen: mobile_snapshot -> capture_screen -> record_transition. Get PAST the entry gate into the app's real main content \u2014 the login/landing screens are the ENTRANCE, not the app. If test credentials are provided (see the Test Credentials section), SIGN IN with them FIRST and map the authenticated app; that is the real product surface. Prefer a real login when credentials are provided; only if login genuinely cannot be completed after honest attempts (persistent error, the form will not accept the credential, no valid account) fall back to any guest/demo/skip entry the app offers and map the app fully from there \u2014 reaching the real feature surface that way is far better coverage than getting stuck on the login wall. Do NOT submit forms with junk data or mutate real data. Call finish_exploration ONLY when every reachable screen \u2014 including the authenticated content \u2014 has been captured and no unvisited frontier screens remain.");
62306
+ } else if (featureName?.trim()) {
62307
+ sections.push(`**Mission:** Feature-scoped deep exploration of **"${featureName.trim()}"**. Drive the app like a real user through THIS feature only: tap, type, submit, and observe outcomes. After a meaningful flow, call record_journey. Do NOT deep-dive unrelated features. Call finish_exploration when this feature's flows are covered.`);
62306
62308
  } else {
62307
62309
  sections.push("**Mission:** Use the focus features like a real user. Tap, type, submit, and observe outcomes. After a meaningful flow, call record_journey. Call finish_exploration when the flows are covered.");
62308
62310
  }
62309
62311
  sections.push("");
62312
+ if (mode === "deep" && featureName?.trim()) {
62313
+ sections.push(`## Feature: ${featureName.trim()}`);
62314
+ if (featureContext?.plannerDescription?.trim()) {
62315
+ sections.push(featureContext.plannerDescription.trim());
62316
+ }
62317
+ if (featureContext?.priorInsight?.trim()) {
62318
+ sections.push(`Prior insight: ${featureContext.priorInsight.trim()}`);
62319
+ }
62320
+ if (featureContext?.pageIntel && featureContext.pageIntel.length > 0) {
62321
+ sections.push("### Target screens / routes for this feature");
62322
+ for (const page of featureContext.pageIntel.slice(0, 12)) {
62323
+ sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
62324
+ }
62325
+ }
62326
+ if (featureContext?.flowIntel && featureContext.flowIntel.length > 0) {
62327
+ sections.push("### Known flows");
62328
+ for (const flow of featureContext.flowIntel.slice(0, 8)) {
62329
+ sections.push(`- ${flow}`);
62330
+ }
62331
+ }
62332
+ if (featureContext?.otherFeatures && featureContext.otherFeatures.length > 0) {
62333
+ sections.push("### Other features \u2014 do NOT explore these (navigation context only)");
62334
+ for (const other of featureContext.otherFeatures.slice(0, 12)) {
62335
+ const pages = other.pages?.length ? ` [${other.pages.slice(0, 4).join(", ")}]` : "";
62336
+ sections.push(`- ${other.name}${pages}`);
62337
+ }
62338
+ }
62339
+ sections.push("");
62340
+ }
62341
+ if (userNotes?.trim()) {
62342
+ sections.push(`## Operator notes
62343
+ ${userNotes.trim()}`);
62344
+ sections.push("");
62345
+ }
62310
62346
  const screenCount = state2.getScreenCount();
62311
62347
  sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
62312
62348
  const currentScreenId = state2.getCurrentScreenId();
@@ -62463,7 +62499,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62463
62499
  } catch {
62464
62500
  }
62465
62501
  };
62466
- const mode = ctx.mode === "deep" ? "deep" : "survey";
62502
+ const mode = ctx.mode === "survey" ? "survey" : "deep";
62467
62503
  const platform3 = platformForMedium(ctx.medium);
62468
62504
  const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
62469
62505
  const targetAppId = ctx.target?.appId?.trim() || void 0;
@@ -62751,7 +62787,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
62751
62787
  maxIterations,
62752
62788
  latestObservation: getLatest()?.observation ?? null,
62753
62789
  journeys,
62754
- transitions: collectedTransitions
62790
+ transitions: collectedTransitions,
62791
+ featureName: ctx.featureName,
62792
+ featureContext: ctx.featureContext,
62793
+ userNotes: ctx.userNotes
62755
62794
  });
62756
62795
  const messages = [
62757
62796
  { role: "system", content: systemPrompt },
@@ -66320,6 +66359,34 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
66320
66359
  if (context.featureName) {
66321
66360
  sections.push(`## Feature: ${context.featureName}
66322
66361
  SCOPE: this is a feature-focused deep run. Plan scenarios ONLY for the "${context.featureName}" feature \u2014 its screens, forms, and flows. Screens belonging to other features are navigation context / prerequisites only; do NOT plan scenarios whose primary goal exercises another feature.`);
66362
+ if (context.featureContext?.plannerDescription?.trim()) {
66363
+ sections.push(context.featureContext.plannerDescription.trim());
66364
+ }
66365
+ if (context.featureContext?.priorInsight?.trim()) {
66366
+ sections.push(`Prior insight: ${context.featureContext.priorInsight.trim()}`);
66367
+ }
66368
+ if (context.featureContext?.pageIntel && context.featureContext.pageIntel.length > 0) {
66369
+ sections.push("### Feature page intel");
66370
+ for (const page of context.featureContext.pageIntel.slice(0, 12)) {
66371
+ sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
66372
+ }
66373
+ }
66374
+ if (context.featureContext?.flowIntel && context.featureContext.flowIntel.length > 0) {
66375
+ sections.push("### Known flows");
66376
+ for (const flow of context.featureContext.flowIntel.slice(0, 8)) {
66377
+ sections.push(`- ${flow}`);
66378
+ }
66379
+ }
66380
+ if (context.featureContext?.otherFeatures && context.featureContext.otherFeatures.length > 0) {
66381
+ sections.push("### Other features (do not plan primary scenarios for these)");
66382
+ for (const other of context.featureContext.otherFeatures.slice(0, 12)) {
66383
+ sections.push(`- ${other.name}`);
66384
+ }
66385
+ }
66386
+ }
66387
+ if (context.userNotes?.trim()) {
66388
+ sections.push(`## Operator notes
66389
+ ${context.userNotes.trim()}`);
66323
66390
  }
66324
66391
  const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
66325
66392
  const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
@@ -66446,7 +66513,7 @@ ${constraints.join("\n")}`);
66446
66513
  routePattern: null,
66447
66514
  title: state2.screenName ?? null,
66448
66515
  pageType: state2.screenType ?? "UNKNOWN",
66449
- requiresAuth: false,
66516
+ requiresAuth: state2.screenType?.toLowerCase() === "auth",
66450
66517
  visitCount: state2.visited ? 1 : 0,
66451
66518
  lastVisitedAt: state2.visited ? now : null,
66452
66519
  elementsCovered: 0,
@@ -66466,6 +66533,8 @@ ${constraints.join("\n")}`);
66466
66533
  const toPageId = idByScreen.get(t.toScreenId);
66467
66534
  if (!fromPageId || !toPageId)
66468
66535
  continue;
66536
+ const fromScreen = graph.screens.get(t.fromScreenId);
66537
+ const toScreen = graph.screens.get(t.toScreenId);
66469
66538
  transitions.push({
66470
66539
  id: `mtx-${txSeq++}`,
66471
66540
  fromPageId,
@@ -66473,7 +66542,7 @@ ${constraints.join("\n")}`);
66473
66542
  triggerType: t.action.toUpperCase(),
66474
66543
  triggerLabel: t.viaRef ?? null,
66475
66544
  observedCount: 1,
66476
- requiresAuth: false
66545
+ requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
66477
66546
  });
66478
66547
  }
66479
66548
  return {
@@ -66676,7 +66745,9 @@ ${constraints.join("\n")}`);
66676
66745
  recordedJourneys,
66677
66746
  credentials: input.credentials,
66678
66747
  testBudget: input.testBudget,
66679
- incremental: input.incremental
66748
+ incremental: input.incremental,
66749
+ userNotes: input.userNotes,
66750
+ featureContext: input.featureContext
66680
66751
  })
66681
66752
  });
66682
66753
  const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
@@ -66935,8 +67006,38 @@ var require_mobile_generator = __commonJS({
66935
67006
  return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
66936
67007
  }
66937
67008
  var EDITABLE_ELEMENT_ROLE_RE = /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/;
67009
+ var EDITABLE_IDENTITY_RE = /\b(email|e-?mail|password|passwd|passcode|username|user[_-]?name|login|search|query|otp|pin|phone|mobile|input|textfield|text[_-]?field|edit|typehere)\b/i;
66938
67010
  function isEditableMobileElement(element) {
66939
- return EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim());
67011
+ if (EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim()))
67012
+ return true;
67013
+ const identity = [element.accessibilityId, element.resourceId, element.label].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
67014
+ if (identity && EDITABLE_IDENTITY_RE.test(identity))
67015
+ return true;
67016
+ return false;
67017
+ }
67018
+ function isIdentityBearingTapTarget(element) {
67019
+ if (weakAssertionTargetReason(element))
67020
+ return false;
67021
+ if (isBareContainerLikeElement(element))
67022
+ return false;
67023
+ if (isEditableMobileElement(element))
67024
+ return false;
67025
+ if (/button|link|tab|menuitem|checkbox|switch|radio/i.test(element.role))
67026
+ return true;
67027
+ const label = [
67028
+ element.label,
67029
+ element.text,
67030
+ element.accessibilityId,
67031
+ element.resourceId
67032
+ ].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
67033
+ return /\b(sign\s*in|log\s*in|login|submit|save|continue|create|send|confirm|done|next|apply|checkout|pay|finish|update|register|sign\s*up)\b/i.test(label);
67034
+ }
67035
+ function isBareContainerLikeElement(element) {
67036
+ if (BARE_CONTAINER_CLASS_RE.test(element.role)) {
67037
+ const hasIdentity = Boolean(nonEmpty2(element.text) || nonEmpty2(element.label) || nonEmpty2(element.accessibilityId));
67038
+ return !hasIdentity;
67039
+ }
67040
+ return false;
66940
67041
  }
66941
67042
  function nonEmpty2(value) {
66942
67043
  if (typeof value !== "string")
@@ -67428,7 +67529,12 @@ ${statePacket}` },
67428
67529
  };
67429
67530
  }
67430
67531
  const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
67431
- if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
67532
+ const screenChanged = changedScreen(before, absorbed);
67533
+ const identityTap = isIdentityBearingTapTarget(element);
67534
+ if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
67535
+ if (!screenChanged && identityTap) {
67536
+ log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
67537
+ }
67432
67538
  recordAction({
67433
67539
  type: "TAP",
67434
67540
  selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
@@ -67572,6 +67678,13 @@ ${statePacket}` },
67572
67678
  const element = resolveRef(toolArgs.ref);
67573
67679
  if (!element)
67574
67680
  return unknownRef(toolArgs.ref);
67681
+ const weakReason = weakAssertionTargetReason(element);
67682
+ if (weakReason) {
67683
+ return {
67684
+ ok: false,
67685
+ error: `Ref ${element.ref} is not a durable assertion target (${weakReason}). Re-snapshot and assert a human-visible label, text, or stable control that proves the screen outcome.`
67686
+ };
67687
+ }
67575
67688
  const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
67576
67689
  if (!expected)
67577
67690
  return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
@@ -67830,19 +67943,46 @@ ${statePacket}` },
67830
67943
  if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
67831
67944
  return "auth/session scenario tapped a login or sign-in control without recording a credential path";
67832
67945
  }
67833
- if (!isCredentialSignIn)
67946
+ if (isCredentialSignIn) {
67947
+ const typedEmail = typedEmailIndexes.length > 0;
67948
+ const typedPassword = typedPasswordIndexes.length > 0;
67949
+ const tappedSubmit = submitAfterCredentials;
67950
+ if (typedEmail && typedPassword && tappedSubmit) {
67951
+ } else {
67952
+ const missing = [
67953
+ typedEmail ? null : "email/username entry",
67954
+ typedPassword ? null : "password entry",
67955
+ tappedSubmit ? null : "sign-in submit tap"
67956
+ ].filter((value) => value !== null);
67957
+ return `credential sign-in scenario did not record ${missing.join(", ")}`;
67958
+ }
67959
+ }
67960
+ const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
67961
+ if (!isHappyPath)
67834
67962
  return null;
67835
- const typedEmail = typedEmailIndexes.length > 0;
67836
- const typedPassword = typedPasswordIndexes.length > 0;
67837
- const tappedSubmit = submitAfterCredentials;
67838
- if (typedEmail && typedPassword && tappedSubmit)
67963
+ let lastTypeIndex = -1;
67964
+ let lastAssertIndex = -1;
67965
+ actions.forEach((action, index) => {
67966
+ if (action.type === "TYPE")
67967
+ lastTypeIndex = index;
67968
+ if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
67969
+ lastAssertIndex = index;
67970
+ });
67971
+ if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
67839
67972
  return null;
67840
- const missing = [
67841
- typedEmail ? null : "email/username entry",
67842
- typedPassword ? null : "password entry",
67843
- tappedSubmit ? null : "sign-in submit tap"
67844
- ].filter((value) => value !== null);
67845
- return `credential sign-in scenario did not record ${missing.join(", ")}`;
67973
+ const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
67974
+ const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
67975
+ const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
67976
+ const commitAfterType = actions.some((action, index) => {
67977
+ if (action.type !== "TAP" || index <= lastTypeIndex || index >= lastAssertIndex)
67978
+ return false;
67979
+ const textForAction = actionText(action);
67980
+ return /\b(sign\s*in|log\s*in|login|continue|submit|save|create|send|confirm|done|next|apply|post|checkout|pay|finish|update|register|sign\s*up)\b/i.test(textForAction);
67981
+ });
67982
+ if (screensDiffer && !commitAfterType) {
67983
+ return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
67984
+ }
67985
+ return null;
67846
67986
  }
67847
67987
  function actionCycleKey(action) {
67848
67988
  return `${action.type}|${action.selector}|${action.value ?? ""}`;
@@ -68777,8 +68917,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
68777
68917
  function coerceSteps(value) {
68778
68918
  if (!Array.isArray(value) || value.length === 0)
68779
68919
  return void 0;
68780
- const allValid = value.every((step) => isRecord(step) && typeof step.action === "string" && typeof step.order === "number");
68781
- return allValid ? value : void 0;
68920
+ const steps = [];
68921
+ for (let i = 0; i < value.length; i++) {
68922
+ const step = value[i];
68923
+ if (!isRecord(step) || typeof step.action !== "string" || !step.action.trim()) {
68924
+ return void 0;
68925
+ }
68926
+ const orderRaw = step.order;
68927
+ const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : i;
68928
+ steps.push({
68929
+ ...step,
68930
+ order,
68931
+ action: step.action
68932
+ });
68933
+ }
68934
+ return steps.map((step, order) => ({ ...step, order }));
68782
68935
  }
68783
68936
  function parseHealVerdict(content) {
68784
68937
  const cleaned = stripCodeFences(content);
@@ -72999,6 +73152,48 @@ var require_snapshot_observation = __commonJS({
72999
73152
  var PROMOTED_FORM_FIELD_CAP = 12;
73000
73153
  var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
73001
73154
  var DATA_GRID_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:table|grid|rowgroup|row|gridcell|cell)\b/i;
73155
+ var DEEP_NONNAV_CAP_CENSUS = 16;
73156
+ var CENSUS_NAV_LINK_CAP = 60;
73157
+ var CENSUS_MIN_ACTION_GROUP = 3;
73158
+ var DESTRUCTIVE_ACTION_VERB_RE = /^(?:delete|remove|destroy|suspend|deactivate|disable|revoke|ban|block|archive|terminate|reject|cancel|reset|purge|wipe|unpublish|unsubscribe)$/i;
73159
+ function leadingActionVerb(label) {
73160
+ if (!label)
73161
+ return null;
73162
+ const m = label.trim().match(/[A-Za-z]+/);
73163
+ return m ? m[0].toLowerCase() : null;
73164
+ }
73165
+ function isNavLinkEntry(entry) {
73166
+ return entry.element.elementType === "LINK" && !!entry.element.triggersNavigation;
73167
+ }
73168
+ function isRowRepeatedControl(element) {
73169
+ return !!element.enrichment?.rowContext || !!element.containerPath && DATA_GRID_CONTAINER_SEGMENT_RE.test(element.containerPath);
73170
+ }
73171
+ function rowContainerScope(containerPath) {
73172
+ if (!containerPath)
73173
+ return null;
73174
+ const m = containerPath.match(/^(.*?)\s*>\s*row\b/i);
73175
+ return m ? m[1].trim() : containerPath.trim();
73176
+ }
73177
+ function containerScopeLabel(scope) {
73178
+ if (!scope)
73179
+ return null;
73180
+ const landmarks = [...scope.matchAll(/(?:table|grid|treegrid|list|listbox|tree|rowgroup):([^>]+)/gi)];
73181
+ if (landmarks.length > 0)
73182
+ return landmarks[landmarks.length - 1][1].trim();
73183
+ const segs = scope.split(">").map((s) => s.trim()).filter(Boolean);
73184
+ return segs.length ? segs[segs.length - 1] : null;
73185
+ }
73186
+ function rowIdentityKey(element) {
73187
+ const rc = element.enrichment?.rowContext;
73188
+ if (rc)
73189
+ return `rc:${rc}`;
73190
+ const cp = element.containerPath;
73191
+ if (cp) {
73192
+ const m = cp.match(/>\s*row:(.+)$/i);
73193
+ return m ? `row:${m[1].trim()}` : `cp:${cp}`;
73194
+ }
73195
+ return element.ref ? `ref:${element.ref}` : "unknown";
73196
+ }
73002
73197
  function isGroupedFormField(element) {
73003
73198
  if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
73004
73199
  return false;
@@ -73014,6 +73209,7 @@ var require_snapshot_observation = __commonJS({
73014
73209
  const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
73015
73210
  const artifact = page.snapshotArtifact;
73016
73211
  const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
73212
+ const rowActionCensus = richRender && options.mode !== "survey" && (options.rowActionCensus ?? process.env.DISCOVERY_SNAPSHOT_ROW_CENSUS === "true");
73017
73213
  const activeScope = inferActiveSnapshotScope(page);
73018
73214
  const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
73019
73215
  const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
@@ -73027,10 +73223,17 @@ var require_snapshot_observation = __commonJS({
73027
73223
  });
73028
73224
  let interactive;
73029
73225
  if (options.mode === "survey") {
73030
- const navLinks = allInteractive.filter(({ element }) => element.elementType === "LINK" && element.triggersNavigation);
73031
- const others = allInteractive.filter(({ element }) => !(element.elementType === "LINK" && element.triggersNavigation));
73226
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73227
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
73032
73228
  const othersCap = richRender ? 6 : 4;
73033
73229
  interactive = [...navLinks, ...others.slice(0, othersCap)];
73230
+ } else if (rowActionCensus) {
73231
+ const navLinks = allInteractive.filter(isNavLinkEntry);
73232
+ const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
73233
+ interactive = [
73234
+ ...navLinks.slice(0, CENSUS_NAV_LINK_CAP),
73235
+ ...others.slice(0, options.elementLimit ?? DEEP_NONNAV_CAP_CENSUS)
73236
+ ];
73034
73237
  } else {
73035
73238
  interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
73036
73239
  }
@@ -73043,6 +73246,36 @@ var require_snapshot_observation = __commonJS({
73043
73246
  }
73044
73247
  }
73045
73248
  }
73249
+ if (rowActionCensus && interactive.length > 0) {
73250
+ const shownVerbs = new Set(interactive.filter((entry) => !isNavLinkEntry(entry) && isRowRepeatedControl(entry.element)).map(({ element }) => leadingActionVerb(qualifyElementLabel(element))).filter(Boolean));
73251
+ const bestByVerb = /* @__PURE__ */ new Map();
73252
+ for (const entry of allInteractive) {
73253
+ if (isNavLinkEntry(entry) || !isRowRepeatedControl(entry.element))
73254
+ continue;
73255
+ const verb = leadingActionVerb(qualifyElementLabel(entry.element));
73256
+ if (!verb || !DESTRUCTIVE_ACTION_VERB_RE.test(verb))
73257
+ continue;
73258
+ if (shownVerbs.has(verb))
73259
+ continue;
73260
+ if (interactive.includes(entry))
73261
+ continue;
73262
+ const existing = bestByVerb.get(verb);
73263
+ if (!existing) {
73264
+ bestByVerb.set(verb, entry);
73265
+ continue;
73266
+ }
73267
+ if (existing.element.isDisabled && !entry.element.isDisabled)
73268
+ bestByVerb.set(verb, entry);
73269
+ }
73270
+ const swapIns = [...bestByVerb.values()];
73271
+ if (swapIns.length > 0) {
73272
+ const navShown = interactive.filter(isNavLinkEntry);
73273
+ const nonNavShown = interactive.filter((entry) => !isNavLinkEntry(entry));
73274
+ const evictable = Math.min(swapIns.length, nonNavShown.length);
73275
+ const trimmed = nonNavShown.slice(0, nonNavShown.length - evictable);
73276
+ interactive = [...navShown, ...trimmed, ...swapIns.slice(0, evictable)];
73277
+ }
73278
+ }
73046
73279
  const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
73047
73280
  const allForms = artifact?.formGroups ?? page.formGroups ?? [];
73048
73281
  const forms = allForms.slice(0, options.formLimit ?? 3);
@@ -73342,6 +73575,54 @@ var require_snapshot_observation = __commonJS({
73342
73575
  const hidden = allInteractive.length - interactive.length;
73343
73576
  if (hidden > 0) {
73344
73577
  lines.push(` # +${hidden} more interactive element(s) not individually listed (capped) \u2014 some may appear in the duplicates block above; if your target isn't here, narrow the scope or act on it by ref.`);
73578
+ if (rowActionCensus) {
73579
+ const shownSet = new Set(interactive.map((entry) => entry.element));
73580
+ const groups = /* @__PURE__ */ new Map();
73581
+ for (const entry of allInteractive) {
73582
+ const el = entry.element;
73583
+ if (!isRowRepeatedControl(el))
73584
+ continue;
73585
+ const verb = leadingActionVerb(qualifyElementLabel(el));
73586
+ if (!verb)
73587
+ continue;
73588
+ const role = (el.role ?? el.elementType ?? "control").toLowerCase();
73589
+ const scope = rowContainerScope(el.containerPath);
73590
+ const key = JSON.stringify([scope ?? "", role, verb]);
73591
+ let group = groups.get(key);
73592
+ if (!group) {
73593
+ group = { verb, role, scopeLabel: containerScopeLabel(scope), rows: /* @__PURE__ */ new Set(), shownRows: /* @__PURE__ */ new Set(), disabledRows: /* @__PURE__ */ new Set(), controls: 0 };
73594
+ groups.set(key, group);
73595
+ }
73596
+ const rowKey = rowIdentityKey(el);
73597
+ group.rows.add(rowKey);
73598
+ group.controls += 1;
73599
+ if (shownSet.has(el))
73600
+ group.shownRows.add(rowKey);
73601
+ if (el.isDisabled)
73602
+ group.disabledRows.add(rowKey);
73603
+ }
73604
+ const collapsed = [...groups.values()].map((g) => ({
73605
+ verb: g.verb,
73606
+ role: g.role,
73607
+ scopeLabel: g.scopeLabel,
73608
+ total: g.rows.size,
73609
+ shown: g.shownRows.size,
73610
+ disabled: g.disabledRows.size,
73611
+ controls: g.controls
73612
+ })).filter((g) => g.total >= CENSUS_MIN_ACTION_GROUP && g.total > g.shown).sort((a, b) => {
73613
+ const aDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(a.verb) ? 0 : 1;
73614
+ const bDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(b.verb) ? 0 : 1;
73615
+ return aDestructive - bDestructive || b.total - b.shown - (a.total - a.shown);
73616
+ });
73617
+ for (const g of collapsed) {
73618
+ const verbLabel = g.verb.charAt(0).toUpperCase() + g.verb.slice(1);
73619
+ const destructiveTag = DESTRUCTIVE_ACTION_VERB_RE.test(g.verb) ? " [destructive]" : "";
73620
+ const where = g.scopeLabel ? ` in ${yamlScalar(g.scopeLabel)}` : "";
73621
+ const controlsNote = g.controls > g.total ? ` (${g.controls} controls across ${g.total} rows)` : "";
73622
+ const disabledNote = g.disabled > 0 ? `; ${g.disabled} disabled at capture` : "";
73623
+ lines.push(` # \u21B3 "${verbLabel}" ${g.role}${destructiveTag}${where}: present on ${g.total} rows${controlsNote} \u2014 ${g.shown} listed above, ${g.total - g.shown} collapsed${disabledNote}; the same action repeats down these rows \u2014 target a specific row by its row identity (the row's Edit link / row_context above), not a bare "${verbLabel}" locator.`);
73624
+ }
73625
+ }
73345
73626
  }
73346
73627
  }
73347
73628
  }
@@ -74364,13 +74645,14 @@ var require_auth_recipe = __commonJS({
74364
74645
  "../runner-core/dist/services/auth-recipe.js"(exports) {
74365
74646
  "use strict";
74366
74647
  Object.defineProperty(exports, "__esModule", { value: true });
74367
- exports.AUTH_MARKER_VERSION = void 0;
74648
+ exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
74368
74649
  exports.computeRecipeHash = computeRecipeHash;
74369
74650
  exports.buildAuthRecipe = buildAuthRecipe;
74370
74651
  exports.authProofLocator = authProofLocator;
74371
74652
  exports.urlProofPredicateExpr = urlProofPredicateExpr;
74372
74653
  exports.renderAuthProofAssertion = renderAuthProofAssertion;
74373
74654
  exports.renderAuthFn = renderAuthFn;
74655
+ exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
74374
74656
  exports.absoluteRoute = absoluteRoute;
74375
74657
  exports.renderReplayClickArrow = renderReplayClickArrow;
74376
74658
  exports.renderReplaySettleArrow = renderReplaySettleArrow;
@@ -74489,6 +74771,17 @@ var require_auth_recipe = __commonJS({
74489
74771
  lines.push(END_MARKER);
74490
74772
  return lines.join("\n");
74491
74773
  }
74774
+ exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
74775
+ "AUTH_FILE",
74776
+ "SHARED_AUTH_INJECT",
74777
+ "VALIDATE_RUN_ID",
74778
+ "VALIDATE_TASK_ARN",
74779
+ "VALIDATE_RUNNER_AUTH",
74780
+ "VALIDATE_SERVER_URL"
74781
+ ]);
74782
+ function isReservedRunnerEnvVar(key) {
74783
+ return key.startsWith("VALIDATE_") || exports.RESERVED_RUNNER_ENV_VARS.has(key);
74784
+ }
74492
74785
  function absoluteRoute(route2, baseUrl) {
74493
74786
  try {
74494
74787
  return new URL(route2, baseUrl).href;
@@ -315593,9 +315886,15 @@ var require_mobile_discovery_agent = __commonJS({
315593
315886
  log2(`
315594
315887
  Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
315595
315888
  if (callbacks?.onExploreComplete) {
315596
- await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions)).catch((err) => {
315597
- log2(`[stream] explore-complete callback failed: ${err instanceof Error ? err.message : String(err)}`);
315598
- });
315889
+ try {
315890
+ await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions));
315891
+ } catch (err) {
315892
+ const message = checkpointErrorMessage(err);
315893
+ log2(`[stream] explore-complete callback failed: ${message}`);
315894
+ if (isTerminalDiscoveryCheckpointError(err)) {
315895
+ throw err;
315896
+ }
315897
+ }
315599
315898
  }
315600
315899
  if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
315601
315900
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -315623,8 +315922,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315623
315922
  pausePhase: "explore"
315624
315923
  };
315625
315924
  }
315626
- log2("\nRelaunching app for a clean state before planning/generation...");
315627
- await driver.relaunch();
315925
+ if (!isSurvey) {
315926
+ log2("\nRelaunching app for a clean state before planning/generation...");
315927
+ await driver.relaunch();
315928
+ }
315628
315929
  log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
315629
315930
  const graph = state2.toScreenGraph();
315630
315931
  const screenMap = state2.buildInMemoryScreenMap();
@@ -315647,7 +315948,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
315647
315948
  hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
315648
315949
  },
315649
315950
  incremental: ctx.incremental,
315650
- testBudget
315951
+ testBudget,
315952
+ userNotes: ctx.userNotes,
315953
+ featureContext: ctx.featureContext
315651
315954
  }, {
315652
315955
  modelOverride: ctx.planModel ?? ctx.generateModel,
315653
315956
  onAICall
@@ -315820,10 +316123,20 @@ Fatal error: ${msg}`);
315820
316123
  log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
315821
316124
  return false;
315822
316125
  }) ?? false;
315823
- if (!cleared && resetPolicy === "CLEAR_APP_DATA") {
315824
- log2("CLEAR_APP_DATA requested but app-data reset was unavailable; continuing without clearing app data.");
316126
+ if (cleared)
316127
+ return true;
316128
+ if (resetPolicy === "CLEAR_APP_DATA") {
316129
+ log2("CLEAR_APP_DATA requested but app-data reset was unavailable; a post-explore relaunch will reset state before planning.");
316130
+ }
316131
+ if (mode !== "survey") {
316132
+ return false;
315825
316133
  }
315826
- return cleared;
316134
+ try {
316135
+ await driver.relaunch();
316136
+ } catch (err) {
316137
+ log2(`App relaunch failed after unsuccessful app-data reset: ${err instanceof Error ? err.message : String(err)}`);
316138
+ }
316139
+ return false;
315827
316140
  }
315828
316141
  function shouldPauseAfterTruncatedExplore(explore) {
315829
316142
  return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
@@ -315870,6 +316183,7 @@ var require_discover_agent = __commonJS({
315870
316183
  var claude_discovery_runner_js_1 = require_claude_discovery_runner();
315871
316184
  var agentic_cli_constants_js_1 = require_agentic_cli_constants();
315872
316185
  var canvas_detector_js_1 = require_canvas_detector();
316186
+ var auth_recipe_js_1 = require_auth_recipe();
315873
316187
  async function runCanvasPreflight(ctx, sharedBrowser, log2) {
315874
316188
  const cached = ctx.appRenderType ?? null;
315875
316189
  const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
@@ -316565,7 +316879,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
316565
316879
  }
316566
316880
  if (envRefs.size > 0) {
316567
316881
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
316568
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
316882
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
316569
316883
  if (missing.length > 0) {
316570
316884
  autoVariableDefaults = {};
316571
316885
  for (const key of missing) {
@@ -317081,7 +317395,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
317081
317395
  envRefs.add(match[1]);
317082
317396
  if (envRefs.size > 0) {
317083
317397
  const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
317084
- const missing = [...envRefs].filter((k) => !existingKeys.has(k));
317398
+ const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
317085
317399
  if (missing.length > 0) {
317086
317400
  autoVariableDefaults = {};
317087
317401
  for (const key of missing) {
@@ -329963,9 +330277,9 @@ var require_dist2 = __commonJS({
329963
330277
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
329964
330278
  };
329965
330279
  Object.defineProperty(exports, "__esModule", { value: true });
329966
- exports.buildImportSystemRules = exports.buildImportKickoff = exports.buildImportContractMd = exports.MAX_ARCHIVE_BYTES = exports.isUnsafeEntryName = exports.extractZipSafely = exports.downloadArchiveCapped = exports.detectArchiveKind = exports.acquireRepoForImport = exports.runFrameworkImportOnRunner = exports.getAgenticEngine = exports.resolveRefFromSnapshot = exports.replayAuthRecipeIntoBrowser = exports.runRegisterCaptureRun = exports.runLoginCaptureRun = exports.AUTH_MARKER_VERSION = exports.buildHealAuthStamp = exports.extractAuthRegion = exports.upsertAuthBlock = exports.buildAuthRecipe = exports.commandToReplayStep = exports.buildReplayPlan = exports.absoluteRoute = exports.renderReplaySettleArrow = exports.renderReplayClickArrow = exports.renderAuthFn = exports.canonicalizeRoute = exports.executeGrokPerBatchHeal = exports.executeGrokPerTestHeal = exports.headerVal = exports.classifyAuthType = exports.isTextExcerptable = exports.isStaticAssetByExt = exports.isApiCall = exports.buildApiCallSummary = exports.computeMobileHealOutcome = exports.detectMobileStepSecretLeak = exports.redactMobileSecretsFromText = exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports.executeMobileLiveHeal = exports.executeMobileTestWithHealing = exports.executeMobileScriptDrivenHeal = exports.runMobileGeneratePhase = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
329967
- exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.stripJsonFences = exports.scrubCredentials = exports.withAIRetry = exports.snapshotMatchingChildren = exports.findNewChromiumChildPid = exports.killProcessTree = exports.gracefulCloseWithTreeKill = exports.liveBrowserRegistry = exports.REPO_SWITCH_REMOVED_FRACTION = exports.MAX_MANIFEST_FILES = exports.toDiffSummary = exports.isLikelyTestFile = exports.sanitizeRepoRelPath = exports.sha256Hex = exports.hashFiles = exports.enumerateTestFiles = exports.buildNextManifest = exports.detectRepoSwitch = exports.classifyImportDiff = exports.parseImportManifest = exports.REPO_ANALYSIS_SCHEMA_VERSION = exports.validateSourceEndpoints = exports.validateAuthoredTest = exports.validateRepoMapFile = exports.validateRepoMemoryFile = exports.buildTestingStandardsSection = exports.buildRepoAuthorSystemRules = exports.buildRepoAuthorKickoff = exports.buildRepoAuthorContractMd = exports.buildRepoMapSystemRules = exports.buildRepoMapKickoff = exports.buildRepoMapContractMd = exports.buildRepoMemorySystemRules = exports.buildRepoMemoryKickoff = exports.buildRepoMemoryContractMd = exports.runRepoAnalysisOnRunner = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = void 0;
329968
- exports.UNSUPPORTED_RENDERER_PREFIX = void 0;
330280
+ exports.buildImportContractMd = exports.MAX_ARCHIVE_BYTES = exports.isUnsafeEntryName = exports.extractZipSafely = exports.downloadArchiveCapped = exports.detectArchiveKind = exports.acquireRepoForImport = exports.runFrameworkImportOnRunner = exports.getAgenticEngine = exports.resolveRefFromSnapshot = exports.replayAuthRecipeIntoBrowser = exports.runRegisterCaptureRun = exports.runLoginCaptureRun = exports.isReservedRunnerEnvVar = exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = exports.buildHealAuthStamp = exports.extractAuthRegion = exports.upsertAuthBlock = exports.buildAuthRecipe = exports.commandToReplayStep = exports.buildReplayPlan = exports.absoluteRoute = exports.renderReplaySettleArrow = exports.renderReplayClickArrow = exports.renderAuthFn = exports.canonicalizeRoute = exports.executeGrokPerBatchHeal = exports.executeGrokPerTestHeal = exports.headerVal = exports.classifyAuthType = exports.isTextExcerptable = exports.isStaticAssetByExt = exports.isApiCall = exports.buildApiCallSummary = exports.computeMobileHealOutcome = exports.detectMobileStepSecretLeak = exports.redactMobileSecretsFromText = exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports.executeMobileLiveHeal = exports.executeMobileTestWithHealing = exports.executeMobileScriptDrivenHeal = exports.runMobileGeneratePhase = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
330281
+ exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.stripJsonFences = exports.scrubCredentials = exports.withAIRetry = exports.snapshotMatchingChildren = exports.findNewChromiumChildPid = exports.killProcessTree = exports.gracefulCloseWithTreeKill = exports.liveBrowserRegistry = exports.REPO_SWITCH_REMOVED_FRACTION = exports.MAX_MANIFEST_FILES = exports.toDiffSummary = exports.isLikelyTestFile = exports.sanitizeRepoRelPath = exports.sha256Hex = exports.hashFiles = exports.enumerateTestFiles = exports.buildNextManifest = exports.detectRepoSwitch = exports.classifyImportDiff = exports.parseImportManifest = exports.REPO_ANALYSIS_SCHEMA_VERSION = exports.validateSourceEndpoints = exports.validateAuthoredTest = exports.validateRepoMapFile = exports.validateRepoMemoryFile = exports.buildTestingStandardsSection = exports.buildRepoAuthorSystemRules = exports.buildRepoAuthorKickoff = exports.buildRepoAuthorContractMd = exports.buildRepoMapSystemRules = exports.buildRepoMapKickoff = exports.buildRepoMapContractMd = exports.buildRepoMemorySystemRules = exports.buildRepoMemoryKickoff = exports.buildRepoMemoryContractMd = exports.runRepoAnalysisOnRunner = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = exports.buildImportSystemRules = exports.buildImportKickoff = void 0;
330282
+ exports.UNSUPPORTED_RENDERER_PREFIX = exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = void 0;
329969
330283
  __exportStar(require_ai_provider(), exports);
329970
330284
  __exportStar(require_mobile_types(), exports);
329971
330285
  __exportStar(require_mobile_source_parser(), exports);
@@ -330102,6 +330416,12 @@ var require_dist2 = __commonJS({
330102
330416
  Object.defineProperty(exports, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
330103
330417
  return auth_recipe_js_1.AUTH_MARKER_VERSION;
330104
330418
  } });
330419
+ Object.defineProperty(exports, "RESERVED_RUNNER_ENV_VARS", { enumerable: true, get: function() {
330420
+ return auth_recipe_js_1.RESERVED_RUNNER_ENV_VARS;
330421
+ } });
330422
+ Object.defineProperty(exports, "isReservedRunnerEnvVar", { enumerable: true, get: function() {
330423
+ return auth_recipe_js_1.isReservedRunnerEnvVar;
330424
+ } });
330105
330425
  var auth_capture_run_js_1 = require_auth_capture_run();
330106
330426
  Object.defineProperty(exports, "runLoginCaptureRun", { enumerable: true, get: function() {
330107
330427
  return auth_capture_run_js_1.runLoginCaptureRun;
@@ -330329,7 +330649,7 @@ var require_package4 = __commonJS({
330329
330649
  "package.json"(exports, module) {
330330
330650
  module.exports = {
330331
330651
  name: "@validate.qa/runner",
330332
- version: "1.0.32",
330652
+ version: "1.0.33",
330333
330653
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
330334
330654
  bin: {
330335
330655
  "validate-runner": "dist/cli.js"
@@ -330945,25 +331265,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
330945
331265
  }
330946
331266
  return elementId;
330947
331267
  }
331268
+ async function sendKeysToFocusedInput(sessionPath, value) {
331269
+ await appiumRequest(`${sessionPath}/keys`, {
331270
+ method: "POST",
331271
+ body: JSON.stringify({ text: value, value: Array.from(value) })
331272
+ });
331273
+ }
330948
331274
  async function typeText(sessionPath, value, bounds) {
330949
331275
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330950
- if (!elementId) {
330951
- throw new Error("No focused element available for TYPE. Tap an input field first.");
331276
+ if (elementId) {
331277
+ try {
331278
+ await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
331279
+ method: "POST",
331280
+ body: JSON.stringify({ text: value })
331281
+ });
331282
+ return;
331283
+ } catch (err) {
331284
+ if (isAppiumDriverDeadError(err)) throw err;
331285
+ }
330952
331286
  }
330953
- await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
330954
- method: "POST",
330955
- body: JSON.stringify({ text: value })
330956
- });
331287
+ await sendKeysToFocusedInput(sessionPath, value);
330957
331288
  }
330958
331289
  async function clearField(sessionPath, bounds) {
330959
331290
  const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
330960
- if (!elementId) {
330961
- throw new Error("No focused element available for CLEAR. Tap an input field first.");
331291
+ if (elementId) {
331292
+ try {
331293
+ await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
331294
+ method: "POST",
331295
+ body: JSON.stringify({})
331296
+ });
331297
+ return;
331298
+ } catch (err) {
331299
+ if (isAppiumDriverDeadError(err)) throw err;
331300
+ }
331301
+ }
331302
+ try {
331303
+ await appiumRequest(`${sessionPath}/keys`, {
331304
+ method: "POST",
331305
+ // Common select-all + delete sequence is platform-dependent; empty keys
331306
+ // after focus is the portable baseline Appium accepts for many hybrids.
331307
+ body: JSON.stringify({ text: "", value: [] })
331308
+ });
331309
+ } catch (err) {
331310
+ if (isAppiumDriverDeadError(err)) throw err;
331311
+ throw new Error(
331312
+ `No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
331313
+ );
330962
331314
  }
330963
- await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
330964
- method: "POST",
330965
- body: JSON.stringify({})
330966
- });
330967
331315
  }
330968
331316
  async function swipe(sessionPath, direction, distance, windowSize) {
330969
331317
  const dist = clampMobileSwipeDistance(distance);
@@ -332204,6 +332552,10 @@ async function startMobileNetworkCapture(options) {
332204
332552
 
332205
332553
  // src/services/appium-executor.ts
332206
332554
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
332555
+ var ELEMENT_ID_PATTERN2 = /^[A-Za-z0-9._:-]{1,128}$/;
332556
+ function validateElementId(elementId) {
332557
+ return typeof elementId === "string" && ELEMENT_ID_PATTERN2.test(elementId) ? elementId : null;
332558
+ }
332207
332559
  var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
332208
332560
  const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
332209
332561
  return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
@@ -332338,6 +332690,9 @@ async function createSession(target) {
332338
332690
  });
332339
332691
  const sessionId = result?.value?.sessionId ?? result?.sessionId;
332340
332692
  if (!sessionId) throw new Error("Appium session creation returned no session ID");
332693
+ if (target.platform === "ANDROID") {
332694
+ await prepareAndroidInputEnvironment(target.deviceId);
332695
+ }
332341
332696
  return sessionId;
332342
332697
  }
332343
332698
  var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
@@ -332725,7 +333080,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
332725
333080
  method: "POST",
332726
333081
  body: JSON.stringify({ using: strategy.using, value: strategy.value })
332727
333082
  });
332728
- const elementId = result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"];
333083
+ const elementId = validateElementId(
333084
+ result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
333085
+ );
332729
333086
  if (elementId) return elementId;
332730
333087
  } catch (err) {
332731
333088
  if (isTransportError(err)) throw err;
@@ -332765,24 +333122,25 @@ async function tapCoordinates(sessionId, bounds) {
332765
333122
  })
332766
333123
  });
332767
333124
  }
332768
- async function getActiveElementId2(sessionId) {
332769
- try {
332770
- const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
332771
- return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
332772
- } catch {
332773
- return null;
332774
- }
332775
- }
332776
- async function sendKeysToFocusedInput(sessionId, value) {
332777
- await wdRequest(`/session/${sessionId}/keys`, {
332778
- method: "POST",
332779
- body: JSON.stringify({ text: value, value: Array.from(value) })
332780
- });
332781
- }
332782
333125
  function encodeAndroidInputText(value) {
332783
333126
  const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
332784
333127
  return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
332785
333128
  }
333129
+ async function prepareAndroidInputEnvironment(deviceId) {
333130
+ if (!deviceId) return;
333131
+ const settingsToDisable = [
333132
+ ["secure", "stylus_handwriting_enabled", "0"]
333133
+ ];
333134
+ for (const [namespace, key, value] of settingsToDisable) {
333135
+ try {
333136
+ await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
333137
+ timeout: 8e3,
333138
+ windowsHide: true
333139
+ });
333140
+ } catch {
333141
+ }
333142
+ }
333143
+ }
332786
333144
  async function sendAndroidTextToFocusedInput(deviceId, value) {
332787
333145
  if (!deviceId) {
332788
333146
  throw new Error("Android keyboard fallback requires a device id");
@@ -332844,100 +333202,28 @@ async function executeType(sessionId, step, context) {
332844
333202
  if (step.value === void 0 || step.value === null || step.value === "") return;
332845
333203
  const value = String(step.value);
332846
333204
  const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
332847
- let elementId = null;
332848
- let lastInputError = null;
332849
- let tappedBounds = false;
332850
- if (bounds) {
332851
- await tapCoordinates(sessionId, bounds);
332852
- tappedBounds = true;
332853
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332854
- elementId = await getActiveElementId2(sessionId);
332855
- if (elementId) {
332856
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332857
- if (!lastInputError) return;
332858
- elementId = null;
332859
- }
332860
- }
332861
- if (step.target) {
332862
- try {
332863
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
332864
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332865
- if (!lastInputError) return;
332866
- elementId = null;
332867
- } catch (error2) {
332868
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
332869
- lastInputError = error2;
332870
- }
332871
- }
332872
- if (!elementId && bounds) {
332873
- await tapCoordinates(sessionId, bounds);
332874
- tappedBounds = true;
332875
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332876
- elementId = await getActiveElementId2(sessionId);
332877
- }
332878
- if (!elementId) {
332879
- elementId = await getActiveElementId2(sessionId);
332880
- }
332881
- if (elementId) {
332882
- lastInputError = await trySendElementValue(sessionId, elementId, value);
332883
- if (!lastInputError) return;
332884
- elementId = null;
332885
- if (!bounds) {
332886
- throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
332887
- }
332888
- }
332889
- if (bounds) {
332890
- if (!tappedBounds) {
332891
- await tapCoordinates(sessionId, bounds);
332892
- tappedBounds = true;
332893
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332894
- }
332895
- try {
332896
- await sendKeysToFocusedInput(sessionId, value);
332897
- return;
332898
- } catch (error2) {
332899
- if (isTransportError(error2)) throw error2;
332900
- lastInputError = error2;
332901
- }
332902
- }
332903
- if (context.platform === "ANDROID" && tappedBounds) {
332904
- try {
332905
- context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
332906
- await sendAndroidTextToFocusedInput(context.deviceId, value);
332907
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332908
- return;
332909
- } catch (error2) {
332910
- if (isTransportError(error2)) throw error2;
332911
- lastInputError = error2;
333205
+ const sessionPath = `/session/${sessionId}`;
333206
+ try {
333207
+ await typeText(sessionPath, value, bounds);
333208
+ return;
333209
+ } catch (error2) {
333210
+ if (isTransportError(error2)) throw error2;
333211
+ if (context.platform === "ANDROID" && bounds) {
333212
+ try {
333213
+ context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
333214
+ await sendAndroidTextToFocusedInput(context.deviceId, value);
333215
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
333216
+ return;
333217
+ } catch (androidError) {
333218
+ if (isTransportError(androidError)) throw androidError;
333219
+ }
332912
333220
  }
333221
+ throw error2 instanceof Error ? error2 : new Error(String(error2));
332913
333222
  }
332914
- throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
332915
333223
  }
332916
333224
  async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
332917
333225
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
332918
- let elementId = null;
332919
- if (step.target) {
332920
- try {
332921
- elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
332922
- } catch (error2) {
332923
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
332924
- }
332925
- }
332926
- if (!elementId && bounds) {
332927
- await tapCoordinates(sessionId, bounds);
332928
- await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
332929
- elementId = await getActiveElementId2(sessionId);
332930
- }
332931
- if (!elementId) {
332932
- elementId = await getActiveElementId2(sessionId);
332933
- }
332934
- if (!elementId) {
332935
- throw new Error("CLEAR step could not resolve a target or focused element");
332936
- }
332937
- await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
332938
- method: "POST",
332939
- body: "{}"
332940
- });
333226
+ await clearField(`/session/${sessionId}`, bounds);
332941
333227
  }
332942
333228
  async function executeSwipe(sessionId, step) {
332943
333229
  const direction = step.metadata?.direction || "up";