@validate.qa/runner 1.0.31 → 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.
- package/dist/cli.js +645 -172
- package/dist/cli.mjs +645 -172
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5001,6 +5001,29 @@ var require_mobile_observation = __commonJS({
|
|
|
5001
5001
|
}
|
|
5002
5002
|
});
|
|
5003
5003
|
|
|
5004
|
+
// ../runner-core/dist/services/mobile/mobile-keyboard.js
|
|
5005
|
+
var require_mobile_keyboard = __commonJS({
|
|
5006
|
+
"../runner-core/dist/services/mobile/mobile-keyboard.js"(exports2) {
|
|
5007
|
+
"use strict";
|
|
5008
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
5009
|
+
exports2.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
|
|
5010
|
+
exports2.dismissKeyboardForRetry = dismissKeyboardForRetry;
|
|
5011
|
+
var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
|
|
5012
|
+
var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
|
|
5013
|
+
function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
|
|
5014
|
+
if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
|
|
5015
|
+
return false;
|
|
5016
|
+
return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
|
|
5017
|
+
}
|
|
5018
|
+
async function dismissKeyboardForRetry(driver) {
|
|
5019
|
+
if (typeof driver.dismissKeyboard === "function") {
|
|
5020
|
+
return driver.dismissKeyboard();
|
|
5021
|
+
}
|
|
5022
|
+
return driver.back();
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
});
|
|
5026
|
+
|
|
5004
5027
|
// ../runner-core/dist/utils/scrub-credentials.js
|
|
5005
5028
|
var require_scrub_credentials = __commonJS({
|
|
5006
5029
|
"../runner-core/dist/utils/scrub-credentials.js"(exports2) {
|
|
@@ -61982,6 +62005,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
61982
62005
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
61983
62006
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
61984
62007
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
62008
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
61985
62009
|
var credential_tools_js_1 = require_credential_tools();
|
|
61986
62010
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
61987
62011
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
@@ -61991,6 +62015,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
61991
62015
|
var MAX_RECENT_EXCHANGES = 24;
|
|
61992
62016
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
61993
62017
|
var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
|
|
62018
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
61994
62019
|
var MOBILE_SNAPSHOT_TOOL = {
|
|
61995
62020
|
type: "function",
|
|
61996
62021
|
function: {
|
|
@@ -62260,7 +62285,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
62260
62285
|
return { screen, signal, refMap, observation };
|
|
62261
62286
|
}
|
|
62262
62287
|
function buildStatePacket(input) {
|
|
62263
|
-
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
|
|
62288
|
+
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
|
|
62264
62289
|
const sections = [];
|
|
62265
62290
|
sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
|
|
62266
62291
|
sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
|
|
@@ -62273,10 +62298,46 @@ var require_mobile_explorer = __commonJS({
|
|
|
62273
62298
|
sections.push("");
|
|
62274
62299
|
if (mode === "survey") {
|
|
62275
62300
|
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.");
|
|
62301
|
+
} else if (featureName?.trim()) {
|
|
62302
|
+
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.`);
|
|
62276
62303
|
} else {
|
|
62277
62304
|
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.");
|
|
62278
62305
|
}
|
|
62279
62306
|
sections.push("");
|
|
62307
|
+
if (mode === "deep" && featureName?.trim()) {
|
|
62308
|
+
sections.push(`## Feature: ${featureName.trim()}`);
|
|
62309
|
+
if (featureContext?.plannerDescription?.trim()) {
|
|
62310
|
+
sections.push(featureContext.plannerDescription.trim());
|
|
62311
|
+
}
|
|
62312
|
+
if (featureContext?.priorInsight?.trim()) {
|
|
62313
|
+
sections.push(`Prior insight: ${featureContext.priorInsight.trim()}`);
|
|
62314
|
+
}
|
|
62315
|
+
if (featureContext?.pageIntel && featureContext.pageIntel.length > 0) {
|
|
62316
|
+
sections.push("### Target screens / routes for this feature");
|
|
62317
|
+
for (const page of featureContext.pageIntel.slice(0, 12)) {
|
|
62318
|
+
sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
|
|
62319
|
+
}
|
|
62320
|
+
}
|
|
62321
|
+
if (featureContext?.flowIntel && featureContext.flowIntel.length > 0) {
|
|
62322
|
+
sections.push("### Known flows");
|
|
62323
|
+
for (const flow of featureContext.flowIntel.slice(0, 8)) {
|
|
62324
|
+
sections.push(`- ${flow}`);
|
|
62325
|
+
}
|
|
62326
|
+
}
|
|
62327
|
+
if (featureContext?.otherFeatures && featureContext.otherFeatures.length > 0) {
|
|
62328
|
+
sections.push("### Other features \u2014 do NOT explore these (navigation context only)");
|
|
62329
|
+
for (const other of featureContext.otherFeatures.slice(0, 12)) {
|
|
62330
|
+
const pages = other.pages?.length ? ` [${other.pages.slice(0, 4).join(", ")}]` : "";
|
|
62331
|
+
sections.push(`- ${other.name}${pages}`);
|
|
62332
|
+
}
|
|
62333
|
+
}
|
|
62334
|
+
sections.push("");
|
|
62335
|
+
}
|
|
62336
|
+
if (userNotes?.trim()) {
|
|
62337
|
+
sections.push(`## Operator notes
|
|
62338
|
+
${userNotes.trim()}`);
|
|
62339
|
+
sections.push("");
|
|
62340
|
+
}
|
|
62280
62341
|
const screenCount = state2.getScreenCount();
|
|
62281
62342
|
sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
|
|
62282
62343
|
const currentScreenId = state2.getCurrentScreenId();
|
|
@@ -62433,7 +62494,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62433
62494
|
} catch {
|
|
62434
62495
|
}
|
|
62435
62496
|
};
|
|
62436
|
-
const mode = ctx.mode === "
|
|
62497
|
+
const mode = ctx.mode === "survey" ? "survey" : "deep";
|
|
62437
62498
|
const platform3 = platformForMedium(ctx.medium);
|
|
62438
62499
|
const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
|
|
62439
62500
|
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
@@ -62556,9 +62617,46 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62556
62617
|
externalOwner: boundary.owner
|
|
62557
62618
|
};
|
|
62558
62619
|
}
|
|
62559
|
-
|
|
62620
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
62621
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
62622
|
+
try {
|
|
62623
|
+
await driver.launchApp();
|
|
62624
|
+
const retrySnap = await driver.snapshot();
|
|
62625
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.windowSize, retrySnap.screenSignal);
|
|
62626
|
+
if (!retryRecovered)
|
|
62627
|
+
continue;
|
|
62628
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
62629
|
+
latest = retryRecovered;
|
|
62630
|
+
lastSnapshotIteration = iterationForObservation;
|
|
62631
|
+
if (retryBoundary.kind === "target" || retryBoundary.kind === "unknown") {
|
|
62632
|
+
const screenId = ingestSnapshot(retryRecovered, iterationForObservation);
|
|
62633
|
+
captureScreenshot(retrySnap.screenshot, screenId);
|
|
62634
|
+
log2(` Recovered target app after ${attempt} additional activation attempt(s).`);
|
|
62635
|
+
return {
|
|
62636
|
+
observation: retryRecovered.observation,
|
|
62637
|
+
screenId,
|
|
62638
|
+
externalBlocked: true,
|
|
62639
|
+
externalOwner: boundary.owner
|
|
62640
|
+
};
|
|
62641
|
+
}
|
|
62642
|
+
if (retryBoundary.kind === "transient_external") {
|
|
62643
|
+
log2(` Recovery surfaced system overlay ${retryBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
62644
|
+
return {
|
|
62645
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
62646
|
+
screenId: null,
|
|
62647
|
+
externalBlocked: true,
|
|
62648
|
+
transientExternal: true,
|
|
62649
|
+
externalOwner: boundary.owner
|
|
62650
|
+
};
|
|
62651
|
+
}
|
|
62652
|
+
lastExternalOwner = retryBoundary.owner;
|
|
62653
|
+
} catch (err) {
|
|
62654
|
+
log2(` Target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62655
|
+
}
|
|
62656
|
+
}
|
|
62657
|
+
log2(` Relaunch still outside target app (${lastExternalOwner}); no screen recorded.`);
|
|
62560
62658
|
return {
|
|
62561
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
62659
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no screen was recorded)`,
|
|
62562
62660
|
screenId: null,
|
|
62563
62661
|
externalBlocked: true,
|
|
62564
62662
|
externalOwner: boundary.owner
|
|
@@ -62684,7 +62782,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62684
62782
|
maxIterations,
|
|
62685
62783
|
latestObservation: getLatest()?.observation ?? null,
|
|
62686
62784
|
journeys,
|
|
62687
|
-
transitions: collectedTransitions
|
|
62785
|
+
transitions: collectedTransitions,
|
|
62786
|
+
featureName: ctx.featureName,
|
|
62787
|
+
featureContext: ctx.featureContext,
|
|
62788
|
+
userNotes: ctx.userNotes
|
|
62688
62789
|
});
|
|
62689
62790
|
const messages = [
|
|
62690
62791
|
{ role: "system", content: systemPrompt },
|
|
@@ -62936,7 +63037,19 @@ Exploration complete: ${summary}`);
|
|
|
62936
63037
|
const fromScreenId = state2.getCurrentScreenId();
|
|
62937
63038
|
const boundaryBeforeAction = currentBoundary();
|
|
62938
63039
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
62939
|
-
|
|
63040
|
+
let actionResult = await driver.tap(resolved.bounds);
|
|
63041
|
+
if (!actionResult.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(actionResult.error, latest()?.screen.elements ?? [])) {
|
|
63042
|
+
log2(" Tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
63043
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
63044
|
+
const retryResult = await driver.tap(resolved.bounds);
|
|
63045
|
+
actionResult = retryResult.ok ? retryResult : {
|
|
63046
|
+
...retryResult,
|
|
63047
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
63048
|
+
source: retryResult.source ?? dismissResult.source,
|
|
63049
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
63050
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
63051
|
+
};
|
|
63052
|
+
}
|
|
62940
63053
|
const absorbed = await absorbActionResult(actionResult, "mobile_tap");
|
|
62941
63054
|
const { observation, screenId } = absorbed;
|
|
62942
63055
|
if (!startedOutsideTarget && !absorbed.externalBlocked && !absorbed.transientExternal) {
|
|
@@ -65936,6 +66049,25 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
65936
66049
|
function targetAppScreens(graph, appId) {
|
|
65937
66050
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
65938
66051
|
}
|
|
66052
|
+
function isGenericNonFeatureScreen(state2) {
|
|
66053
|
+
const type = state2.screenType?.trim().toLowerCase();
|
|
66054
|
+
if (type === "error" || type === "permission")
|
|
66055
|
+
return true;
|
|
66056
|
+
if (type && type !== "unknown")
|
|
66057
|
+
return false;
|
|
66058
|
+
const actionableCount = state2.elements.filter((el) => el.actionable).length;
|
|
66059
|
+
if (actionableCount > 0)
|
|
66060
|
+
return false;
|
|
66061
|
+
const evidenceText = [
|
|
66062
|
+
state2.screenName,
|
|
66063
|
+
state2.screenType,
|
|
66064
|
+
...state2.observations ?? []
|
|
66065
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ");
|
|
66066
|
+
return /\b(empty|loading|blank|spinner|intermediate|placeholder|no actionable)\b/i.test(evidenceText);
|
|
66067
|
+
}
|
|
66068
|
+
function plannableTargetAppScreens(graph, appId) {
|
|
66069
|
+
return targetAppScreens(graph, appId).filter((state2) => !isGenericNonFeatureScreen(state2));
|
|
66070
|
+
}
|
|
65939
66071
|
exports2.MOBILE_SURVEY_SYSTEM_PROMPT = `You are a feature analyst for a native mobile application (iOS or Android). You receive evidence from an AI explorer that mapped the app's screens by tapping, typing, and swiping through the live UI. Your job is to organize screens into meaningful feature groups.
|
|
65940
66072
|
|
|
65941
66073
|
## Output Discipline
|
|
@@ -65955,8 +66087,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65955
66087
|
5. CRUD screens for the same entity belong together
|
|
65956
66088
|
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).
|
|
65957
66089
|
7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
|
|
65958
|
-
8. Do NOT create standalone features for generic error / empty screens (network-error, permission-denied). If a feature screen is broken or crashes, keep it in its intended feature and note the failure in the description.
|
|
65959
|
-
9. COVER EVERY SCREEN: every explored screen except generic error / permission screens MUST appear in exactly one feature group's routes. Do not leave screens ungrouped \u2014 a screen the explorer reached is part of SOME feature. Attach an ambiguous 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.
|
|
66090
|
+
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.
|
|
66091
|
+
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.
|
|
65960
66092
|
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.
|
|
65961
66093
|
|
|
65962
66094
|
## Output JSON Schema
|
|
@@ -66222,8 +66354,36 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66222
66354
|
if (context.featureName) {
|
|
66223
66355
|
sections.push(`## Feature: ${context.featureName}
|
|
66224
66356
|
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.`);
|
|
66357
|
+
if (context.featureContext?.plannerDescription?.trim()) {
|
|
66358
|
+
sections.push(context.featureContext.plannerDescription.trim());
|
|
66359
|
+
}
|
|
66360
|
+
if (context.featureContext?.priorInsight?.trim()) {
|
|
66361
|
+
sections.push(`Prior insight: ${context.featureContext.priorInsight.trim()}`);
|
|
66362
|
+
}
|
|
66363
|
+
if (context.featureContext?.pageIntel && context.featureContext.pageIntel.length > 0) {
|
|
66364
|
+
sections.push("### Feature page intel");
|
|
66365
|
+
for (const page of context.featureContext.pageIntel.slice(0, 12)) {
|
|
66366
|
+
sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
|
|
66367
|
+
}
|
|
66368
|
+
}
|
|
66369
|
+
if (context.featureContext?.flowIntel && context.featureContext.flowIntel.length > 0) {
|
|
66370
|
+
sections.push("### Known flows");
|
|
66371
|
+
for (const flow of context.featureContext.flowIntel.slice(0, 8)) {
|
|
66372
|
+
sections.push(`- ${flow}`);
|
|
66373
|
+
}
|
|
66374
|
+
}
|
|
66375
|
+
if (context.featureContext?.otherFeatures && context.featureContext.otherFeatures.length > 0) {
|
|
66376
|
+
sections.push("### Other features (do not plan primary scenarios for these)");
|
|
66377
|
+
for (const other of context.featureContext.otherFeatures.slice(0, 12)) {
|
|
66378
|
+
sections.push(`- ${other.name}`);
|
|
66379
|
+
}
|
|
66380
|
+
}
|
|
66381
|
+
}
|
|
66382
|
+
if (context.userNotes?.trim()) {
|
|
66383
|
+
sections.push(`## Operator notes
|
|
66384
|
+
${context.userNotes.trim()}`);
|
|
66225
66385
|
}
|
|
66226
|
-
const orderedScreens =
|
|
66386
|
+
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66227
66387
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66228
66388
|
sections.push(`## Screens Discovered (${orderedScreens.length})`);
|
|
66229
66389
|
for (const state2 of orderedScreens) {
|
|
@@ -66332,7 +66492,7 @@ ${constraints.join("\n")}`);
|
|
|
66332
66492
|
const now = /* @__PURE__ */ new Date();
|
|
66333
66493
|
const idByScreen = /* @__PURE__ */ new Map();
|
|
66334
66494
|
let pageSeq = 0;
|
|
66335
|
-
const orderedScreens =
|
|
66495
|
+
const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66336
66496
|
for (const state2 of orderedScreens) {
|
|
66337
66497
|
idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
|
|
66338
66498
|
}
|
|
@@ -66348,7 +66508,7 @@ ${constraints.join("\n")}`);
|
|
|
66348
66508
|
routePattern: null,
|
|
66349
66509
|
title: state2.screenName ?? null,
|
|
66350
66510
|
pageType: state2.screenType ?? "UNKNOWN",
|
|
66351
|
-
requiresAuth:
|
|
66511
|
+
requiresAuth: state2.screenType?.toLowerCase() === "auth",
|
|
66352
66512
|
visitCount: state2.visited ? 1 : 0,
|
|
66353
66513
|
lastVisitedAt: state2.visited ? now : null,
|
|
66354
66514
|
elementsCovered: 0,
|
|
@@ -66368,6 +66528,8 @@ ${constraints.join("\n")}`);
|
|
|
66368
66528
|
const toPageId = idByScreen.get(t.toScreenId);
|
|
66369
66529
|
if (!fromPageId || !toPageId)
|
|
66370
66530
|
continue;
|
|
66531
|
+
const fromScreen = graph.screens.get(t.fromScreenId);
|
|
66532
|
+
const toScreen = graph.screens.get(t.toScreenId);
|
|
66371
66533
|
transitions.push({
|
|
66372
66534
|
id: `mtx-${txSeq++}`,
|
|
66373
66535
|
fromPageId,
|
|
@@ -66375,7 +66537,7 @@ ${constraints.join("\n")}`);
|
|
|
66375
66537
|
triggerType: t.action.toUpperCase(),
|
|
66376
66538
|
triggerLabel: t.viaRef ?? null,
|
|
66377
66539
|
observedCount: 1,
|
|
66378
|
-
requiresAuth:
|
|
66540
|
+
requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
|
|
66379
66541
|
});
|
|
66380
66542
|
}
|
|
66381
66543
|
return {
|
|
@@ -66393,7 +66555,7 @@ ${constraints.join("\n")}`);
|
|
|
66393
66555
|
}
|
|
66394
66556
|
function buildMobileFeatureGroupsFromGraph(graph, appId) {
|
|
66395
66557
|
const groups = /* @__PURE__ */ new Map();
|
|
66396
|
-
const orderedScreens =
|
|
66558
|
+
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66397
66559
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66398
66560
|
for (const state2 of orderedScreens) {
|
|
66399
66561
|
const screenTypeLower = state2.screenType?.toLowerCase();
|
|
@@ -66440,7 +66602,7 @@ ${constraints.join("\n")}`);
|
|
|
66440
66602
|
return [];
|
|
66441
66603
|
if (input.featureName?.trim())
|
|
66442
66604
|
return [];
|
|
66443
|
-
const orderedScreens =
|
|
66605
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66444
66606
|
if (orderedScreens.length === 0)
|
|
66445
66607
|
return [];
|
|
66446
66608
|
const entryScreenId = orderedScreens[0].screenId;
|
|
@@ -66578,7 +66740,9 @@ ${constraints.join("\n")}`);
|
|
|
66578
66740
|
recordedJourneys,
|
|
66579
66741
|
credentials: input.credentials,
|
|
66580
66742
|
testBudget: input.testBudget,
|
|
66581
|
-
incremental: input.incremental
|
|
66743
|
+
incremental: input.incremental,
|
|
66744
|
+
userNotes: input.userNotes,
|
|
66745
|
+
featureContext: input.featureContext
|
|
66582
66746
|
})
|
|
66583
66747
|
});
|
|
66584
66748
|
const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
|
|
@@ -66642,9 +66806,11 @@ var require_mobile_generator = __commonJS({
|
|
|
66642
66806
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
66643
66807
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
66644
66808
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
66809
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
66645
66810
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
66646
66811
|
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
66647
66812
|
var MAX_RECENT_EXCHANGES = 24;
|
|
66813
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
66648
66814
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
66649
66815
|
function finalizeAction(input) {
|
|
66650
66816
|
return { ...input, url: "", selector: input.selector ?? "" };
|
|
@@ -66835,8 +67001,38 @@ var require_mobile_generator = __commonJS({
|
|
|
66835
67001
|
return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
|
|
66836
67002
|
}
|
|
66837
67003
|
var EDITABLE_ELEMENT_ROLE_RE = /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/;
|
|
67004
|
+
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;
|
|
66838
67005
|
function isEditableMobileElement(element) {
|
|
66839
|
-
|
|
67006
|
+
if (EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim()))
|
|
67007
|
+
return true;
|
|
67008
|
+
const identity = [element.accessibilityId, element.resourceId, element.label].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
67009
|
+
if (identity && EDITABLE_IDENTITY_RE.test(identity))
|
|
67010
|
+
return true;
|
|
67011
|
+
return false;
|
|
67012
|
+
}
|
|
67013
|
+
function isIdentityBearingTapTarget(element) {
|
|
67014
|
+
if (weakAssertionTargetReason(element))
|
|
67015
|
+
return false;
|
|
67016
|
+
if (isBareContainerLikeElement(element))
|
|
67017
|
+
return false;
|
|
67018
|
+
if (isEditableMobileElement(element))
|
|
67019
|
+
return false;
|
|
67020
|
+
if (/button|link|tab|menuitem|checkbox|switch|radio/i.test(element.role))
|
|
67021
|
+
return true;
|
|
67022
|
+
const label = [
|
|
67023
|
+
element.label,
|
|
67024
|
+
element.text,
|
|
67025
|
+
element.accessibilityId,
|
|
67026
|
+
element.resourceId
|
|
67027
|
+
].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
67028
|
+
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);
|
|
67029
|
+
}
|
|
67030
|
+
function isBareContainerLikeElement(element) {
|
|
67031
|
+
if (BARE_CONTAINER_CLASS_RE.test(element.role)) {
|
|
67032
|
+
const hasIdentity = Boolean(nonEmpty2(element.text) || nonEmpty2(element.label) || nonEmpty2(element.accessibilityId));
|
|
67033
|
+
return !hasIdentity;
|
|
67034
|
+
}
|
|
67035
|
+
return false;
|
|
66840
67036
|
}
|
|
66841
67037
|
function nonEmpty2(value) {
|
|
66842
67038
|
if (typeof value !== "string")
|
|
@@ -67060,10 +67256,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
67060
67256
|
externalOwner: boundary.owner
|
|
67061
67257
|
};
|
|
67062
67258
|
}
|
|
67063
|
-
|
|
67259
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
67260
|
+
let lastRecovered = recovered;
|
|
67261
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
67262
|
+
try {
|
|
67263
|
+
await driver.launchApp();
|
|
67264
|
+
const retrySnap = await driver.snapshot();
|
|
67265
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.screenSignal, retrySnap.windowSize);
|
|
67266
|
+
if (!retryRecovered)
|
|
67267
|
+
continue;
|
|
67268
|
+
lastRecovered = retryRecovered;
|
|
67269
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
67270
|
+
setLatest(retryRecovered);
|
|
67271
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(retryBoundary)) {
|
|
67272
|
+
emitScreenshot(retrySnap.screenshot);
|
|
67273
|
+
log2(` recovered target app after ${attempt} additional activation attempt(s).`);
|
|
67274
|
+
return {
|
|
67275
|
+
resolved: retryRecovered,
|
|
67276
|
+
observation: retryRecovered.observation,
|
|
67277
|
+
externalBlocked: true,
|
|
67278
|
+
externalOwner: boundary.owner
|
|
67279
|
+
};
|
|
67280
|
+
}
|
|
67281
|
+
if (retryBoundary.kind === "transient_external") {
|
|
67282
|
+
log2(` recovery surfaced system overlay ${retryBoundary.owner}; not recording it as a target-app step.`);
|
|
67283
|
+
return {
|
|
67284
|
+
resolved: retryRecovered,
|
|
67285
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
67286
|
+
externalBlocked: true,
|
|
67287
|
+
transientExternal: true,
|
|
67288
|
+
externalOwner: boundary.owner
|
|
67289
|
+
};
|
|
67290
|
+
}
|
|
67291
|
+
lastExternalOwner = retryBoundary.owner;
|
|
67292
|
+
} catch (err) {
|
|
67293
|
+
log2(` target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
67294
|
+
}
|
|
67295
|
+
}
|
|
67296
|
+
log2(` relaunch still outside target app (${lastExternalOwner}); no target-app step recorded.`);
|
|
67064
67297
|
return {
|
|
67065
|
-
resolved:
|
|
67066
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
67298
|
+
resolved: lastRecovered,
|
|
67299
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no target-app step was recorded)`,
|
|
67067
67300
|
externalBlocked: true,
|
|
67068
67301
|
externalOwner: boundary.owner
|
|
67069
67302
|
};
|
|
@@ -67277,9 +67510,26 @@ ${statePacket}` },
|
|
|
67277
67510
|
return unknownRef(toolArgs.ref);
|
|
67278
67511
|
const beforeScreenName = screenName();
|
|
67279
67512
|
const before = latest();
|
|
67280
|
-
|
|
67513
|
+
let result = await driver.tap(element.bounds);
|
|
67514
|
+
if (!result.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(result.error, before?.screen.elements ?? [])) {
|
|
67515
|
+
log2(" tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
67516
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
67517
|
+
const retryResult = await driver.tap(element.bounds);
|
|
67518
|
+
result = retryResult.ok ? retryResult : {
|
|
67519
|
+
...retryResult,
|
|
67520
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
67521
|
+
source: retryResult.source ?? dismissResult.source,
|
|
67522
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
67523
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
67524
|
+
};
|
|
67525
|
+
}
|
|
67281
67526
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67282
|
-
|
|
67527
|
+
const screenChanged = changedScreen(before, absorbed);
|
|
67528
|
+
const identityTap = isIdentityBearingTapTarget(element);
|
|
67529
|
+
if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
|
|
67530
|
+
if (!screenChanged && identityTap) {
|
|
67531
|
+
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67532
|
+
}
|
|
67283
67533
|
recordAction({
|
|
67284
67534
|
type: "TAP",
|
|
67285
67535
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
@@ -67423,6 +67673,13 @@ ${statePacket}` },
|
|
|
67423
67673
|
const element = resolveRef(toolArgs.ref);
|
|
67424
67674
|
if (!element)
|
|
67425
67675
|
return unknownRef(toolArgs.ref);
|
|
67676
|
+
const weakReason = weakAssertionTargetReason(element);
|
|
67677
|
+
if (weakReason) {
|
|
67678
|
+
return {
|
|
67679
|
+
ok: false,
|
|
67680
|
+
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.`
|
|
67681
|
+
};
|
|
67682
|
+
}
|
|
67426
67683
|
const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
|
|
67427
67684
|
if (!expected)
|
|
67428
67685
|
return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
|
|
@@ -67681,19 +67938,46 @@ ${statePacket}` },
|
|
|
67681
67938
|
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67682
67939
|
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67683
67940
|
}
|
|
67684
|
-
if (
|
|
67941
|
+
if (isCredentialSignIn) {
|
|
67942
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
67943
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
67944
|
+
const tappedSubmit = submitAfterCredentials;
|
|
67945
|
+
if (typedEmail && typedPassword && tappedSubmit) {
|
|
67946
|
+
} else {
|
|
67947
|
+
const missing = [
|
|
67948
|
+
typedEmail ? null : "email/username entry",
|
|
67949
|
+
typedPassword ? null : "password entry",
|
|
67950
|
+
tappedSubmit ? null : "sign-in submit tap"
|
|
67951
|
+
].filter((value) => value !== null);
|
|
67952
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67953
|
+
}
|
|
67954
|
+
}
|
|
67955
|
+
const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
|
|
67956
|
+
if (!isHappyPath)
|
|
67685
67957
|
return null;
|
|
67686
|
-
|
|
67687
|
-
|
|
67688
|
-
|
|
67689
|
-
|
|
67958
|
+
let lastTypeIndex = -1;
|
|
67959
|
+
let lastAssertIndex = -1;
|
|
67960
|
+
actions.forEach((action, index) => {
|
|
67961
|
+
if (action.type === "TYPE")
|
|
67962
|
+
lastTypeIndex = index;
|
|
67963
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
67964
|
+
lastAssertIndex = index;
|
|
67965
|
+
});
|
|
67966
|
+
if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
|
|
67690
67967
|
return null;
|
|
67691
|
-
const
|
|
67692
|
-
|
|
67693
|
-
|
|
67694
|
-
|
|
67695
|
-
|
|
67696
|
-
|
|
67968
|
+
const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
|
|
67969
|
+
const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
|
|
67970
|
+
const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
|
|
67971
|
+
const commitAfterType = actions.some((action, index) => {
|
|
67972
|
+
if (action.type !== "TAP" || index <= lastTypeIndex || index >= lastAssertIndex)
|
|
67973
|
+
return false;
|
|
67974
|
+
const textForAction = actionText(action);
|
|
67975
|
+
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);
|
|
67976
|
+
});
|
|
67977
|
+
if (screensDiffer && !commitAfterType) {
|
|
67978
|
+
return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
|
|
67979
|
+
}
|
|
67980
|
+
return null;
|
|
67697
67981
|
}
|
|
67698
67982
|
function actionCycleKey(action) {
|
|
67699
67983
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
@@ -68628,8 +68912,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68628
68912
|
function coerceSteps(value) {
|
|
68629
68913
|
if (!Array.isArray(value) || value.length === 0)
|
|
68630
68914
|
return void 0;
|
|
68631
|
-
const
|
|
68632
|
-
|
|
68915
|
+
const steps = [];
|
|
68916
|
+
for (let i = 0; i < value.length; i++) {
|
|
68917
|
+
const step = value[i];
|
|
68918
|
+
if (!isRecord(step) || typeof step.action !== "string" || !step.action.trim()) {
|
|
68919
|
+
return void 0;
|
|
68920
|
+
}
|
|
68921
|
+
const orderRaw = step.order;
|
|
68922
|
+
const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : i;
|
|
68923
|
+
steps.push({
|
|
68924
|
+
...step,
|
|
68925
|
+
order,
|
|
68926
|
+
action: step.action
|
|
68927
|
+
});
|
|
68928
|
+
}
|
|
68929
|
+
return steps.map((step, order) => ({ ...step, order }));
|
|
68633
68930
|
}
|
|
68634
68931
|
function parseHealVerdict(content) {
|
|
68635
68932
|
const cleaned = stripCodeFences(content);
|
|
@@ -72850,6 +73147,48 @@ var require_snapshot_observation = __commonJS({
|
|
|
72850
73147
|
var PROMOTED_FORM_FIELD_CAP = 12;
|
|
72851
73148
|
var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
|
|
72852
73149
|
var DATA_GRID_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:table|grid|rowgroup|row|gridcell|cell)\b/i;
|
|
73150
|
+
var DEEP_NONNAV_CAP_CENSUS = 16;
|
|
73151
|
+
var CENSUS_NAV_LINK_CAP = 60;
|
|
73152
|
+
var CENSUS_MIN_ACTION_GROUP = 3;
|
|
73153
|
+
var DESTRUCTIVE_ACTION_VERB_RE = /^(?:delete|remove|destroy|suspend|deactivate|disable|revoke|ban|block|archive|terminate|reject|cancel|reset|purge|wipe|unpublish|unsubscribe)$/i;
|
|
73154
|
+
function leadingActionVerb(label) {
|
|
73155
|
+
if (!label)
|
|
73156
|
+
return null;
|
|
73157
|
+
const m = label.trim().match(/[A-Za-z]+/);
|
|
73158
|
+
return m ? m[0].toLowerCase() : null;
|
|
73159
|
+
}
|
|
73160
|
+
function isNavLinkEntry(entry) {
|
|
73161
|
+
return entry.element.elementType === "LINK" && !!entry.element.triggersNavigation;
|
|
73162
|
+
}
|
|
73163
|
+
function isRowRepeatedControl(element) {
|
|
73164
|
+
return !!element.enrichment?.rowContext || !!element.containerPath && DATA_GRID_CONTAINER_SEGMENT_RE.test(element.containerPath);
|
|
73165
|
+
}
|
|
73166
|
+
function rowContainerScope(containerPath) {
|
|
73167
|
+
if (!containerPath)
|
|
73168
|
+
return null;
|
|
73169
|
+
const m = containerPath.match(/^(.*?)\s*>\s*row\b/i);
|
|
73170
|
+
return m ? m[1].trim() : containerPath.trim();
|
|
73171
|
+
}
|
|
73172
|
+
function containerScopeLabel(scope) {
|
|
73173
|
+
if (!scope)
|
|
73174
|
+
return null;
|
|
73175
|
+
const landmarks = [...scope.matchAll(/(?:table|grid|treegrid|list|listbox|tree|rowgroup):([^>]+)/gi)];
|
|
73176
|
+
if (landmarks.length > 0)
|
|
73177
|
+
return landmarks[landmarks.length - 1][1].trim();
|
|
73178
|
+
const segs = scope.split(">").map((s) => s.trim()).filter(Boolean);
|
|
73179
|
+
return segs.length ? segs[segs.length - 1] : null;
|
|
73180
|
+
}
|
|
73181
|
+
function rowIdentityKey(element) {
|
|
73182
|
+
const rc = element.enrichment?.rowContext;
|
|
73183
|
+
if (rc)
|
|
73184
|
+
return `rc:${rc}`;
|
|
73185
|
+
const cp = element.containerPath;
|
|
73186
|
+
if (cp) {
|
|
73187
|
+
const m = cp.match(/>\s*row:(.+)$/i);
|
|
73188
|
+
return m ? `row:${m[1].trim()}` : `cp:${cp}`;
|
|
73189
|
+
}
|
|
73190
|
+
return element.ref ? `ref:${element.ref}` : "unknown";
|
|
73191
|
+
}
|
|
72853
73192
|
function isGroupedFormField(element) {
|
|
72854
73193
|
if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
|
|
72855
73194
|
return false;
|
|
@@ -72865,6 +73204,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
72865
73204
|
const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
|
|
72866
73205
|
const artifact = page.snapshotArtifact;
|
|
72867
73206
|
const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
|
|
73207
|
+
const rowActionCensus = richRender && options.mode !== "survey" && (options.rowActionCensus ?? process.env.DISCOVERY_SNAPSHOT_ROW_CENSUS === "true");
|
|
72868
73208
|
const activeScope = inferActiveSnapshotScope(page);
|
|
72869
73209
|
const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
|
|
72870
73210
|
const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
|
|
@@ -72878,10 +73218,17 @@ var require_snapshot_observation = __commonJS({
|
|
|
72878
73218
|
});
|
|
72879
73219
|
let interactive;
|
|
72880
73220
|
if (options.mode === "survey") {
|
|
72881
|
-
const navLinks = allInteractive.filter(
|
|
72882
|
-
const others = allInteractive.filter((
|
|
73221
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73222
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
72883
73223
|
const othersCap = richRender ? 6 : 4;
|
|
72884
73224
|
interactive = [...navLinks, ...others.slice(0, othersCap)];
|
|
73225
|
+
} else if (rowActionCensus) {
|
|
73226
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73227
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
73228
|
+
interactive = [
|
|
73229
|
+
...navLinks.slice(0, CENSUS_NAV_LINK_CAP),
|
|
73230
|
+
...others.slice(0, options.elementLimit ?? DEEP_NONNAV_CAP_CENSUS)
|
|
73231
|
+
];
|
|
72885
73232
|
} else {
|
|
72886
73233
|
interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
|
|
72887
73234
|
}
|
|
@@ -72894,6 +73241,36 @@ var require_snapshot_observation = __commonJS({
|
|
|
72894
73241
|
}
|
|
72895
73242
|
}
|
|
72896
73243
|
}
|
|
73244
|
+
if (rowActionCensus && interactive.length > 0) {
|
|
73245
|
+
const shownVerbs = new Set(interactive.filter((entry) => !isNavLinkEntry(entry) && isRowRepeatedControl(entry.element)).map(({ element }) => leadingActionVerb(qualifyElementLabel(element))).filter(Boolean));
|
|
73246
|
+
const bestByVerb = /* @__PURE__ */ new Map();
|
|
73247
|
+
for (const entry of allInteractive) {
|
|
73248
|
+
if (isNavLinkEntry(entry) || !isRowRepeatedControl(entry.element))
|
|
73249
|
+
continue;
|
|
73250
|
+
const verb = leadingActionVerb(qualifyElementLabel(entry.element));
|
|
73251
|
+
if (!verb || !DESTRUCTIVE_ACTION_VERB_RE.test(verb))
|
|
73252
|
+
continue;
|
|
73253
|
+
if (shownVerbs.has(verb))
|
|
73254
|
+
continue;
|
|
73255
|
+
if (interactive.includes(entry))
|
|
73256
|
+
continue;
|
|
73257
|
+
const existing = bestByVerb.get(verb);
|
|
73258
|
+
if (!existing) {
|
|
73259
|
+
bestByVerb.set(verb, entry);
|
|
73260
|
+
continue;
|
|
73261
|
+
}
|
|
73262
|
+
if (existing.element.isDisabled && !entry.element.isDisabled)
|
|
73263
|
+
bestByVerb.set(verb, entry);
|
|
73264
|
+
}
|
|
73265
|
+
const swapIns = [...bestByVerb.values()];
|
|
73266
|
+
if (swapIns.length > 0) {
|
|
73267
|
+
const navShown = interactive.filter(isNavLinkEntry);
|
|
73268
|
+
const nonNavShown = interactive.filter((entry) => !isNavLinkEntry(entry));
|
|
73269
|
+
const evictable = Math.min(swapIns.length, nonNavShown.length);
|
|
73270
|
+
const trimmed = nonNavShown.slice(0, nonNavShown.length - evictable);
|
|
73271
|
+
interactive = [...navShown, ...trimmed, ...swapIns.slice(0, evictable)];
|
|
73272
|
+
}
|
|
73273
|
+
}
|
|
72897
73274
|
const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
|
|
72898
73275
|
const allForms = artifact?.formGroups ?? page.formGroups ?? [];
|
|
72899
73276
|
const forms = allForms.slice(0, options.formLimit ?? 3);
|
|
@@ -73193,6 +73570,54 @@ var require_snapshot_observation = __commonJS({
|
|
|
73193
73570
|
const hidden = allInteractive.length - interactive.length;
|
|
73194
73571
|
if (hidden > 0) {
|
|
73195
73572
|
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.`);
|
|
73573
|
+
if (rowActionCensus) {
|
|
73574
|
+
const shownSet = new Set(interactive.map((entry) => entry.element));
|
|
73575
|
+
const groups = /* @__PURE__ */ new Map();
|
|
73576
|
+
for (const entry of allInteractive) {
|
|
73577
|
+
const el = entry.element;
|
|
73578
|
+
if (!isRowRepeatedControl(el))
|
|
73579
|
+
continue;
|
|
73580
|
+
const verb = leadingActionVerb(qualifyElementLabel(el));
|
|
73581
|
+
if (!verb)
|
|
73582
|
+
continue;
|
|
73583
|
+
const role = (el.role ?? el.elementType ?? "control").toLowerCase();
|
|
73584
|
+
const scope = rowContainerScope(el.containerPath);
|
|
73585
|
+
const key = JSON.stringify([scope ?? "", role, verb]);
|
|
73586
|
+
let group = groups.get(key);
|
|
73587
|
+
if (!group) {
|
|
73588
|
+
group = { verb, role, scopeLabel: containerScopeLabel(scope), rows: /* @__PURE__ */ new Set(), shownRows: /* @__PURE__ */ new Set(), disabledRows: /* @__PURE__ */ new Set(), controls: 0 };
|
|
73589
|
+
groups.set(key, group);
|
|
73590
|
+
}
|
|
73591
|
+
const rowKey = rowIdentityKey(el);
|
|
73592
|
+
group.rows.add(rowKey);
|
|
73593
|
+
group.controls += 1;
|
|
73594
|
+
if (shownSet.has(el))
|
|
73595
|
+
group.shownRows.add(rowKey);
|
|
73596
|
+
if (el.isDisabled)
|
|
73597
|
+
group.disabledRows.add(rowKey);
|
|
73598
|
+
}
|
|
73599
|
+
const collapsed = [...groups.values()].map((g) => ({
|
|
73600
|
+
verb: g.verb,
|
|
73601
|
+
role: g.role,
|
|
73602
|
+
scopeLabel: g.scopeLabel,
|
|
73603
|
+
total: g.rows.size,
|
|
73604
|
+
shown: g.shownRows.size,
|
|
73605
|
+
disabled: g.disabledRows.size,
|
|
73606
|
+
controls: g.controls
|
|
73607
|
+
})).filter((g) => g.total >= CENSUS_MIN_ACTION_GROUP && g.total > g.shown).sort((a, b) => {
|
|
73608
|
+
const aDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(a.verb) ? 0 : 1;
|
|
73609
|
+
const bDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(b.verb) ? 0 : 1;
|
|
73610
|
+
return aDestructive - bDestructive || b.total - b.shown - (a.total - a.shown);
|
|
73611
|
+
});
|
|
73612
|
+
for (const g of collapsed) {
|
|
73613
|
+
const verbLabel = g.verb.charAt(0).toUpperCase() + g.verb.slice(1);
|
|
73614
|
+
const destructiveTag = DESTRUCTIVE_ACTION_VERB_RE.test(g.verb) ? " [destructive]" : "";
|
|
73615
|
+
const where = g.scopeLabel ? ` in ${yamlScalar(g.scopeLabel)}` : "";
|
|
73616
|
+
const controlsNote = g.controls > g.total ? ` (${g.controls} controls across ${g.total} rows)` : "";
|
|
73617
|
+
const disabledNote = g.disabled > 0 ? `; ${g.disabled} disabled at capture` : "";
|
|
73618
|
+
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.`);
|
|
73619
|
+
}
|
|
73620
|
+
}
|
|
73196
73621
|
}
|
|
73197
73622
|
}
|
|
73198
73623
|
}
|
|
@@ -74215,13 +74640,14 @@ var require_auth_recipe = __commonJS({
|
|
|
74215
74640
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
74216
74641
|
"use strict";
|
|
74217
74642
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
74218
|
-
exports2.AUTH_MARKER_VERSION = void 0;
|
|
74643
|
+
exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
|
|
74219
74644
|
exports2.computeRecipeHash = computeRecipeHash;
|
|
74220
74645
|
exports2.buildAuthRecipe = buildAuthRecipe;
|
|
74221
74646
|
exports2.authProofLocator = authProofLocator;
|
|
74222
74647
|
exports2.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
74223
74648
|
exports2.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
74224
74649
|
exports2.renderAuthFn = renderAuthFn;
|
|
74650
|
+
exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
74225
74651
|
exports2.absoluteRoute = absoluteRoute;
|
|
74226
74652
|
exports2.renderReplayClickArrow = renderReplayClickArrow;
|
|
74227
74653
|
exports2.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
@@ -74340,6 +74766,17 @@ var require_auth_recipe = __commonJS({
|
|
|
74340
74766
|
lines.push(END_MARKER);
|
|
74341
74767
|
return lines.join("\n");
|
|
74342
74768
|
}
|
|
74769
|
+
exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
74770
|
+
"AUTH_FILE",
|
|
74771
|
+
"SHARED_AUTH_INJECT",
|
|
74772
|
+
"VALIDATE_RUN_ID",
|
|
74773
|
+
"VALIDATE_TASK_ARN",
|
|
74774
|
+
"VALIDATE_RUNNER_AUTH",
|
|
74775
|
+
"VALIDATE_SERVER_URL"
|
|
74776
|
+
]);
|
|
74777
|
+
function isReservedRunnerEnvVar(key) {
|
|
74778
|
+
return key.startsWith("VALIDATE_") || exports2.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
74779
|
+
}
|
|
74343
74780
|
function absoluteRoute(route2, baseUrl) {
|
|
74344
74781
|
try {
|
|
74345
74782
|
return new URL(route2, baseUrl).href;
|
|
@@ -315412,8 +315849,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315412
315849
|
};
|
|
315413
315850
|
}
|
|
315414
315851
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
315415
|
-
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", log2);
|
|
315416
|
-
log2(exploreAppDataCleared ? "App data cleared \u2014 exploring from a deterministic cold (logged-out) state." : target?.resetPolicy === "PRESERVE_APP_DATA" ? "App data reset skipped by policy \u2014 exploring with preserved app data." : "App data reset unavailable (platform/device policy) \u2014 exploring the app in its current state.");
|
|
315852
|
+
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", mode, log2);
|
|
315853
|
+
log2(exploreAppDataCleared ? "App data cleared \u2014 exploring from a deterministic cold (logged-out) state." : target?.resetPolicy === "PRESERVE_APP_DATA" ? "App data reset skipped by policy \u2014 exploring with preserved app data." : mode === "survey" && (target?.resetPolicy ?? "AUTO") === "AUTO" ? "App data reset skipped for mobile survey AUTO \u2014 exploring with preserved app data. Use resetPolicy=CLEAR_APP_DATA for a cold logged-out survey." : "App data reset unavailable (platform/device policy) \u2014 exploring the app in its current state.");
|
|
315417
315854
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
315418
315855
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
315419
315856
|
const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
|
|
@@ -315444,9 +315881,15 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315444
315881
|
log2(`
|
|
315445
315882
|
Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
|
|
315446
315883
|
if (callbacks?.onExploreComplete) {
|
|
315447
|
-
|
|
315448
|
-
|
|
315449
|
-
})
|
|
315884
|
+
try {
|
|
315885
|
+
await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions));
|
|
315886
|
+
} catch (err) {
|
|
315887
|
+
const message = checkpointErrorMessage(err);
|
|
315888
|
+
log2(`[stream] explore-complete callback failed: ${message}`);
|
|
315889
|
+
if (isTerminalDiscoveryCheckpointError(err)) {
|
|
315890
|
+
throw err;
|
|
315891
|
+
}
|
|
315892
|
+
}
|
|
315450
315893
|
}
|
|
315451
315894
|
if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
|
|
315452
315895
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
@@ -315474,8 +315917,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315474
315917
|
pausePhase: "explore"
|
|
315475
315918
|
};
|
|
315476
315919
|
}
|
|
315477
|
-
|
|
315478
|
-
|
|
315920
|
+
if (!isSurvey) {
|
|
315921
|
+
log2("\nRelaunching app for a clean state before planning/generation...");
|
|
315922
|
+
await driver.relaunch();
|
|
315923
|
+
}
|
|
315479
315924
|
log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
|
|
315480
315925
|
const graph = state2.toScreenGraph();
|
|
315481
315926
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
@@ -315498,7 +315943,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315498
315943
|
hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
|
|
315499
315944
|
},
|
|
315500
315945
|
incremental: ctx.incremental,
|
|
315501
|
-
testBudget
|
|
315946
|
+
testBudget,
|
|
315947
|
+
userNotes: ctx.userNotes,
|
|
315948
|
+
featureContext: ctx.featureContext
|
|
315502
315949
|
}, {
|
|
315503
315950
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
315504
315951
|
onAICall
|
|
@@ -315655,10 +316102,11 @@ Fatal error: ${msg}`);
|
|
|
315655
316102
|
};
|
|
315656
316103
|
}
|
|
315657
316104
|
}
|
|
315658
|
-
async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
|
|
316105
|
+
async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
|
|
315659
316106
|
void phase;
|
|
315660
316107
|
const resetPolicy = target?.resetPolicy ?? "AUTO";
|
|
315661
|
-
|
|
316108
|
+
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
316109
|
+
if (shouldPreserveAppData) {
|
|
315662
316110
|
try {
|
|
315663
316111
|
await driver.relaunch();
|
|
315664
316112
|
} catch (err) {
|
|
@@ -315670,10 +316118,20 @@ Fatal error: ${msg}`);
|
|
|
315670
316118
|
log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
315671
316119
|
return false;
|
|
315672
316120
|
}) ?? false;
|
|
315673
|
-
if (
|
|
315674
|
-
|
|
316121
|
+
if (cleared)
|
|
316122
|
+
return true;
|
|
316123
|
+
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
316124
|
+
log2("CLEAR_APP_DATA requested but app-data reset was unavailable; a post-explore relaunch will reset state before planning.");
|
|
316125
|
+
}
|
|
316126
|
+
if (mode !== "survey") {
|
|
316127
|
+
return false;
|
|
315675
316128
|
}
|
|
315676
|
-
|
|
316129
|
+
try {
|
|
316130
|
+
await driver.relaunch();
|
|
316131
|
+
} catch (err) {
|
|
316132
|
+
log2(`App relaunch failed after unsuccessful app-data reset: ${err instanceof Error ? err.message : String(err)}`);
|
|
316133
|
+
}
|
|
316134
|
+
return false;
|
|
315677
316135
|
}
|
|
315678
316136
|
function shouldPauseAfterTruncatedExplore(explore) {
|
|
315679
316137
|
return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
|
|
@@ -315720,6 +316178,7 @@ var require_discover_agent = __commonJS({
|
|
|
315720
316178
|
var claude_discovery_runner_js_1 = require_claude_discovery_runner();
|
|
315721
316179
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
315722
316180
|
var canvas_detector_js_1 = require_canvas_detector();
|
|
316181
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
315723
316182
|
async function runCanvasPreflight(ctx, sharedBrowser, log2) {
|
|
315724
316183
|
const cached = ctx.appRenderType ?? null;
|
|
315725
316184
|
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
@@ -316415,7 +316874,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
316415
316874
|
}
|
|
316416
316875
|
if (envRefs.size > 0) {
|
|
316417
316876
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
316418
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
316877
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
316419
316878
|
if (missing.length > 0) {
|
|
316420
316879
|
autoVariableDefaults = {};
|
|
316421
316880
|
for (const key of missing) {
|
|
@@ -316931,7 +317390,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
316931
317390
|
envRefs.add(match[1]);
|
|
316932
317391
|
if (envRefs.size > 0) {
|
|
316933
317392
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
316934
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
317393
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
316935
317394
|
if (missing.length > 0) {
|
|
316936
317395
|
autoVariableDefaults = {};
|
|
316937
317396
|
for (const key of missing) {
|
|
@@ -329813,9 +330272,9 @@ var require_dist2 = __commonJS({
|
|
|
329813
330272
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
329814
330273
|
};
|
|
329815
330274
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
329816
|
-
exports2.
|
|
329817
|
-
exports2.
|
|
329818
|
-
exports2.UNSUPPORTED_RENDERER_PREFIX = void 0;
|
|
330275
|
+
exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.isReservedRunnerEnvVar = exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.canonicalizeRoute = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
|
|
330276
|
+
exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = void 0;
|
|
330277
|
+
exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = void 0;
|
|
329819
330278
|
__exportStar(require_ai_provider(), exports2);
|
|
329820
330279
|
__exportStar(require_mobile_types(), exports2);
|
|
329821
330280
|
__exportStar(require_mobile_source_parser(), exports2);
|
|
@@ -329952,6 +330411,12 @@ var require_dist2 = __commonJS({
|
|
|
329952
330411
|
Object.defineProperty(exports2, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
|
|
329953
330412
|
return auth_recipe_js_1.AUTH_MARKER_VERSION;
|
|
329954
330413
|
} });
|
|
330414
|
+
Object.defineProperty(exports2, "RESERVED_RUNNER_ENV_VARS", { enumerable: true, get: function() {
|
|
330415
|
+
return auth_recipe_js_1.RESERVED_RUNNER_ENV_VARS;
|
|
330416
|
+
} });
|
|
330417
|
+
Object.defineProperty(exports2, "isReservedRunnerEnvVar", { enumerable: true, get: function() {
|
|
330418
|
+
return auth_recipe_js_1.isReservedRunnerEnvVar;
|
|
330419
|
+
} });
|
|
329955
330420
|
var auth_capture_run_js_1 = require_auth_capture_run();
|
|
329956
330421
|
Object.defineProperty(exports2, "runLoginCaptureRun", { enumerable: true, get: function() {
|
|
329957
330422
|
return auth_capture_run_js_1.runLoginCaptureRun;
|
|
@@ -330179,7 +330644,7 @@ var require_package4 = __commonJS({
|
|
|
330179
330644
|
"package.json"(exports2, module2) {
|
|
330180
330645
|
module2.exports = {
|
|
330181
330646
|
name: "@validate.qa/runner",
|
|
330182
|
-
version: "1.0.
|
|
330647
|
+
version: "1.0.33",
|
|
330183
330648
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330184
330649
|
bin: {
|
|
330185
330650
|
"validate-runner": "dist/cli.js"
|
|
@@ -330795,25 +331260,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
|
330795
331260
|
}
|
|
330796
331261
|
return elementId;
|
|
330797
331262
|
}
|
|
331263
|
+
async function sendKeysToFocusedInput(sessionPath, value) {
|
|
331264
|
+
await appiumRequest(`${sessionPath}/keys`, {
|
|
331265
|
+
method: "POST",
|
|
331266
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
331267
|
+
});
|
|
331268
|
+
}
|
|
330798
331269
|
async function typeText(sessionPath, value, bounds) {
|
|
330799
331270
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330800
|
-
if (
|
|
330801
|
-
|
|
331271
|
+
if (elementId) {
|
|
331272
|
+
try {
|
|
331273
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
331274
|
+
method: "POST",
|
|
331275
|
+
body: JSON.stringify({ text: value })
|
|
331276
|
+
});
|
|
331277
|
+
return;
|
|
331278
|
+
} catch (err) {
|
|
331279
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331280
|
+
}
|
|
330802
331281
|
}
|
|
330803
|
-
await
|
|
330804
|
-
method: "POST",
|
|
330805
|
-
body: JSON.stringify({ text: value })
|
|
330806
|
-
});
|
|
331282
|
+
await sendKeysToFocusedInput(sessionPath, value);
|
|
330807
331283
|
}
|
|
330808
331284
|
async function clearField(sessionPath, bounds) {
|
|
330809
331285
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330810
|
-
if (
|
|
330811
|
-
|
|
331286
|
+
if (elementId) {
|
|
331287
|
+
try {
|
|
331288
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
331289
|
+
method: "POST",
|
|
331290
|
+
body: JSON.stringify({})
|
|
331291
|
+
});
|
|
331292
|
+
return;
|
|
331293
|
+
} catch (err) {
|
|
331294
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331295
|
+
}
|
|
331296
|
+
}
|
|
331297
|
+
try {
|
|
331298
|
+
await appiumRequest(`${sessionPath}/keys`, {
|
|
331299
|
+
method: "POST",
|
|
331300
|
+
// Common select-all + delete sequence is platform-dependent; empty keys
|
|
331301
|
+
// after focus is the portable baseline Appium accepts for many hybrids.
|
|
331302
|
+
body: JSON.stringify({ text: "", value: [] })
|
|
331303
|
+
});
|
|
331304
|
+
} catch (err) {
|
|
331305
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331306
|
+
throw new Error(
|
|
331307
|
+
`No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
|
|
331308
|
+
);
|
|
330812
331309
|
}
|
|
330813
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
330814
|
-
method: "POST",
|
|
330815
|
-
body: JSON.stringify({})
|
|
330816
|
-
});
|
|
330817
331310
|
}
|
|
330818
331311
|
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
330819
331312
|
const dist = clampMobileSwipeDistance(distance);
|
|
@@ -330843,6 +331336,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
|
|
|
330843
331336
|
async function pressBack(sessionPath) {
|
|
330844
331337
|
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
330845
331338
|
}
|
|
331339
|
+
function isKeyboardAlreadyHiddenError(err) {
|
|
331340
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331341
|
+
return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
|
|
331342
|
+
}
|
|
331343
|
+
async function dismissKeyboard(sessionPath, platform3) {
|
|
331344
|
+
if (platform3 === "ANDROID") {
|
|
331345
|
+
await pressBack(sessionPath);
|
|
331346
|
+
return;
|
|
331347
|
+
}
|
|
331348
|
+
try {
|
|
331349
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, {
|
|
331350
|
+
method: "POST",
|
|
331351
|
+
body: "{}"
|
|
331352
|
+
});
|
|
331353
|
+
return;
|
|
331354
|
+
} catch (err) {
|
|
331355
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331356
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331357
|
+
}
|
|
331358
|
+
try {
|
|
331359
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
331360
|
+
method: "POST",
|
|
331361
|
+
body: JSON.stringify({ script: "mobile: hideKeyboard", args: [{}] })
|
|
331362
|
+
});
|
|
331363
|
+
return;
|
|
331364
|
+
} catch (err) {
|
|
331365
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331366
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331367
|
+
}
|
|
331368
|
+
await pressBack(sessionPath);
|
|
331369
|
+
}
|
|
330846
331370
|
async function pressHome(sessionPath, platform3) {
|
|
330847
331371
|
if (platform3 === "IOS") {
|
|
330848
331372
|
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
@@ -332020,6 +332544,10 @@ async function startMobileNetworkCapture(options) {
|
|
|
332020
332544
|
|
|
332021
332545
|
// src/services/appium-executor.ts
|
|
332022
332546
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
332547
|
+
var ELEMENT_ID_PATTERN2 = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
332548
|
+
function validateElementId(elementId) {
|
|
332549
|
+
return typeof elementId === "string" && ELEMENT_ID_PATTERN2.test(elementId) ? elementId : null;
|
|
332550
|
+
}
|
|
332023
332551
|
var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
332024
332552
|
const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
332025
332553
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
|
|
@@ -332154,6 +332682,9 @@ async function createSession(target) {
|
|
|
332154
332682
|
});
|
|
332155
332683
|
const sessionId = result?.value?.sessionId ?? result?.sessionId;
|
|
332156
332684
|
if (!sessionId) throw new Error("Appium session creation returned no session ID");
|
|
332685
|
+
if (target.platform === "ANDROID") {
|
|
332686
|
+
await prepareAndroidInputEnvironment(target.deviceId);
|
|
332687
|
+
}
|
|
332157
332688
|
return sessionId;
|
|
332158
332689
|
}
|
|
332159
332690
|
var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
|
|
@@ -332541,7 +333072,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
332541
333072
|
method: "POST",
|
|
332542
333073
|
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
332543
333074
|
});
|
|
332544
|
-
const elementId =
|
|
333075
|
+
const elementId = validateElementId(
|
|
333076
|
+
result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
|
|
333077
|
+
);
|
|
332545
333078
|
if (elementId) return elementId;
|
|
332546
333079
|
} catch (err) {
|
|
332547
333080
|
if (isTransportError(err)) throw err;
|
|
@@ -332581,24 +333114,25 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
332581
333114
|
})
|
|
332582
333115
|
});
|
|
332583
333116
|
}
|
|
332584
|
-
async function getActiveElementId2(sessionId) {
|
|
332585
|
-
try {
|
|
332586
|
-
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
332587
|
-
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
332588
|
-
} catch {
|
|
332589
|
-
return null;
|
|
332590
|
-
}
|
|
332591
|
-
}
|
|
332592
|
-
async function sendKeysToFocusedInput(sessionId, value) {
|
|
332593
|
-
await wdRequest(`/session/${sessionId}/keys`, {
|
|
332594
|
-
method: "POST",
|
|
332595
|
-
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
332596
|
-
});
|
|
332597
|
-
}
|
|
332598
333117
|
function encodeAndroidInputText(value) {
|
|
332599
333118
|
const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
332600
333119
|
return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
|
|
332601
333120
|
}
|
|
333121
|
+
async function prepareAndroidInputEnvironment(deviceId) {
|
|
333122
|
+
if (!deviceId) return;
|
|
333123
|
+
const settingsToDisable = [
|
|
333124
|
+
["secure", "stylus_handwriting_enabled", "0"]
|
|
333125
|
+
];
|
|
333126
|
+
for (const [namespace, key, value] of settingsToDisable) {
|
|
333127
|
+
try {
|
|
333128
|
+
await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
333129
|
+
timeout: 8e3,
|
|
333130
|
+
windowsHide: true
|
|
333131
|
+
});
|
|
333132
|
+
} catch {
|
|
333133
|
+
}
|
|
333134
|
+
}
|
|
333135
|
+
}
|
|
332602
333136
|
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
332603
333137
|
if (!deviceId) {
|
|
332604
333138
|
throw new Error("Android keyboard fallback requires a device id");
|
|
@@ -332660,100 +333194,28 @@ async function executeType(sessionId, step, context) {
|
|
|
332660
333194
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
332661
333195
|
const value = String(step.value);
|
|
332662
333196
|
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
332663
|
-
|
|
332664
|
-
|
|
332665
|
-
|
|
332666
|
-
|
|
332667
|
-
|
|
332668
|
-
|
|
332669
|
-
|
|
332670
|
-
|
|
332671
|
-
|
|
332672
|
-
|
|
332673
|
-
|
|
332674
|
-
|
|
332675
|
-
|
|
332676
|
-
|
|
332677
|
-
|
|
332678
|
-
try {
|
|
332679
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
332680
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332681
|
-
if (!lastInputError) return;
|
|
332682
|
-
elementId = null;
|
|
332683
|
-
} catch (error2) {
|
|
332684
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
332685
|
-
lastInputError = error2;
|
|
332686
|
-
}
|
|
332687
|
-
}
|
|
332688
|
-
if (!elementId && bounds) {
|
|
332689
|
-
await tapCoordinates(sessionId, bounds);
|
|
332690
|
-
tappedBounds = true;
|
|
332691
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332692
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332693
|
-
}
|
|
332694
|
-
if (!elementId) {
|
|
332695
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332696
|
-
}
|
|
332697
|
-
if (elementId) {
|
|
332698
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332699
|
-
if (!lastInputError) return;
|
|
332700
|
-
elementId = null;
|
|
332701
|
-
if (!bounds) {
|
|
332702
|
-
throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
|
|
332703
|
-
}
|
|
332704
|
-
}
|
|
332705
|
-
if (bounds) {
|
|
332706
|
-
if (!tappedBounds) {
|
|
332707
|
-
await tapCoordinates(sessionId, bounds);
|
|
332708
|
-
tappedBounds = true;
|
|
332709
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332710
|
-
}
|
|
332711
|
-
try {
|
|
332712
|
-
await sendKeysToFocusedInput(sessionId, value);
|
|
332713
|
-
return;
|
|
332714
|
-
} catch (error2) {
|
|
332715
|
-
if (isTransportError(error2)) throw error2;
|
|
332716
|
-
lastInputError = error2;
|
|
332717
|
-
}
|
|
332718
|
-
}
|
|
332719
|
-
if (context.platform === "ANDROID" && tappedBounds) {
|
|
332720
|
-
try {
|
|
332721
|
-
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
332722
|
-
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
332723
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332724
|
-
return;
|
|
332725
|
-
} catch (error2) {
|
|
332726
|
-
if (isTransportError(error2)) throw error2;
|
|
332727
|
-
lastInputError = error2;
|
|
333197
|
+
const sessionPath = `/session/${sessionId}`;
|
|
333198
|
+
try {
|
|
333199
|
+
await typeText(sessionPath, value, bounds);
|
|
333200
|
+
return;
|
|
333201
|
+
} catch (error2) {
|
|
333202
|
+
if (isTransportError(error2)) throw error2;
|
|
333203
|
+
if (context.platform === "ANDROID" && bounds) {
|
|
333204
|
+
try {
|
|
333205
|
+
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
333206
|
+
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
333207
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
333208
|
+
return;
|
|
333209
|
+
} catch (androidError) {
|
|
333210
|
+
if (isTransportError(androidError)) throw androidError;
|
|
333211
|
+
}
|
|
332728
333212
|
}
|
|
333213
|
+
throw error2 instanceof Error ? error2 : new Error(String(error2));
|
|
332729
333214
|
}
|
|
332730
|
-
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
332731
333215
|
}
|
|
332732
333216
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
332733
333217
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
332734
|
-
|
|
332735
|
-
if (step.target) {
|
|
332736
|
-
try {
|
|
332737
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
332738
|
-
} catch (error2) {
|
|
332739
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
332740
|
-
}
|
|
332741
|
-
}
|
|
332742
|
-
if (!elementId && bounds) {
|
|
332743
|
-
await tapCoordinates(sessionId, bounds);
|
|
332744
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332745
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332746
|
-
}
|
|
332747
|
-
if (!elementId) {
|
|
332748
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332749
|
-
}
|
|
332750
|
-
if (!elementId) {
|
|
332751
|
-
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
332752
|
-
}
|
|
332753
|
-
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
|
|
332754
|
-
method: "POST",
|
|
332755
|
-
body: "{}"
|
|
332756
|
-
});
|
|
333218
|
+
await clearField(`/session/${sessionId}`, bounds);
|
|
332757
333219
|
}
|
|
332758
333220
|
async function executeSwipe(sessionId, step) {
|
|
332759
333221
|
const direction = step.metadata?.direction || "up";
|
|
@@ -334836,6 +335298,10 @@ var MobileBridgeDriver = class {
|
|
|
334836
335298
|
async back() {
|
|
334837
335299
|
return this.runAction(() => pressBack(this.sessionPath));
|
|
334838
335300
|
}
|
|
335301
|
+
/** Platform-aware software keyboard dismissal. */
|
|
335302
|
+
async dismissKeyboard() {
|
|
335303
|
+
return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
|
|
335304
|
+
}
|
|
334839
335305
|
/** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
|
|
334840
335306
|
async home() {
|
|
334841
335307
|
return this.runAction(() => pressHome(this.sessionPath, this.platform));
|
|
@@ -335221,8 +335687,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
335221
335687
|
pageUrl: call.pageUrl ?? pageUrl
|
|
335222
335688
|
}));
|
|
335223
335689
|
}
|
|
335690
|
+
var runnerPackage = require_package4();
|
|
335224
335691
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
335225
335692
|
var IS_CLOUD = process.env.RUNNER_CLOUD === "true";
|
|
335693
|
+
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
335694
|
+
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
335695
|
+
var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION;
|
|
335226
335696
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
335227
335697
|
var DEFAULT_TYPE_LIMITS = {
|
|
335228
335698
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -338056,6 +338526,9 @@ async function startRunner(opts) {
|
|
|
338056
338526
|
method: "POST",
|
|
338057
338527
|
headers,
|
|
338058
338528
|
body: JSON.stringify({
|
|
338529
|
+
runnerName: RUNNER_NAME,
|
|
338530
|
+
runnerVersion: RUNNER_VERSION,
|
|
338531
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
338059
338532
|
runsCompleted,
|
|
338060
338533
|
activeRuns,
|
|
338061
338534
|
maxConcurrent: MAX_CONCURRENT,
|