@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.mjs
CHANGED
|
@@ -5006,6 +5006,29 @@ var require_mobile_observation = __commonJS({
|
|
|
5006
5006
|
}
|
|
5007
5007
|
});
|
|
5008
5008
|
|
|
5009
|
+
// ../runner-core/dist/services/mobile/mobile-keyboard.js
|
|
5010
|
+
var require_mobile_keyboard = __commonJS({
|
|
5011
|
+
"../runner-core/dist/services/mobile/mobile-keyboard.js"(exports) {
|
|
5012
|
+
"use strict";
|
|
5013
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5014
|
+
exports.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
|
|
5015
|
+
exports.dismissKeyboardForRetry = dismissKeyboardForRetry;
|
|
5016
|
+
var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
|
|
5017
|
+
var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
|
|
5018
|
+
function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
|
|
5019
|
+
if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
|
|
5020
|
+
return false;
|
|
5021
|
+
return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
|
|
5022
|
+
}
|
|
5023
|
+
async function dismissKeyboardForRetry(driver) {
|
|
5024
|
+
if (typeof driver.dismissKeyboard === "function") {
|
|
5025
|
+
return driver.dismissKeyboard();
|
|
5026
|
+
}
|
|
5027
|
+
return driver.back();
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
});
|
|
5031
|
+
|
|
5009
5032
|
// ../runner-core/dist/utils/scrub-credentials.js
|
|
5010
5033
|
var require_scrub_credentials = __commonJS({
|
|
5011
5034
|
"../runner-core/dist/utils/scrub-credentials.js"(exports) {
|
|
@@ -61987,6 +62010,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
61987
62010
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
61988
62011
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
61989
62012
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
62013
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
61990
62014
|
var credential_tools_js_1 = require_credential_tools();
|
|
61991
62015
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
61992
62016
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
@@ -61996,6 +62020,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
61996
62020
|
var MAX_RECENT_EXCHANGES = 24;
|
|
61997
62021
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
61998
62022
|
var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
|
|
62023
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
61999
62024
|
var MOBILE_SNAPSHOT_TOOL = {
|
|
62000
62025
|
type: "function",
|
|
62001
62026
|
function: {
|
|
@@ -62265,7 +62290,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
62265
62290
|
return { screen, signal, refMap, observation };
|
|
62266
62291
|
}
|
|
62267
62292
|
function buildStatePacket(input) {
|
|
62268
|
-
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
|
|
62293
|
+
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
|
|
62269
62294
|
const sections = [];
|
|
62270
62295
|
sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
|
|
62271
62296
|
sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
|
|
@@ -62278,10 +62303,46 @@ var require_mobile_explorer = __commonJS({
|
|
|
62278
62303
|
sections.push("");
|
|
62279
62304
|
if (mode === "survey") {
|
|
62280
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.`);
|
|
62281
62308
|
} else {
|
|
62282
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.");
|
|
62283
62310
|
}
|
|
62284
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
|
+
}
|
|
62285
62346
|
const screenCount = state2.getScreenCount();
|
|
62286
62347
|
sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
|
|
62287
62348
|
const currentScreenId = state2.getCurrentScreenId();
|
|
@@ -62438,7 +62499,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62438
62499
|
} catch {
|
|
62439
62500
|
}
|
|
62440
62501
|
};
|
|
62441
|
-
const mode = ctx.mode === "
|
|
62502
|
+
const mode = ctx.mode === "survey" ? "survey" : "deep";
|
|
62442
62503
|
const platform3 = platformForMedium(ctx.medium);
|
|
62443
62504
|
const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
|
|
62444
62505
|
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
@@ -62561,9 +62622,46 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62561
62622
|
externalOwner: boundary.owner
|
|
62562
62623
|
};
|
|
62563
62624
|
}
|
|
62564
|
-
|
|
62625
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
62626
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
62627
|
+
try {
|
|
62628
|
+
await driver.launchApp();
|
|
62629
|
+
const retrySnap = await driver.snapshot();
|
|
62630
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.windowSize, retrySnap.screenSignal);
|
|
62631
|
+
if (!retryRecovered)
|
|
62632
|
+
continue;
|
|
62633
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
62634
|
+
latest = retryRecovered;
|
|
62635
|
+
lastSnapshotIteration = iterationForObservation;
|
|
62636
|
+
if (retryBoundary.kind === "target" || retryBoundary.kind === "unknown") {
|
|
62637
|
+
const screenId = ingestSnapshot(retryRecovered, iterationForObservation);
|
|
62638
|
+
captureScreenshot(retrySnap.screenshot, screenId);
|
|
62639
|
+
log2(` Recovered target app after ${attempt} additional activation attempt(s).`);
|
|
62640
|
+
return {
|
|
62641
|
+
observation: retryRecovered.observation,
|
|
62642
|
+
screenId,
|
|
62643
|
+
externalBlocked: true,
|
|
62644
|
+
externalOwner: boundary.owner
|
|
62645
|
+
};
|
|
62646
|
+
}
|
|
62647
|
+
if (retryBoundary.kind === "transient_external") {
|
|
62648
|
+
log2(` Recovery surfaced system overlay ${retryBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
62649
|
+
return {
|
|
62650
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
62651
|
+
screenId: null,
|
|
62652
|
+
externalBlocked: true,
|
|
62653
|
+
transientExternal: true,
|
|
62654
|
+
externalOwner: boundary.owner
|
|
62655
|
+
};
|
|
62656
|
+
}
|
|
62657
|
+
lastExternalOwner = retryBoundary.owner;
|
|
62658
|
+
} catch (err) {
|
|
62659
|
+
log2(` Target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62660
|
+
}
|
|
62661
|
+
}
|
|
62662
|
+
log2(` Relaunch still outside target app (${lastExternalOwner}); no screen recorded.`);
|
|
62565
62663
|
return {
|
|
62566
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
62664
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no screen was recorded)`,
|
|
62567
62665
|
screenId: null,
|
|
62568
62666
|
externalBlocked: true,
|
|
62569
62667
|
externalOwner: boundary.owner
|
|
@@ -62689,7 +62787,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62689
62787
|
maxIterations,
|
|
62690
62788
|
latestObservation: getLatest()?.observation ?? null,
|
|
62691
62789
|
journeys,
|
|
62692
|
-
transitions: collectedTransitions
|
|
62790
|
+
transitions: collectedTransitions,
|
|
62791
|
+
featureName: ctx.featureName,
|
|
62792
|
+
featureContext: ctx.featureContext,
|
|
62793
|
+
userNotes: ctx.userNotes
|
|
62693
62794
|
});
|
|
62694
62795
|
const messages = [
|
|
62695
62796
|
{ role: "system", content: systemPrompt },
|
|
@@ -62941,7 +63042,19 @@ Exploration complete: ${summary}`);
|
|
|
62941
63042
|
const fromScreenId = state2.getCurrentScreenId();
|
|
62942
63043
|
const boundaryBeforeAction = currentBoundary();
|
|
62943
63044
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
62944
|
-
|
|
63045
|
+
let actionResult = await driver.tap(resolved.bounds);
|
|
63046
|
+
if (!actionResult.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(actionResult.error, latest()?.screen.elements ?? [])) {
|
|
63047
|
+
log2(" Tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
63048
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
63049
|
+
const retryResult = await driver.tap(resolved.bounds);
|
|
63050
|
+
actionResult = retryResult.ok ? retryResult : {
|
|
63051
|
+
...retryResult,
|
|
63052
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
63053
|
+
source: retryResult.source ?? dismissResult.source,
|
|
63054
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
63055
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
63056
|
+
};
|
|
63057
|
+
}
|
|
62945
63058
|
const absorbed = await absorbActionResult(actionResult, "mobile_tap");
|
|
62946
63059
|
const { observation, screenId } = absorbed;
|
|
62947
63060
|
if (!startedOutsideTarget && !absorbed.externalBlocked && !absorbed.transientExternal) {
|
|
@@ -65941,6 +66054,25 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
65941
66054
|
function targetAppScreens(graph, appId) {
|
|
65942
66055
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
65943
66056
|
}
|
|
66057
|
+
function isGenericNonFeatureScreen(state2) {
|
|
66058
|
+
const type = state2.screenType?.trim().toLowerCase();
|
|
66059
|
+
if (type === "error" || type === "permission")
|
|
66060
|
+
return true;
|
|
66061
|
+
if (type && type !== "unknown")
|
|
66062
|
+
return false;
|
|
66063
|
+
const actionableCount = state2.elements.filter((el) => el.actionable).length;
|
|
66064
|
+
if (actionableCount > 0)
|
|
66065
|
+
return false;
|
|
66066
|
+
const evidenceText = [
|
|
66067
|
+
state2.screenName,
|
|
66068
|
+
state2.screenType,
|
|
66069
|
+
...state2.observations ?? []
|
|
66070
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ");
|
|
66071
|
+
return /\b(empty|loading|blank|spinner|intermediate|placeholder|no actionable)\b/i.test(evidenceText);
|
|
66072
|
+
}
|
|
66073
|
+
function plannableTargetAppScreens(graph, appId) {
|
|
66074
|
+
return targetAppScreens(graph, appId).filter((state2) => !isGenericNonFeatureScreen(state2));
|
|
66075
|
+
}
|
|
65944
66076
|
exports.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.
|
|
65945
66077
|
|
|
65946
66078
|
## Output Discipline
|
|
@@ -65960,8 +66092,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65960
66092
|
5. CRUD screens for the same entity belong together
|
|
65961
66093
|
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).
|
|
65962
66094
|
7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
|
|
65963
|
-
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.
|
|
65964
|
-
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.
|
|
66095
|
+
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.
|
|
66096
|
+
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.
|
|
65965
66097
|
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.
|
|
65966
66098
|
|
|
65967
66099
|
## Output JSON Schema
|
|
@@ -66227,8 +66359,36 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66227
66359
|
if (context.featureName) {
|
|
66228
66360
|
sections.push(`## Feature: ${context.featureName}
|
|
66229
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()}`);
|
|
66230
66390
|
}
|
|
66231
|
-
const orderedScreens =
|
|
66391
|
+
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66232
66392
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66233
66393
|
sections.push(`## Screens Discovered (${orderedScreens.length})`);
|
|
66234
66394
|
for (const state2 of orderedScreens) {
|
|
@@ -66337,7 +66497,7 @@ ${constraints.join("\n")}`);
|
|
|
66337
66497
|
const now = /* @__PURE__ */ new Date();
|
|
66338
66498
|
const idByScreen = /* @__PURE__ */ new Map();
|
|
66339
66499
|
let pageSeq = 0;
|
|
66340
|
-
const orderedScreens =
|
|
66500
|
+
const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66341
66501
|
for (const state2 of orderedScreens) {
|
|
66342
66502
|
idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
|
|
66343
66503
|
}
|
|
@@ -66353,7 +66513,7 @@ ${constraints.join("\n")}`);
|
|
|
66353
66513
|
routePattern: null,
|
|
66354
66514
|
title: state2.screenName ?? null,
|
|
66355
66515
|
pageType: state2.screenType ?? "UNKNOWN",
|
|
66356
|
-
requiresAuth:
|
|
66516
|
+
requiresAuth: state2.screenType?.toLowerCase() === "auth",
|
|
66357
66517
|
visitCount: state2.visited ? 1 : 0,
|
|
66358
66518
|
lastVisitedAt: state2.visited ? now : null,
|
|
66359
66519
|
elementsCovered: 0,
|
|
@@ -66373,6 +66533,8 @@ ${constraints.join("\n")}`);
|
|
|
66373
66533
|
const toPageId = idByScreen.get(t.toScreenId);
|
|
66374
66534
|
if (!fromPageId || !toPageId)
|
|
66375
66535
|
continue;
|
|
66536
|
+
const fromScreen = graph.screens.get(t.fromScreenId);
|
|
66537
|
+
const toScreen = graph.screens.get(t.toScreenId);
|
|
66376
66538
|
transitions.push({
|
|
66377
66539
|
id: `mtx-${txSeq++}`,
|
|
66378
66540
|
fromPageId,
|
|
@@ -66380,7 +66542,7 @@ ${constraints.join("\n")}`);
|
|
|
66380
66542
|
triggerType: t.action.toUpperCase(),
|
|
66381
66543
|
triggerLabel: t.viaRef ?? null,
|
|
66382
66544
|
observedCount: 1,
|
|
66383
|
-
requiresAuth:
|
|
66545
|
+
requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
|
|
66384
66546
|
});
|
|
66385
66547
|
}
|
|
66386
66548
|
return {
|
|
@@ -66398,7 +66560,7 @@ ${constraints.join("\n")}`);
|
|
|
66398
66560
|
}
|
|
66399
66561
|
function buildMobileFeatureGroupsFromGraph(graph, appId) {
|
|
66400
66562
|
const groups = /* @__PURE__ */ new Map();
|
|
66401
|
-
const orderedScreens =
|
|
66563
|
+
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66402
66564
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66403
66565
|
for (const state2 of orderedScreens) {
|
|
66404
66566
|
const screenTypeLower = state2.screenType?.toLowerCase();
|
|
@@ -66445,7 +66607,7 @@ ${constraints.join("\n")}`);
|
|
|
66445
66607
|
return [];
|
|
66446
66608
|
if (input.featureName?.trim())
|
|
66447
66609
|
return [];
|
|
66448
|
-
const orderedScreens =
|
|
66610
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66449
66611
|
if (orderedScreens.length === 0)
|
|
66450
66612
|
return [];
|
|
66451
66613
|
const entryScreenId = orderedScreens[0].screenId;
|
|
@@ -66583,7 +66745,9 @@ ${constraints.join("\n")}`);
|
|
|
66583
66745
|
recordedJourneys,
|
|
66584
66746
|
credentials: input.credentials,
|
|
66585
66747
|
testBudget: input.testBudget,
|
|
66586
|
-
incremental: input.incremental
|
|
66748
|
+
incremental: input.incremental,
|
|
66749
|
+
userNotes: input.userNotes,
|
|
66750
|
+
featureContext: input.featureContext
|
|
66587
66751
|
})
|
|
66588
66752
|
});
|
|
66589
66753
|
const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
|
|
@@ -66647,9 +66811,11 @@ var require_mobile_generator = __commonJS({
|
|
|
66647
66811
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
66648
66812
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
66649
66813
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
66814
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
66650
66815
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
66651
66816
|
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
66652
66817
|
var MAX_RECENT_EXCHANGES = 24;
|
|
66818
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
66653
66819
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
66654
66820
|
function finalizeAction(input) {
|
|
66655
66821
|
return { ...input, url: "", selector: input.selector ?? "" };
|
|
@@ -66840,8 +67006,38 @@ var require_mobile_generator = __commonJS({
|
|
|
66840
67006
|
return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
|
|
66841
67007
|
}
|
|
66842
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;
|
|
66843
67010
|
function isEditableMobileElement(element) {
|
|
66844
|
-
|
|
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;
|
|
66845
67041
|
}
|
|
66846
67042
|
function nonEmpty2(value) {
|
|
66847
67043
|
if (typeof value !== "string")
|
|
@@ -67065,10 +67261,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
67065
67261
|
externalOwner: boundary.owner
|
|
67066
67262
|
};
|
|
67067
67263
|
}
|
|
67068
|
-
|
|
67264
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
67265
|
+
let lastRecovered = recovered;
|
|
67266
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
67267
|
+
try {
|
|
67268
|
+
await driver.launchApp();
|
|
67269
|
+
const retrySnap = await driver.snapshot();
|
|
67270
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.screenSignal, retrySnap.windowSize);
|
|
67271
|
+
if (!retryRecovered)
|
|
67272
|
+
continue;
|
|
67273
|
+
lastRecovered = retryRecovered;
|
|
67274
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
67275
|
+
setLatest(retryRecovered);
|
|
67276
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(retryBoundary)) {
|
|
67277
|
+
emitScreenshot(retrySnap.screenshot);
|
|
67278
|
+
log2(` recovered target app after ${attempt} additional activation attempt(s).`);
|
|
67279
|
+
return {
|
|
67280
|
+
resolved: retryRecovered,
|
|
67281
|
+
observation: retryRecovered.observation,
|
|
67282
|
+
externalBlocked: true,
|
|
67283
|
+
externalOwner: boundary.owner
|
|
67284
|
+
};
|
|
67285
|
+
}
|
|
67286
|
+
if (retryBoundary.kind === "transient_external") {
|
|
67287
|
+
log2(` recovery surfaced system overlay ${retryBoundary.owner}; not recording it as a target-app step.`);
|
|
67288
|
+
return {
|
|
67289
|
+
resolved: retryRecovered,
|
|
67290
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
67291
|
+
externalBlocked: true,
|
|
67292
|
+
transientExternal: true,
|
|
67293
|
+
externalOwner: boundary.owner
|
|
67294
|
+
};
|
|
67295
|
+
}
|
|
67296
|
+
lastExternalOwner = retryBoundary.owner;
|
|
67297
|
+
} catch (err) {
|
|
67298
|
+
log2(` target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
67299
|
+
}
|
|
67300
|
+
}
|
|
67301
|
+
log2(` relaunch still outside target app (${lastExternalOwner}); no target-app step recorded.`);
|
|
67069
67302
|
return {
|
|
67070
|
-
resolved:
|
|
67071
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
67303
|
+
resolved: lastRecovered,
|
|
67304
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no target-app step was recorded)`,
|
|
67072
67305
|
externalBlocked: true,
|
|
67073
67306
|
externalOwner: boundary.owner
|
|
67074
67307
|
};
|
|
@@ -67282,9 +67515,26 @@ ${statePacket}` },
|
|
|
67282
67515
|
return unknownRef(toolArgs.ref);
|
|
67283
67516
|
const beforeScreenName = screenName();
|
|
67284
67517
|
const before = latest();
|
|
67285
|
-
|
|
67518
|
+
let result = await driver.tap(element.bounds);
|
|
67519
|
+
if (!result.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(result.error, before?.screen.elements ?? [])) {
|
|
67520
|
+
log2(" tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
67521
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
67522
|
+
const retryResult = await driver.tap(element.bounds);
|
|
67523
|
+
result = retryResult.ok ? retryResult : {
|
|
67524
|
+
...retryResult,
|
|
67525
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
67526
|
+
source: retryResult.source ?? dismissResult.source,
|
|
67527
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
67528
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
67529
|
+
};
|
|
67530
|
+
}
|
|
67286
67531
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67287
|
-
|
|
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
|
+
}
|
|
67288
67538
|
recordAction({
|
|
67289
67539
|
type: "TAP",
|
|
67290
67540
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
@@ -67428,6 +67678,13 @@ ${statePacket}` },
|
|
|
67428
67678
|
const element = resolveRef(toolArgs.ref);
|
|
67429
67679
|
if (!element)
|
|
67430
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
|
+
}
|
|
67431
67688
|
const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
|
|
67432
67689
|
if (!expected)
|
|
67433
67690
|
return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
|
|
@@ -67686,19 +67943,46 @@ ${statePacket}` },
|
|
|
67686
67943
|
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67687
67944
|
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67688
67945
|
}
|
|
67689
|
-
if (
|
|
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)
|
|
67690
67962
|
return null;
|
|
67691
|
-
|
|
67692
|
-
|
|
67693
|
-
|
|
67694
|
-
|
|
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)
|
|
67695
67972
|
return null;
|
|
67696
|
-
const
|
|
67697
|
-
|
|
67698
|
-
|
|
67699
|
-
|
|
67700
|
-
|
|
67701
|
-
|
|
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;
|
|
67702
67986
|
}
|
|
67703
67987
|
function actionCycleKey(action) {
|
|
67704
67988
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
@@ -68633,8 +68917,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68633
68917
|
function coerceSteps(value) {
|
|
68634
68918
|
if (!Array.isArray(value) || value.length === 0)
|
|
68635
68919
|
return void 0;
|
|
68636
|
-
const
|
|
68637
|
-
|
|
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 }));
|
|
68638
68935
|
}
|
|
68639
68936
|
function parseHealVerdict(content) {
|
|
68640
68937
|
const cleaned = stripCodeFences(content);
|
|
@@ -72855,6 +73152,48 @@ var require_snapshot_observation = __commonJS({
|
|
|
72855
73152
|
var PROMOTED_FORM_FIELD_CAP = 12;
|
|
72856
73153
|
var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
|
|
72857
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
|
+
}
|
|
72858
73197
|
function isGroupedFormField(element) {
|
|
72859
73198
|
if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
|
|
72860
73199
|
return false;
|
|
@@ -72870,6 +73209,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
72870
73209
|
const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
|
|
72871
73210
|
const artifact = page.snapshotArtifact;
|
|
72872
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");
|
|
72873
73213
|
const activeScope = inferActiveSnapshotScope(page);
|
|
72874
73214
|
const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
|
|
72875
73215
|
const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
|
|
@@ -72883,10 +73223,17 @@ var require_snapshot_observation = __commonJS({
|
|
|
72883
73223
|
});
|
|
72884
73224
|
let interactive;
|
|
72885
73225
|
if (options.mode === "survey") {
|
|
72886
|
-
const navLinks = allInteractive.filter(
|
|
72887
|
-
const others = allInteractive.filter((
|
|
73226
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73227
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
72888
73228
|
const othersCap = richRender ? 6 : 4;
|
|
72889
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
|
+
];
|
|
72890
73237
|
} else {
|
|
72891
73238
|
interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
|
|
72892
73239
|
}
|
|
@@ -72899,6 +73246,36 @@ var require_snapshot_observation = __commonJS({
|
|
|
72899
73246
|
}
|
|
72900
73247
|
}
|
|
72901
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
|
+
}
|
|
72902
73279
|
const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
|
|
72903
73280
|
const allForms = artifact?.formGroups ?? page.formGroups ?? [];
|
|
72904
73281
|
const forms = allForms.slice(0, options.formLimit ?? 3);
|
|
@@ -73198,6 +73575,54 @@ var require_snapshot_observation = __commonJS({
|
|
|
73198
73575
|
const hidden = allInteractive.length - interactive.length;
|
|
73199
73576
|
if (hidden > 0) {
|
|
73200
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
|
+
}
|
|
73201
73626
|
}
|
|
73202
73627
|
}
|
|
73203
73628
|
}
|
|
@@ -74220,13 +74645,14 @@ var require_auth_recipe = __commonJS({
|
|
|
74220
74645
|
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
74221
74646
|
"use strict";
|
|
74222
74647
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74223
|
-
exports.AUTH_MARKER_VERSION = void 0;
|
|
74648
|
+
exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
|
|
74224
74649
|
exports.computeRecipeHash = computeRecipeHash;
|
|
74225
74650
|
exports.buildAuthRecipe = buildAuthRecipe;
|
|
74226
74651
|
exports.authProofLocator = authProofLocator;
|
|
74227
74652
|
exports.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
74228
74653
|
exports.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
74229
74654
|
exports.renderAuthFn = renderAuthFn;
|
|
74655
|
+
exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
74230
74656
|
exports.absoluteRoute = absoluteRoute;
|
|
74231
74657
|
exports.renderReplayClickArrow = renderReplayClickArrow;
|
|
74232
74658
|
exports.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
@@ -74345,6 +74771,17 @@ var require_auth_recipe = __commonJS({
|
|
|
74345
74771
|
lines.push(END_MARKER);
|
|
74346
74772
|
return lines.join("\n");
|
|
74347
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
|
+
}
|
|
74348
74785
|
function absoluteRoute(route2, baseUrl) {
|
|
74349
74786
|
try {
|
|
74350
74787
|
return new URL(route2, baseUrl).href;
|
|
@@ -315417,8 +315854,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315417
315854
|
};
|
|
315418
315855
|
}
|
|
315419
315856
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
315420
|
-
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", log2);
|
|
315421
|
-
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.");
|
|
315857
|
+
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", mode, log2);
|
|
315858
|
+
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.");
|
|
315422
315859
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
315423
315860
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
315424
315861
|
const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
|
|
@@ -315449,9 +315886,15 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315449
315886
|
log2(`
|
|
315450
315887
|
Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
|
|
315451
315888
|
if (callbacks?.onExploreComplete) {
|
|
315452
|
-
|
|
315453
|
-
|
|
315454
|
-
})
|
|
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
|
+
}
|
|
315455
315898
|
}
|
|
315456
315899
|
if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
|
|
315457
315900
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
@@ -315479,8 +315922,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315479
315922
|
pausePhase: "explore"
|
|
315480
315923
|
};
|
|
315481
315924
|
}
|
|
315482
|
-
|
|
315483
|
-
|
|
315925
|
+
if (!isSurvey) {
|
|
315926
|
+
log2("\nRelaunching app for a clean state before planning/generation...");
|
|
315927
|
+
await driver.relaunch();
|
|
315928
|
+
}
|
|
315484
315929
|
log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
|
|
315485
315930
|
const graph = state2.toScreenGraph();
|
|
315486
315931
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
@@ -315503,7 +315948,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315503
315948
|
hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
|
|
315504
315949
|
},
|
|
315505
315950
|
incremental: ctx.incremental,
|
|
315506
|
-
testBudget
|
|
315951
|
+
testBudget,
|
|
315952
|
+
userNotes: ctx.userNotes,
|
|
315953
|
+
featureContext: ctx.featureContext
|
|
315507
315954
|
}, {
|
|
315508
315955
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
315509
315956
|
onAICall
|
|
@@ -315660,10 +316107,11 @@ Fatal error: ${msg}`);
|
|
|
315660
316107
|
};
|
|
315661
316108
|
}
|
|
315662
316109
|
}
|
|
315663
|
-
async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
|
|
316110
|
+
async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
|
|
315664
316111
|
void phase;
|
|
315665
316112
|
const resetPolicy = target?.resetPolicy ?? "AUTO";
|
|
315666
|
-
|
|
316113
|
+
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
316114
|
+
if (shouldPreserveAppData) {
|
|
315667
316115
|
try {
|
|
315668
316116
|
await driver.relaunch();
|
|
315669
316117
|
} catch (err) {
|
|
@@ -315675,10 +316123,20 @@ Fatal error: ${msg}`);
|
|
|
315675
316123
|
log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
315676
316124
|
return false;
|
|
315677
316125
|
}) ?? false;
|
|
315678
|
-
if (
|
|
315679
|
-
|
|
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;
|
|
315680
316133
|
}
|
|
315681
|
-
|
|
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;
|
|
315682
316140
|
}
|
|
315683
316141
|
function shouldPauseAfterTruncatedExplore(explore) {
|
|
315684
316142
|
return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
|
|
@@ -315725,6 +316183,7 @@ var require_discover_agent = __commonJS({
|
|
|
315725
316183
|
var claude_discovery_runner_js_1 = require_claude_discovery_runner();
|
|
315726
316184
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
315727
316185
|
var canvas_detector_js_1 = require_canvas_detector();
|
|
316186
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
315728
316187
|
async function runCanvasPreflight(ctx, sharedBrowser, log2) {
|
|
315729
316188
|
const cached = ctx.appRenderType ?? null;
|
|
315730
316189
|
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
@@ -316420,7 +316879,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
316420
316879
|
}
|
|
316421
316880
|
if (envRefs.size > 0) {
|
|
316422
316881
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
316423
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
316882
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
316424
316883
|
if (missing.length > 0) {
|
|
316425
316884
|
autoVariableDefaults = {};
|
|
316426
316885
|
for (const key of missing) {
|
|
@@ -316936,7 +317395,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
316936
317395
|
envRefs.add(match[1]);
|
|
316937
317396
|
if (envRefs.size > 0) {
|
|
316938
317397
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
316939
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
317398
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
316940
317399
|
if (missing.length > 0) {
|
|
316941
317400
|
autoVariableDefaults = {};
|
|
316942
317401
|
for (const key of missing) {
|
|
@@ -329818,9 +330277,9 @@ var require_dist2 = __commonJS({
|
|
|
329818
330277
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
329819
330278
|
};
|
|
329820
330279
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
329821
|
-
exports.
|
|
329822
|
-
exports.
|
|
329823
|
-
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;
|
|
329824
330283
|
__exportStar(require_ai_provider(), exports);
|
|
329825
330284
|
__exportStar(require_mobile_types(), exports);
|
|
329826
330285
|
__exportStar(require_mobile_source_parser(), exports);
|
|
@@ -329957,6 +330416,12 @@ var require_dist2 = __commonJS({
|
|
|
329957
330416
|
Object.defineProperty(exports, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
|
|
329958
330417
|
return auth_recipe_js_1.AUTH_MARKER_VERSION;
|
|
329959
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
|
+
} });
|
|
329960
330425
|
var auth_capture_run_js_1 = require_auth_capture_run();
|
|
329961
330426
|
Object.defineProperty(exports, "runLoginCaptureRun", { enumerable: true, get: function() {
|
|
329962
330427
|
return auth_capture_run_js_1.runLoginCaptureRun;
|
|
@@ -330184,7 +330649,7 @@ var require_package4 = __commonJS({
|
|
|
330184
330649
|
"package.json"(exports, module) {
|
|
330185
330650
|
module.exports = {
|
|
330186
330651
|
name: "@validate.qa/runner",
|
|
330187
|
-
version: "1.0.
|
|
330652
|
+
version: "1.0.33",
|
|
330188
330653
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330189
330654
|
bin: {
|
|
330190
330655
|
"validate-runner": "dist/cli.js"
|
|
@@ -330800,25 +331265,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
|
330800
331265
|
}
|
|
330801
331266
|
return elementId;
|
|
330802
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
|
+
}
|
|
330803
331274
|
async function typeText(sessionPath, value, bounds) {
|
|
330804
331275
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330805
|
-
if (
|
|
330806
|
-
|
|
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
|
+
}
|
|
330807
331286
|
}
|
|
330808
|
-
await
|
|
330809
|
-
method: "POST",
|
|
330810
|
-
body: JSON.stringify({ text: value })
|
|
330811
|
-
});
|
|
331287
|
+
await sendKeysToFocusedInput(sessionPath, value);
|
|
330812
331288
|
}
|
|
330813
331289
|
async function clearField(sessionPath, bounds) {
|
|
330814
331290
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330815
|
-
if (
|
|
330816
|
-
|
|
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
|
+
);
|
|
330817
331314
|
}
|
|
330818
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
330819
|
-
method: "POST",
|
|
330820
|
-
body: JSON.stringify({})
|
|
330821
|
-
});
|
|
330822
331315
|
}
|
|
330823
331316
|
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
330824
331317
|
const dist = clampMobileSwipeDistance(distance);
|
|
@@ -330848,6 +331341,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
|
|
|
330848
331341
|
async function pressBack(sessionPath) {
|
|
330849
331342
|
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
330850
331343
|
}
|
|
331344
|
+
function isKeyboardAlreadyHiddenError(err) {
|
|
331345
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331346
|
+
return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
|
|
331347
|
+
}
|
|
331348
|
+
async function dismissKeyboard(sessionPath, platform3) {
|
|
331349
|
+
if (platform3 === "ANDROID") {
|
|
331350
|
+
await pressBack(sessionPath);
|
|
331351
|
+
return;
|
|
331352
|
+
}
|
|
331353
|
+
try {
|
|
331354
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, {
|
|
331355
|
+
method: "POST",
|
|
331356
|
+
body: "{}"
|
|
331357
|
+
});
|
|
331358
|
+
return;
|
|
331359
|
+
} catch (err) {
|
|
331360
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331361
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331362
|
+
}
|
|
331363
|
+
try {
|
|
331364
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
331365
|
+
method: "POST",
|
|
331366
|
+
body: JSON.stringify({ script: "mobile: hideKeyboard", args: [{}] })
|
|
331367
|
+
});
|
|
331368
|
+
return;
|
|
331369
|
+
} catch (err) {
|
|
331370
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331371
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331372
|
+
}
|
|
331373
|
+
await pressBack(sessionPath);
|
|
331374
|
+
}
|
|
330851
331375
|
async function pressHome(sessionPath, platform3) {
|
|
330852
331376
|
if (platform3 === "IOS") {
|
|
330853
331377
|
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
@@ -332028,6 +332552,10 @@ async function startMobileNetworkCapture(options) {
|
|
|
332028
332552
|
|
|
332029
332553
|
// src/services/appium-executor.ts
|
|
332030
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
|
+
}
|
|
332031
332559
|
var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
332032
332560
|
const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
332033
332561
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
|
|
@@ -332162,6 +332690,9 @@ async function createSession(target) {
|
|
|
332162
332690
|
});
|
|
332163
332691
|
const sessionId = result?.value?.sessionId ?? result?.sessionId;
|
|
332164
332692
|
if (!sessionId) throw new Error("Appium session creation returned no session ID");
|
|
332693
|
+
if (target.platform === "ANDROID") {
|
|
332694
|
+
await prepareAndroidInputEnvironment(target.deviceId);
|
|
332695
|
+
}
|
|
332165
332696
|
return sessionId;
|
|
332166
332697
|
}
|
|
332167
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;
|
|
@@ -332549,7 +333080,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
332549
333080
|
method: "POST",
|
|
332550
333081
|
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
332551
333082
|
});
|
|
332552
|
-
const elementId =
|
|
333083
|
+
const elementId = validateElementId(
|
|
333084
|
+
result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
|
|
333085
|
+
);
|
|
332553
333086
|
if (elementId) return elementId;
|
|
332554
333087
|
} catch (err) {
|
|
332555
333088
|
if (isTransportError(err)) throw err;
|
|
@@ -332589,24 +333122,25 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
332589
333122
|
})
|
|
332590
333123
|
});
|
|
332591
333124
|
}
|
|
332592
|
-
async function getActiveElementId2(sessionId) {
|
|
332593
|
-
try {
|
|
332594
|
-
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
332595
|
-
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
332596
|
-
} catch {
|
|
332597
|
-
return null;
|
|
332598
|
-
}
|
|
332599
|
-
}
|
|
332600
|
-
async function sendKeysToFocusedInput(sessionId, value) {
|
|
332601
|
-
await wdRequest(`/session/${sessionId}/keys`, {
|
|
332602
|
-
method: "POST",
|
|
332603
|
-
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
332604
|
-
});
|
|
332605
|
-
}
|
|
332606
333125
|
function encodeAndroidInputText(value) {
|
|
332607
333126
|
const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
332608
333127
|
return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
|
|
332609
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
|
+
}
|
|
332610
333144
|
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
332611
333145
|
if (!deviceId) {
|
|
332612
333146
|
throw new Error("Android keyboard fallback requires a device id");
|
|
@@ -332668,100 +333202,28 @@ async function executeType(sessionId, step, context) {
|
|
|
332668
333202
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
332669
333203
|
const value = String(step.value);
|
|
332670
333204
|
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
332671
|
-
|
|
332672
|
-
|
|
332673
|
-
|
|
332674
|
-
|
|
332675
|
-
|
|
332676
|
-
|
|
332677
|
-
|
|
332678
|
-
|
|
332679
|
-
|
|
332680
|
-
|
|
332681
|
-
|
|
332682
|
-
|
|
332683
|
-
|
|
332684
|
-
|
|
332685
|
-
|
|
332686
|
-
try {
|
|
332687
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
332688
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332689
|
-
if (!lastInputError) return;
|
|
332690
|
-
elementId = null;
|
|
332691
|
-
} catch (error2) {
|
|
332692
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
332693
|
-
lastInputError = error2;
|
|
332694
|
-
}
|
|
332695
|
-
}
|
|
332696
|
-
if (!elementId && bounds) {
|
|
332697
|
-
await tapCoordinates(sessionId, bounds);
|
|
332698
|
-
tappedBounds = true;
|
|
332699
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332700
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332701
|
-
}
|
|
332702
|
-
if (!elementId) {
|
|
332703
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332704
|
-
}
|
|
332705
|
-
if (elementId) {
|
|
332706
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332707
|
-
if (!lastInputError) return;
|
|
332708
|
-
elementId = null;
|
|
332709
|
-
if (!bounds) {
|
|
332710
|
-
throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
|
|
332711
|
-
}
|
|
332712
|
-
}
|
|
332713
|
-
if (bounds) {
|
|
332714
|
-
if (!tappedBounds) {
|
|
332715
|
-
await tapCoordinates(sessionId, bounds);
|
|
332716
|
-
tappedBounds = true;
|
|
332717
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332718
|
-
}
|
|
332719
|
-
try {
|
|
332720
|
-
await sendKeysToFocusedInput(sessionId, value);
|
|
332721
|
-
return;
|
|
332722
|
-
} catch (error2) {
|
|
332723
|
-
if (isTransportError(error2)) throw error2;
|
|
332724
|
-
lastInputError = error2;
|
|
332725
|
-
}
|
|
332726
|
-
}
|
|
332727
|
-
if (context.platform === "ANDROID" && tappedBounds) {
|
|
332728
|
-
try {
|
|
332729
|
-
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
332730
|
-
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
332731
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332732
|
-
return;
|
|
332733
|
-
} catch (error2) {
|
|
332734
|
-
if (isTransportError(error2)) throw error2;
|
|
332735
|
-
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
|
+
}
|
|
332736
333220
|
}
|
|
333221
|
+
throw error2 instanceof Error ? error2 : new Error(String(error2));
|
|
332737
333222
|
}
|
|
332738
|
-
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
332739
333223
|
}
|
|
332740
333224
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
332741
333225
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
332742
|
-
|
|
332743
|
-
if (step.target) {
|
|
332744
|
-
try {
|
|
332745
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
332746
|
-
} catch (error2) {
|
|
332747
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
332748
|
-
}
|
|
332749
|
-
}
|
|
332750
|
-
if (!elementId && bounds) {
|
|
332751
|
-
await tapCoordinates(sessionId, bounds);
|
|
332752
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332753
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332754
|
-
}
|
|
332755
|
-
if (!elementId) {
|
|
332756
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332757
|
-
}
|
|
332758
|
-
if (!elementId) {
|
|
332759
|
-
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
332760
|
-
}
|
|
332761
|
-
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
|
|
332762
|
-
method: "POST",
|
|
332763
|
-
body: "{}"
|
|
332764
|
-
});
|
|
333226
|
+
await clearField(`/session/${sessionId}`, bounds);
|
|
332765
333227
|
}
|
|
332766
333228
|
async function executeSwipe(sessionId, step) {
|
|
332767
333229
|
const direction = step.metadata?.direction || "up";
|
|
@@ -334844,6 +335306,10 @@ var MobileBridgeDriver = class {
|
|
|
334844
335306
|
async back() {
|
|
334845
335307
|
return this.runAction(() => pressBack(this.sessionPath));
|
|
334846
335308
|
}
|
|
335309
|
+
/** Platform-aware software keyboard dismissal. */
|
|
335310
|
+
async dismissKeyboard() {
|
|
335311
|
+
return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
|
|
335312
|
+
}
|
|
334847
335313
|
/** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
|
|
334848
335314
|
async home() {
|
|
334849
335315
|
return this.runAction(() => pressHome(this.sessionPath, this.platform));
|
|
@@ -335229,8 +335695,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
335229
335695
|
pageUrl: call.pageUrl ?? pageUrl
|
|
335230
335696
|
}));
|
|
335231
335697
|
}
|
|
335698
|
+
var runnerPackage = require_package4();
|
|
335232
335699
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
335233
335700
|
var IS_CLOUD = process.env.RUNNER_CLOUD === "true";
|
|
335701
|
+
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
335702
|
+
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
335703
|
+
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;
|
|
335234
335704
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
335235
335705
|
var DEFAULT_TYPE_LIMITS = {
|
|
335236
335706
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -338064,6 +338534,9 @@ async function startRunner(opts) {
|
|
|
338064
338534
|
method: "POST",
|
|
338065
338535
|
headers,
|
|
338066
338536
|
body: JSON.stringify({
|
|
338537
|
+
runnerName: RUNNER_NAME,
|
|
338538
|
+
runnerVersion: RUNNER_VERSION,
|
|
338539
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
338067
338540
|
runsCompleted,
|
|
338068
338541
|
activeRuns,
|
|
338069
338542
|
maxConcurrent: MAX_CONCURRENT,
|