@validate.qa/runner 1.0.31 → 1.0.32
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 +205 -18
- package/dist/cli.mjs +205 -18
- 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: {
|
|
@@ -62556,9 +62581,46 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62556
62581
|
externalOwner: boundary.owner
|
|
62557
62582
|
};
|
|
62558
62583
|
}
|
|
62559
|
-
|
|
62584
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
62585
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
62586
|
+
try {
|
|
62587
|
+
await driver.launchApp();
|
|
62588
|
+
const retrySnap = await driver.snapshot();
|
|
62589
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.windowSize, retrySnap.screenSignal);
|
|
62590
|
+
if (!retryRecovered)
|
|
62591
|
+
continue;
|
|
62592
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
62593
|
+
latest = retryRecovered;
|
|
62594
|
+
lastSnapshotIteration = iterationForObservation;
|
|
62595
|
+
if (retryBoundary.kind === "target" || retryBoundary.kind === "unknown") {
|
|
62596
|
+
const screenId = ingestSnapshot(retryRecovered, iterationForObservation);
|
|
62597
|
+
captureScreenshot(retrySnap.screenshot, screenId);
|
|
62598
|
+
log2(` Recovered target app after ${attempt} additional activation attempt(s).`);
|
|
62599
|
+
return {
|
|
62600
|
+
observation: retryRecovered.observation,
|
|
62601
|
+
screenId,
|
|
62602
|
+
externalBlocked: true,
|
|
62603
|
+
externalOwner: boundary.owner
|
|
62604
|
+
};
|
|
62605
|
+
}
|
|
62606
|
+
if (retryBoundary.kind === "transient_external") {
|
|
62607
|
+
log2(` Recovery surfaced system overlay ${retryBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
62608
|
+
return {
|
|
62609
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
62610
|
+
screenId: null,
|
|
62611
|
+
externalBlocked: true,
|
|
62612
|
+
transientExternal: true,
|
|
62613
|
+
externalOwner: boundary.owner
|
|
62614
|
+
};
|
|
62615
|
+
}
|
|
62616
|
+
lastExternalOwner = retryBoundary.owner;
|
|
62617
|
+
} catch (err) {
|
|
62618
|
+
log2(` Target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62619
|
+
}
|
|
62620
|
+
}
|
|
62621
|
+
log2(` Relaunch still outside target app (${lastExternalOwner}); no screen recorded.`);
|
|
62560
62622
|
return {
|
|
62561
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
62623
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no screen was recorded)`,
|
|
62562
62624
|
screenId: null,
|
|
62563
62625
|
externalBlocked: true,
|
|
62564
62626
|
externalOwner: boundary.owner
|
|
@@ -62936,7 +62998,19 @@ Exploration complete: ${summary}`);
|
|
|
62936
62998
|
const fromScreenId = state2.getCurrentScreenId();
|
|
62937
62999
|
const boundaryBeforeAction = currentBoundary();
|
|
62938
63000
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
62939
|
-
|
|
63001
|
+
let actionResult = await driver.tap(resolved.bounds);
|
|
63002
|
+
if (!actionResult.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(actionResult.error, latest()?.screen.elements ?? [])) {
|
|
63003
|
+
log2(" Tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
63004
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
63005
|
+
const retryResult = await driver.tap(resolved.bounds);
|
|
63006
|
+
actionResult = retryResult.ok ? retryResult : {
|
|
63007
|
+
...retryResult,
|
|
63008
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
63009
|
+
source: retryResult.source ?? dismissResult.source,
|
|
63010
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
63011
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
63012
|
+
};
|
|
63013
|
+
}
|
|
62940
63014
|
const absorbed = await absorbActionResult(actionResult, "mobile_tap");
|
|
62941
63015
|
const { observation, screenId } = absorbed;
|
|
62942
63016
|
if (!startedOutsideTarget && !absorbed.externalBlocked && !absorbed.transientExternal) {
|
|
@@ -65936,6 +66010,25 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
65936
66010
|
function targetAppScreens(graph, appId) {
|
|
65937
66011
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
65938
66012
|
}
|
|
66013
|
+
function isGenericNonFeatureScreen(state2) {
|
|
66014
|
+
const type = state2.screenType?.trim().toLowerCase();
|
|
66015
|
+
if (type === "error" || type === "permission")
|
|
66016
|
+
return true;
|
|
66017
|
+
if (type && type !== "unknown")
|
|
66018
|
+
return false;
|
|
66019
|
+
const actionableCount = state2.elements.filter((el) => el.actionable).length;
|
|
66020
|
+
if (actionableCount > 0)
|
|
66021
|
+
return false;
|
|
66022
|
+
const evidenceText = [
|
|
66023
|
+
state2.screenName,
|
|
66024
|
+
state2.screenType,
|
|
66025
|
+
...state2.observations ?? []
|
|
66026
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ");
|
|
66027
|
+
return /\b(empty|loading|blank|spinner|intermediate|placeholder|no actionable)\b/i.test(evidenceText);
|
|
66028
|
+
}
|
|
66029
|
+
function plannableTargetAppScreens(graph, appId) {
|
|
66030
|
+
return targetAppScreens(graph, appId).filter((state2) => !isGenericNonFeatureScreen(state2));
|
|
66031
|
+
}
|
|
65939
66032
|
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
66033
|
|
|
65941
66034
|
## Output Discipline
|
|
@@ -65955,8 +66048,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65955
66048
|
5. CRUD screens for the same entity belong together
|
|
65956
66049
|
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
66050
|
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.
|
|
66051
|
+
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.
|
|
66052
|
+
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
66053
|
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
66054
|
|
|
65962
66055
|
## Output JSON Schema
|
|
@@ -66223,7 +66316,7 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66223
66316
|
sections.push(`## Feature: ${context.featureName}
|
|
66224
66317
|
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.`);
|
|
66225
66318
|
}
|
|
66226
|
-
const orderedScreens =
|
|
66319
|
+
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66227
66320
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66228
66321
|
sections.push(`## Screens Discovered (${orderedScreens.length})`);
|
|
66229
66322
|
for (const state2 of orderedScreens) {
|
|
@@ -66332,7 +66425,7 @@ ${constraints.join("\n")}`);
|
|
|
66332
66425
|
const now = /* @__PURE__ */ new Date();
|
|
66333
66426
|
const idByScreen = /* @__PURE__ */ new Map();
|
|
66334
66427
|
let pageSeq = 0;
|
|
66335
|
-
const orderedScreens =
|
|
66428
|
+
const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66336
66429
|
for (const state2 of orderedScreens) {
|
|
66337
66430
|
idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
|
|
66338
66431
|
}
|
|
@@ -66393,7 +66486,7 @@ ${constraints.join("\n")}`);
|
|
|
66393
66486
|
}
|
|
66394
66487
|
function buildMobileFeatureGroupsFromGraph(graph, appId) {
|
|
66395
66488
|
const groups = /* @__PURE__ */ new Map();
|
|
66396
|
-
const orderedScreens =
|
|
66489
|
+
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66397
66490
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66398
66491
|
for (const state2 of orderedScreens) {
|
|
66399
66492
|
const screenTypeLower = state2.screenType?.toLowerCase();
|
|
@@ -66440,7 +66533,7 @@ ${constraints.join("\n")}`);
|
|
|
66440
66533
|
return [];
|
|
66441
66534
|
if (input.featureName?.trim())
|
|
66442
66535
|
return [];
|
|
66443
|
-
const orderedScreens =
|
|
66536
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66444
66537
|
if (orderedScreens.length === 0)
|
|
66445
66538
|
return [];
|
|
66446
66539
|
const entryScreenId = orderedScreens[0].screenId;
|
|
@@ -66642,9 +66735,11 @@ var require_mobile_generator = __commonJS({
|
|
|
66642
66735
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
66643
66736
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
66644
66737
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
66738
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
66645
66739
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
66646
66740
|
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
66647
66741
|
var MAX_RECENT_EXCHANGES = 24;
|
|
66742
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
66648
66743
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
66649
66744
|
function finalizeAction(input) {
|
|
66650
66745
|
return { ...input, url: "", selector: input.selector ?? "" };
|
|
@@ -67060,10 +67155,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
67060
67155
|
externalOwner: boundary.owner
|
|
67061
67156
|
};
|
|
67062
67157
|
}
|
|
67063
|
-
|
|
67158
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
67159
|
+
let lastRecovered = recovered;
|
|
67160
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
67161
|
+
try {
|
|
67162
|
+
await driver.launchApp();
|
|
67163
|
+
const retrySnap = await driver.snapshot();
|
|
67164
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.screenSignal, retrySnap.windowSize);
|
|
67165
|
+
if (!retryRecovered)
|
|
67166
|
+
continue;
|
|
67167
|
+
lastRecovered = retryRecovered;
|
|
67168
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
67169
|
+
setLatest(retryRecovered);
|
|
67170
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(retryBoundary)) {
|
|
67171
|
+
emitScreenshot(retrySnap.screenshot);
|
|
67172
|
+
log2(` recovered target app after ${attempt} additional activation attempt(s).`);
|
|
67173
|
+
return {
|
|
67174
|
+
resolved: retryRecovered,
|
|
67175
|
+
observation: retryRecovered.observation,
|
|
67176
|
+
externalBlocked: true,
|
|
67177
|
+
externalOwner: boundary.owner
|
|
67178
|
+
};
|
|
67179
|
+
}
|
|
67180
|
+
if (retryBoundary.kind === "transient_external") {
|
|
67181
|
+
log2(` recovery surfaced system overlay ${retryBoundary.owner}; not recording it as a target-app step.`);
|
|
67182
|
+
return {
|
|
67183
|
+
resolved: retryRecovered,
|
|
67184
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
67185
|
+
externalBlocked: true,
|
|
67186
|
+
transientExternal: true,
|
|
67187
|
+
externalOwner: boundary.owner
|
|
67188
|
+
};
|
|
67189
|
+
}
|
|
67190
|
+
lastExternalOwner = retryBoundary.owner;
|
|
67191
|
+
} catch (err) {
|
|
67192
|
+
log2(` target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
67193
|
+
}
|
|
67194
|
+
}
|
|
67195
|
+
log2(` relaunch still outside target app (${lastExternalOwner}); no target-app step recorded.`);
|
|
67064
67196
|
return {
|
|
67065
|
-
resolved:
|
|
67066
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
67197
|
+
resolved: lastRecovered,
|
|
67198
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no target-app step was recorded)`,
|
|
67067
67199
|
externalBlocked: true,
|
|
67068
67200
|
externalOwner: boundary.owner
|
|
67069
67201
|
};
|
|
@@ -67277,7 +67409,19 @@ ${statePacket}` },
|
|
|
67277
67409
|
return unknownRef(toolArgs.ref);
|
|
67278
67410
|
const beforeScreenName = screenName();
|
|
67279
67411
|
const before = latest();
|
|
67280
|
-
|
|
67412
|
+
let result = await driver.tap(element.bounds);
|
|
67413
|
+
if (!result.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(result.error, before?.screen.elements ?? [])) {
|
|
67414
|
+
log2(" tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
67415
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
67416
|
+
const retryResult = await driver.tap(element.bounds);
|
|
67417
|
+
result = retryResult.ok ? retryResult : {
|
|
67418
|
+
...retryResult,
|
|
67419
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
67420
|
+
source: retryResult.source ?? dismissResult.source,
|
|
67421
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
67422
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
67423
|
+
};
|
|
67424
|
+
}
|
|
67281
67425
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67282
67426
|
if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
|
|
67283
67427
|
recordAction({
|
|
@@ -315412,8 +315556,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315412
315556
|
};
|
|
315413
315557
|
}
|
|
315414
315558
|
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.");
|
|
315559
|
+
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", mode, log2);
|
|
315560
|
+
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
315561
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
315418
315562
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
315419
315563
|
const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
|
|
@@ -315655,10 +315799,11 @@ Fatal error: ${msg}`);
|
|
|
315655
315799
|
};
|
|
315656
315800
|
}
|
|
315657
315801
|
}
|
|
315658
|
-
async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
|
|
315802
|
+
async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
|
|
315659
315803
|
void phase;
|
|
315660
315804
|
const resetPolicy = target?.resetPolicy ?? "AUTO";
|
|
315661
|
-
|
|
315805
|
+
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
315806
|
+
if (shouldPreserveAppData) {
|
|
315662
315807
|
try {
|
|
315663
315808
|
await driver.relaunch();
|
|
315664
315809
|
} catch (err) {
|
|
@@ -330179,7 +330324,7 @@ var require_package4 = __commonJS({
|
|
|
330179
330324
|
"package.json"(exports2, module2) {
|
|
330180
330325
|
module2.exports = {
|
|
330181
330326
|
name: "@validate.qa/runner",
|
|
330182
|
-
version: "1.0.
|
|
330327
|
+
version: "1.0.32",
|
|
330183
330328
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330184
330329
|
bin: {
|
|
330185
330330
|
"validate-runner": "dist/cli.js"
|
|
@@ -330843,6 +330988,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
|
|
|
330843
330988
|
async function pressBack(sessionPath) {
|
|
330844
330989
|
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
330845
330990
|
}
|
|
330991
|
+
function isKeyboardAlreadyHiddenError(err) {
|
|
330992
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
330993
|
+
return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
|
|
330994
|
+
}
|
|
330995
|
+
async function dismissKeyboard(sessionPath, platform3) {
|
|
330996
|
+
if (platform3 === "ANDROID") {
|
|
330997
|
+
await pressBack(sessionPath);
|
|
330998
|
+
return;
|
|
330999
|
+
}
|
|
331000
|
+
try {
|
|
331001
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, {
|
|
331002
|
+
method: "POST",
|
|
331003
|
+
body: "{}"
|
|
331004
|
+
});
|
|
331005
|
+
return;
|
|
331006
|
+
} catch (err) {
|
|
331007
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331008
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331009
|
+
}
|
|
331010
|
+
try {
|
|
331011
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
331012
|
+
method: "POST",
|
|
331013
|
+
body: JSON.stringify({ script: "mobile: hideKeyboard", args: [{}] })
|
|
331014
|
+
});
|
|
331015
|
+
return;
|
|
331016
|
+
} catch (err) {
|
|
331017
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331018
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331019
|
+
}
|
|
331020
|
+
await pressBack(sessionPath);
|
|
331021
|
+
}
|
|
330846
331022
|
async function pressHome(sessionPath, platform3) {
|
|
330847
331023
|
if (platform3 === "IOS") {
|
|
330848
331024
|
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
@@ -334836,6 +335012,10 @@ var MobileBridgeDriver = class {
|
|
|
334836
335012
|
async back() {
|
|
334837
335013
|
return this.runAction(() => pressBack(this.sessionPath));
|
|
334838
335014
|
}
|
|
335015
|
+
/** Platform-aware software keyboard dismissal. */
|
|
335016
|
+
async dismissKeyboard() {
|
|
335017
|
+
return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
|
|
335018
|
+
}
|
|
334839
335019
|
/** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
|
|
334840
335020
|
async home() {
|
|
334841
335021
|
return this.runAction(() => pressHome(this.sessionPath, this.platform));
|
|
@@ -335221,8 +335401,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
335221
335401
|
pageUrl: call.pageUrl ?? pageUrl
|
|
335222
335402
|
}));
|
|
335223
335403
|
}
|
|
335404
|
+
var runnerPackage = require_package4();
|
|
335224
335405
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
335225
335406
|
var IS_CLOUD = process.env.RUNNER_CLOUD === "true";
|
|
335407
|
+
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
335408
|
+
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
335409
|
+
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
335410
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
335227
335411
|
var DEFAULT_TYPE_LIMITS = {
|
|
335228
335412
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -338056,6 +338240,9 @@ async function startRunner(opts) {
|
|
|
338056
338240
|
method: "POST",
|
|
338057
338241
|
headers,
|
|
338058
338242
|
body: JSON.stringify({
|
|
338243
|
+
runnerName: RUNNER_NAME,
|
|
338244
|
+
runnerVersion: RUNNER_VERSION,
|
|
338245
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
338059
338246
|
runsCompleted,
|
|
338060
338247
|
activeRuns,
|
|
338061
338248
|
maxConcurrent: MAX_CONCURRENT,
|
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: {
|
|
@@ -62561,9 +62586,46 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62561
62586
|
externalOwner: boundary.owner
|
|
62562
62587
|
};
|
|
62563
62588
|
}
|
|
62564
|
-
|
|
62589
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
62590
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
62591
|
+
try {
|
|
62592
|
+
await driver.launchApp();
|
|
62593
|
+
const retrySnap = await driver.snapshot();
|
|
62594
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.windowSize, retrySnap.screenSignal);
|
|
62595
|
+
if (!retryRecovered)
|
|
62596
|
+
continue;
|
|
62597
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
62598
|
+
latest = retryRecovered;
|
|
62599
|
+
lastSnapshotIteration = iterationForObservation;
|
|
62600
|
+
if (retryBoundary.kind === "target" || retryBoundary.kind === "unknown") {
|
|
62601
|
+
const screenId = ingestSnapshot(retryRecovered, iterationForObservation);
|
|
62602
|
+
captureScreenshot(retrySnap.screenshot, screenId);
|
|
62603
|
+
log2(` Recovered target app after ${attempt} additional activation attempt(s).`);
|
|
62604
|
+
return {
|
|
62605
|
+
observation: retryRecovered.observation,
|
|
62606
|
+
screenId,
|
|
62607
|
+
externalBlocked: true,
|
|
62608
|
+
externalOwner: boundary.owner
|
|
62609
|
+
};
|
|
62610
|
+
}
|
|
62611
|
+
if (retryBoundary.kind === "transient_external") {
|
|
62612
|
+
log2(` Recovery surfaced system overlay ${retryBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
62613
|
+
return {
|
|
62614
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
62615
|
+
screenId: null,
|
|
62616
|
+
externalBlocked: true,
|
|
62617
|
+
transientExternal: true,
|
|
62618
|
+
externalOwner: boundary.owner
|
|
62619
|
+
};
|
|
62620
|
+
}
|
|
62621
|
+
lastExternalOwner = retryBoundary.owner;
|
|
62622
|
+
} catch (err) {
|
|
62623
|
+
log2(` Target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62624
|
+
}
|
|
62625
|
+
}
|
|
62626
|
+
log2(` Relaunch still outside target app (${lastExternalOwner}); no screen recorded.`);
|
|
62565
62627
|
return {
|
|
62566
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
62628
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no screen was recorded)`,
|
|
62567
62629
|
screenId: null,
|
|
62568
62630
|
externalBlocked: true,
|
|
62569
62631
|
externalOwner: boundary.owner
|
|
@@ -62941,7 +63003,19 @@ Exploration complete: ${summary}`);
|
|
|
62941
63003
|
const fromScreenId = state2.getCurrentScreenId();
|
|
62942
63004
|
const boundaryBeforeAction = currentBoundary();
|
|
62943
63005
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
62944
|
-
|
|
63006
|
+
let actionResult = await driver.tap(resolved.bounds);
|
|
63007
|
+
if (!actionResult.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(actionResult.error, latest()?.screen.elements ?? [])) {
|
|
63008
|
+
log2(" Tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
63009
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
63010
|
+
const retryResult = await driver.tap(resolved.bounds);
|
|
63011
|
+
actionResult = retryResult.ok ? retryResult : {
|
|
63012
|
+
...retryResult,
|
|
63013
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
63014
|
+
source: retryResult.source ?? dismissResult.source,
|
|
63015
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
63016
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
63017
|
+
};
|
|
63018
|
+
}
|
|
62945
63019
|
const absorbed = await absorbActionResult(actionResult, "mobile_tap");
|
|
62946
63020
|
const { observation, screenId } = absorbed;
|
|
62947
63021
|
if (!startedOutsideTarget && !absorbed.externalBlocked && !absorbed.transientExternal) {
|
|
@@ -65941,6 +66015,25 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
65941
66015
|
function targetAppScreens(graph, appId) {
|
|
65942
66016
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
65943
66017
|
}
|
|
66018
|
+
function isGenericNonFeatureScreen(state2) {
|
|
66019
|
+
const type = state2.screenType?.trim().toLowerCase();
|
|
66020
|
+
if (type === "error" || type === "permission")
|
|
66021
|
+
return true;
|
|
66022
|
+
if (type && type !== "unknown")
|
|
66023
|
+
return false;
|
|
66024
|
+
const actionableCount = state2.elements.filter((el) => el.actionable).length;
|
|
66025
|
+
if (actionableCount > 0)
|
|
66026
|
+
return false;
|
|
66027
|
+
const evidenceText = [
|
|
66028
|
+
state2.screenName,
|
|
66029
|
+
state2.screenType,
|
|
66030
|
+
...state2.observations ?? []
|
|
66031
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ");
|
|
66032
|
+
return /\b(empty|loading|blank|spinner|intermediate|placeholder|no actionable)\b/i.test(evidenceText);
|
|
66033
|
+
}
|
|
66034
|
+
function plannableTargetAppScreens(graph, appId) {
|
|
66035
|
+
return targetAppScreens(graph, appId).filter((state2) => !isGenericNonFeatureScreen(state2));
|
|
66036
|
+
}
|
|
65944
66037
|
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
66038
|
|
|
65946
66039
|
## Output Discipline
|
|
@@ -65960,8 +66053,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65960
66053
|
5. CRUD screens for the same entity belong together
|
|
65961
66054
|
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
66055
|
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.
|
|
66056
|
+
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.
|
|
66057
|
+
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
66058
|
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
66059
|
|
|
65967
66060
|
## Output JSON Schema
|
|
@@ -66228,7 +66321,7 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66228
66321
|
sections.push(`## Feature: ${context.featureName}
|
|
66229
66322
|
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.`);
|
|
66230
66323
|
}
|
|
66231
|
-
const orderedScreens =
|
|
66324
|
+
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66232
66325
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66233
66326
|
sections.push(`## Screens Discovered (${orderedScreens.length})`);
|
|
66234
66327
|
for (const state2 of orderedScreens) {
|
|
@@ -66337,7 +66430,7 @@ ${constraints.join("\n")}`);
|
|
|
66337
66430
|
const now = /* @__PURE__ */ new Date();
|
|
66338
66431
|
const idByScreen = /* @__PURE__ */ new Map();
|
|
66339
66432
|
let pageSeq = 0;
|
|
66340
|
-
const orderedScreens =
|
|
66433
|
+
const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66341
66434
|
for (const state2 of orderedScreens) {
|
|
66342
66435
|
idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
|
|
66343
66436
|
}
|
|
@@ -66398,7 +66491,7 @@ ${constraints.join("\n")}`);
|
|
|
66398
66491
|
}
|
|
66399
66492
|
function buildMobileFeatureGroupsFromGraph(graph, appId) {
|
|
66400
66493
|
const groups = /* @__PURE__ */ new Map();
|
|
66401
|
-
const orderedScreens =
|
|
66494
|
+
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66402
66495
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66403
66496
|
for (const state2 of orderedScreens) {
|
|
66404
66497
|
const screenTypeLower = state2.screenType?.toLowerCase();
|
|
@@ -66445,7 +66538,7 @@ ${constraints.join("\n")}`);
|
|
|
66445
66538
|
return [];
|
|
66446
66539
|
if (input.featureName?.trim())
|
|
66447
66540
|
return [];
|
|
66448
|
-
const orderedScreens =
|
|
66541
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66449
66542
|
if (orderedScreens.length === 0)
|
|
66450
66543
|
return [];
|
|
66451
66544
|
const entryScreenId = orderedScreens[0].screenId;
|
|
@@ -66647,9 +66740,11 @@ var require_mobile_generator = __commonJS({
|
|
|
66647
66740
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
66648
66741
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
66649
66742
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
66743
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
66650
66744
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
66651
66745
|
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
66652
66746
|
var MAX_RECENT_EXCHANGES = 24;
|
|
66747
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
66653
66748
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
66654
66749
|
function finalizeAction(input) {
|
|
66655
66750
|
return { ...input, url: "", selector: input.selector ?? "" };
|
|
@@ -67065,10 +67160,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
67065
67160
|
externalOwner: boundary.owner
|
|
67066
67161
|
};
|
|
67067
67162
|
}
|
|
67068
|
-
|
|
67163
|
+
let lastExternalOwner = recoveredBoundary.owner;
|
|
67164
|
+
let lastRecovered = recovered;
|
|
67165
|
+
for (let attempt = 1; attempt <= EXTERNAL_APP_RECOVERY_ATTEMPTS; attempt += 1) {
|
|
67166
|
+
try {
|
|
67167
|
+
await driver.launchApp();
|
|
67168
|
+
const retrySnap = await driver.snapshot();
|
|
67169
|
+
const retryRecovered = buildResolved(retrySnap.xml, retrySnap.screenSignal, retrySnap.windowSize);
|
|
67170
|
+
if (!retryRecovered)
|
|
67171
|
+
continue;
|
|
67172
|
+
lastRecovered = retryRecovered;
|
|
67173
|
+
const retryBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(retryRecovered.signal, targetAppId, platform3);
|
|
67174
|
+
setLatest(retryRecovered);
|
|
67175
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(retryBoundary)) {
|
|
67176
|
+
emitScreenshot(retrySnap.screenshot);
|
|
67177
|
+
log2(` recovered target app after ${attempt} additional activation attempt(s).`);
|
|
67178
|
+
return {
|
|
67179
|
+
resolved: retryRecovered,
|
|
67180
|
+
observation: retryRecovered.observation,
|
|
67181
|
+
externalBlocked: true,
|
|
67182
|
+
externalOwner: boundary.owner
|
|
67183
|
+
};
|
|
67184
|
+
}
|
|
67185
|
+
if (retryBoundary.kind === "transient_external") {
|
|
67186
|
+
log2(` recovery surfaced system overlay ${retryBoundary.owner}; not recording it as a target-app step.`);
|
|
67187
|
+
return {
|
|
67188
|
+
resolved: retryRecovered,
|
|
67189
|
+
observation: `${retryRecovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(retryBoundary)}`,
|
|
67190
|
+
externalBlocked: true,
|
|
67191
|
+
transientExternal: true,
|
|
67192
|
+
externalOwner: boundary.owner
|
|
67193
|
+
};
|
|
67194
|
+
}
|
|
67195
|
+
lastExternalOwner = retryBoundary.owner;
|
|
67196
|
+
} catch (err) {
|
|
67197
|
+
log2(` target app recovery attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
67198
|
+
}
|
|
67199
|
+
}
|
|
67200
|
+
log2(` relaunch still outside target app (${lastExternalOwner}); no target-app step recorded.`);
|
|
67069
67201
|
return {
|
|
67070
|
-
resolved:
|
|
67071
|
-
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${
|
|
67202
|
+
resolved: lastRecovered,
|
|
67203
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${lastExternalOwner}, so no target-app step was recorded)`,
|
|
67072
67204
|
externalBlocked: true,
|
|
67073
67205
|
externalOwner: boundary.owner
|
|
67074
67206
|
};
|
|
@@ -67282,7 +67414,19 @@ ${statePacket}` },
|
|
|
67282
67414
|
return unknownRef(toolArgs.ref);
|
|
67283
67415
|
const beforeScreenName = screenName();
|
|
67284
67416
|
const before = latest();
|
|
67285
|
-
|
|
67417
|
+
let result = await driver.tap(element.bounds);
|
|
67418
|
+
if (!result.ok && (0, mobile_keyboard_js_1.shouldRetryTapAfterKeyboardDismiss)(result.error, before?.screen.elements ?? [])) {
|
|
67419
|
+
log2(" tap target was outside the visible viewport while an input was focused; dismissing keyboard and retrying once.");
|
|
67420
|
+
const dismissResult = await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
67421
|
+
const retryResult = await driver.tap(element.bounds);
|
|
67422
|
+
result = retryResult.ok ? retryResult : {
|
|
67423
|
+
...retryResult,
|
|
67424
|
+
error: retryResult.error ? `${retryResult.error} (retry after keyboard dismiss also failed)` : "Tap retry after keyboard dismiss failed",
|
|
67425
|
+
source: retryResult.source ?? dismissResult.source,
|
|
67426
|
+
screenshot: retryResult.screenshot ?? dismissResult.screenshot,
|
|
67427
|
+
screenSignal: retryResult.screenSignal ?? dismissResult.screenSignal
|
|
67428
|
+
};
|
|
67429
|
+
}
|
|
67286
67430
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67287
67431
|
if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
|
|
67288
67432
|
recordAction({
|
|
@@ -315417,8 +315561,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315417
315561
|
};
|
|
315418
315562
|
}
|
|
315419
315563
|
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.");
|
|
315564
|
+
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", mode, log2);
|
|
315565
|
+
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
315566
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
315423
315567
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
315424
315568
|
const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
|
|
@@ -315660,10 +315804,11 @@ Fatal error: ${msg}`);
|
|
|
315660
315804
|
};
|
|
315661
315805
|
}
|
|
315662
315806
|
}
|
|
315663
|
-
async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
|
|
315807
|
+
async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
|
|
315664
315808
|
void phase;
|
|
315665
315809
|
const resetPolicy = target?.resetPolicy ?? "AUTO";
|
|
315666
|
-
|
|
315810
|
+
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
315811
|
+
if (shouldPreserveAppData) {
|
|
315667
315812
|
try {
|
|
315668
315813
|
await driver.relaunch();
|
|
315669
315814
|
} catch (err) {
|
|
@@ -330184,7 +330329,7 @@ var require_package4 = __commonJS({
|
|
|
330184
330329
|
"package.json"(exports, module) {
|
|
330185
330330
|
module.exports = {
|
|
330186
330331
|
name: "@validate.qa/runner",
|
|
330187
|
-
version: "1.0.
|
|
330332
|
+
version: "1.0.32",
|
|
330188
330333
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330189
330334
|
bin: {
|
|
330190
330335
|
"validate-runner": "dist/cli.js"
|
|
@@ -330848,6 +330993,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
|
|
|
330848
330993
|
async function pressBack(sessionPath) {
|
|
330849
330994
|
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
330850
330995
|
}
|
|
330996
|
+
function isKeyboardAlreadyHiddenError(err) {
|
|
330997
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
330998
|
+
return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
|
|
330999
|
+
}
|
|
331000
|
+
async function dismissKeyboard(sessionPath, platform3) {
|
|
331001
|
+
if (platform3 === "ANDROID") {
|
|
331002
|
+
await pressBack(sessionPath);
|
|
331003
|
+
return;
|
|
331004
|
+
}
|
|
331005
|
+
try {
|
|
331006
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, {
|
|
331007
|
+
method: "POST",
|
|
331008
|
+
body: "{}"
|
|
331009
|
+
});
|
|
331010
|
+
return;
|
|
331011
|
+
} catch (err) {
|
|
331012
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331013
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331014
|
+
}
|
|
331015
|
+
try {
|
|
331016
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
331017
|
+
method: "POST",
|
|
331018
|
+
body: JSON.stringify({ script: "mobile: hideKeyboard", args: [{}] })
|
|
331019
|
+
});
|
|
331020
|
+
return;
|
|
331021
|
+
} catch (err) {
|
|
331022
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331023
|
+
if (isKeyboardAlreadyHiddenError(err)) return;
|
|
331024
|
+
}
|
|
331025
|
+
await pressBack(sessionPath);
|
|
331026
|
+
}
|
|
330851
331027
|
async function pressHome(sessionPath, platform3) {
|
|
330852
331028
|
if (platform3 === "IOS") {
|
|
330853
331029
|
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
@@ -334844,6 +335020,10 @@ var MobileBridgeDriver = class {
|
|
|
334844
335020
|
async back() {
|
|
334845
335021
|
return this.runAction(() => pressBack(this.sessionPath));
|
|
334846
335022
|
}
|
|
335023
|
+
/** Platform-aware software keyboard dismissal. */
|
|
335024
|
+
async dismissKeyboard() {
|
|
335025
|
+
return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
|
|
335026
|
+
}
|
|
334847
335027
|
/** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
|
|
334848
335028
|
async home() {
|
|
334849
335029
|
return this.runAction(() => pressHome(this.sessionPath, this.platform));
|
|
@@ -335229,8 +335409,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
335229
335409
|
pageUrl: call.pageUrl ?? pageUrl
|
|
335230
335410
|
}));
|
|
335231
335411
|
}
|
|
335412
|
+
var runnerPackage = require_package4();
|
|
335232
335413
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
335233
335414
|
var IS_CLOUD = process.env.RUNNER_CLOUD === "true";
|
|
335415
|
+
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
335416
|
+
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
335417
|
+
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
335418
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
335235
335419
|
var DEFAULT_TYPE_LIMITS = {
|
|
335236
335420
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -338064,6 +338248,9 @@ async function startRunner(opts) {
|
|
|
338064
338248
|
method: "POST",
|
|
338065
338249
|
headers,
|
|
338066
338250
|
body: JSON.stringify({
|
|
338251
|
+
runnerName: RUNNER_NAME,
|
|
338252
|
+
runnerVersion: RUNNER_VERSION,
|
|
338253
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
338067
338254
|
runsCompleted,
|
|
338068
338255
|
activeRuns,
|
|
338069
338256
|
maxConcurrent: MAX_CONCURRENT,
|
package/package.json
CHANGED