@validate.qa/runner 1.0.30 → 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 +576 -55
- package/dist/cli.mjs +576 -55
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -2349,40 +2349,40 @@ var init_plans = __esm({
|
|
|
2349
2349
|
type: "extra_projects",
|
|
2350
2350
|
label: "Extra Projects",
|
|
2351
2351
|
description: "Add more projects to your workspace",
|
|
2352
|
-
priceLabel: "$
|
|
2353
|
-
monthlyPriceUsd:
|
|
2352
|
+
priceLabel: "$24.99 / month",
|
|
2353
|
+
monthlyPriceUsd: 24.99,
|
|
2354
2354
|
limitIncrement: 1
|
|
2355
2355
|
},
|
|
2356
2356
|
extra_tests: {
|
|
2357
2357
|
type: "extra_tests",
|
|
2358
2358
|
label: "Extra Tests",
|
|
2359
2359
|
description: "Increase tests per project limit",
|
|
2360
|
-
priceLabel: "$
|
|
2361
|
-
monthlyPriceUsd:
|
|
2362
|
-
limitIncrement:
|
|
2360
|
+
priceLabel: "$12.99 / month",
|
|
2361
|
+
monthlyPriceUsd: 12.99,
|
|
2362
|
+
limitIncrement: 50
|
|
2363
2363
|
},
|
|
2364
2364
|
extra_suite_runs: {
|
|
2365
2365
|
type: "extra_suite_runs",
|
|
2366
2366
|
label: "Extra Suite Runs",
|
|
2367
2367
|
description: "Increase daily suite run limit",
|
|
2368
|
-
priceLabel: "$
|
|
2369
|
-
monthlyPriceUsd:
|
|
2368
|
+
priceLabel: "$12.99 / month",
|
|
2369
|
+
monthlyPriceUsd: 12.99,
|
|
2370
2370
|
limitIncrement: 5
|
|
2371
2371
|
},
|
|
2372
2372
|
extra_discovery_runs: {
|
|
2373
2373
|
type: "extra_discovery_runs",
|
|
2374
2374
|
label: "Extra Discovery Runs",
|
|
2375
2375
|
description: "Increase discovery run limit",
|
|
2376
|
-
priceLabel: "$
|
|
2377
|
-
monthlyPriceUsd:
|
|
2376
|
+
priceLabel: "$19.99 / month",
|
|
2377
|
+
monthlyPriceUsd: 19.99,
|
|
2378
2378
|
limitIncrement: 5
|
|
2379
2379
|
},
|
|
2380
2380
|
extra_test_executions: {
|
|
2381
2381
|
type: "extra_test_executions",
|
|
2382
2382
|
label: "Extra Test Executions",
|
|
2383
2383
|
description: "+100 manual test runs per day, on top of your plan\u2019s daily dashboard run budget",
|
|
2384
|
-
priceLabel: "$
|
|
2385
|
-
monthlyPriceUsd:
|
|
2384
|
+
priceLabel: "$14.99 / month",
|
|
2385
|
+
monthlyPriceUsd: 14.99,
|
|
2386
2386
|
limitIncrement: 100
|
|
2387
2387
|
}
|
|
2388
2388
|
};
|
|
@@ -2407,16 +2407,16 @@ var init_plans = __esm({
|
|
|
2407
2407
|
PLAN_DEFINITIONS = {
|
|
2408
2408
|
standard: {
|
|
2409
2409
|
code: "standard",
|
|
2410
|
-
label: "Standard
|
|
2410
|
+
label: "Standard",
|
|
2411
2411
|
priceLabel: "Free",
|
|
2412
2412
|
monthlyPriceUsd: 0,
|
|
2413
2413
|
billingModel: "free",
|
|
2414
|
-
summary: "For curious developers. Get the full platform - MCP, Discovery, Voice, self-heal - with
|
|
2414
|
+
summary: "For curious developers. Get the full platform - MCP, Discovery, Voice, self-heal - with a starter budget for one real project.",
|
|
2415
2415
|
maxProjects: 1,
|
|
2416
|
-
maxTestsPerProject:
|
|
2416
|
+
maxTestsPerProject: 20,
|
|
2417
2417
|
maxSuiteRunsPerDay: 1,
|
|
2418
|
-
maxTestExecutionsPerDay:
|
|
2419
|
-
//
|
|
2418
|
+
maxTestExecutionsPerDay: 10,
|
|
2419
|
+
// 5 regression / 5 manual at the default 50/50 split
|
|
2420
2420
|
maxDiscoveryRunsPerDay: 1,
|
|
2421
2421
|
// Per day
|
|
2422
2422
|
maxConcurrentDiscoveries: 1,
|
|
@@ -2498,14 +2498,14 @@ var init_plans = __esm({
|
|
|
2498
2498
|
};
|
|
2499
2499
|
TRIAL_DURATION_DAYS = 30;
|
|
2500
2500
|
CREDIT_PRICES_CENTS = {
|
|
2501
|
-
discovery_run:
|
|
2501
|
+
discovery_run: 200,
|
|
2502
|
+
// $2.00
|
|
2503
|
+
suite_run: 50,
|
|
2502
2504
|
// $0.50
|
|
2503
|
-
|
|
2504
|
-
// $0.
|
|
2505
|
-
|
|
2506
|
-
// $0.
|
|
2507
|
-
heal: 10
|
|
2508
|
-
// $0.10
|
|
2505
|
+
test_run: 15,
|
|
2506
|
+
// $0.15
|
|
2507
|
+
heal: 75
|
|
2508
|
+
// $0.75
|
|
2509
2509
|
};
|
|
2510
2510
|
AI_CREDIT_MARGIN = 1.5;
|
|
2511
2511
|
CREDIT_PACKS_CENTS = [2e3, 5e3, 1e4];
|
|
@@ -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) {
|
|
@@ -64547,6 +64621,78 @@ var require_discover_ai_planner = __commonJS({
|
|
|
64547
64621
|
function meaningfulTokens(name) {
|
|
64548
64622
|
return name.toLowerCase().split(/[\s\--_]+/).filter((t) => t.length > 2 && !JOURNEY_MATCH_STOPWORDS.has(t));
|
|
64549
64623
|
}
|
|
64624
|
+
function textIncludesCredentialField(text, field) {
|
|
64625
|
+
return field === "email" ? /\b(e-?mail|username|user\s*name|login\s*id)\b/i.test(text) : /\bpassword|passcode|pin\b/i.test(text);
|
|
64626
|
+
}
|
|
64627
|
+
function textIncludesCredentialSubmit(text) {
|
|
64628
|
+
return /\b(click|tap|press|submit|send)\b[^.]{0,80}\b(sign[ -]?in|signin|log[ -]?in|login|submit|continue)\b/i.test(text) || /\b(sign[ -]?in|signin|log[ -]?in|login|submit|continue)\b[^.]{0,80}\b(button|cta|form)\b/i.test(text) || /\b(submit|sign[ -]?in|signin|log[ -]?in|login)\b[^.]{0,80}\b(test\s+)?credentials\b/i.test(text) || /\b(test\s+)?credentials\b[^.]{0,80}\b(submit|sign[ -]?in|signin|log[ -]?in|login)\b/i.test(text);
|
|
64629
|
+
}
|
|
64630
|
+
function targetPagesHaveCredentialControls(siteMap, targetPages) {
|
|
64631
|
+
const targetSet = new Set(targetPages.map(normalizePlannerRoute));
|
|
64632
|
+
let hasEmail = false;
|
|
64633
|
+
let hasPassword = false;
|
|
64634
|
+
let hasSubmit = false;
|
|
64635
|
+
for (const page of siteMap.pages) {
|
|
64636
|
+
if (!targetSet.has(normalizePlannerRoute(page.route)))
|
|
64637
|
+
continue;
|
|
64638
|
+
for (const element of page.elements ?? []) {
|
|
64639
|
+
const label = [
|
|
64640
|
+
element.label,
|
|
64641
|
+
element.role,
|
|
64642
|
+
element.testId,
|
|
64643
|
+
element.name
|
|
64644
|
+
].filter(Boolean).join(" ");
|
|
64645
|
+
if (!label)
|
|
64646
|
+
continue;
|
|
64647
|
+
if (textIncludesCredentialField(label, "email"))
|
|
64648
|
+
hasEmail = true;
|
|
64649
|
+
if (textIncludesCredentialField(label, "password"))
|
|
64650
|
+
hasPassword = true;
|
|
64651
|
+
if (/\b(sign[ -]?in|signin|log[ -]?in|login|submit|continue)\b/i.test(label) && !/\b(google|apple|facebook|github|sso|oauth|magic\s*link)\b/i.test(label)) {
|
|
64652
|
+
hasSubmit = true;
|
|
64653
|
+
}
|
|
64654
|
+
}
|
|
64655
|
+
}
|
|
64656
|
+
return hasEmail && hasPassword && hasSubmit;
|
|
64657
|
+
}
|
|
64658
|
+
function journeyCoversCredentialSubmit(journey) {
|
|
64659
|
+
if (!journey)
|
|
64660
|
+
return false;
|
|
64661
|
+
const text = [
|
|
64662
|
+
journey.name,
|
|
64663
|
+
journey.expectedOutcome,
|
|
64664
|
+
...journey.steps ?? [],
|
|
64665
|
+
...journey.provenCommands ?? []
|
|
64666
|
+
].join(" ");
|
|
64667
|
+
const hasCredentialPair = /\bcredentials\b/i.test(text) || textIncludesCredentialField(text, "email") && textIncludesCredentialField(text, "password");
|
|
64668
|
+
return hasCredentialPair && textIncludesCredentialSubmit(text);
|
|
64669
|
+
}
|
|
64670
|
+
function scenarioAuthCredentialPolicy(scenario, steps) {
|
|
64671
|
+
const text = [
|
|
64672
|
+
scenario.name,
|
|
64673
|
+
scenario.goal,
|
|
64674
|
+
scenario.type,
|
|
64675
|
+
scenario.variant,
|
|
64676
|
+
...steps.map((step) => step.action)
|
|
64677
|
+
].filter(Boolean).join(" ");
|
|
64678
|
+
if (!/\b(auth|authentication|sign[ -]?in|signin|log[ -]?in|login|session)\b/i.test(text)) {
|
|
64679
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64680
|
+
}
|
|
64681
|
+
if (/\b(log[ -]?out|logout|sign[ -]?out|signout|forgot|reset|recover|magic\s*link|otp|mfa|2fa|sso|google|apple|facebook)\b/i.test(text)) {
|
|
64682
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64683
|
+
}
|
|
64684
|
+
if (!/\b(credential|e-?mail|username|user\s*name|password|valid|invalid|empty|required|wrong|case|special|leading|trailing)\b/i.test(text)) {
|
|
64685
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64686
|
+
}
|
|
64687
|
+
const uiOnly = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/i.test(text);
|
|
64688
|
+
const negative = /\b(invalid|empty|required|missing|wrong|error|validation|case|special|leading|trailing)\b/i.test(text);
|
|
64689
|
+
const happy = /\b(happy|valid\s+credentials|success|successful)\b/i.test(text) || scenario.type === "HAPPY_PATH" || scenario.variant === "happy";
|
|
64690
|
+
return {
|
|
64691
|
+
needsCredentialEvidence: true,
|
|
64692
|
+
needsSubmitStep: !uiOnly && (happy || negative),
|
|
64693
|
+
reason: uiOnly ? "credential UI scenario" : negative ? "credential validation scenario" : "credential sign-in scenario"
|
|
64694
|
+
};
|
|
64695
|
+
}
|
|
64550
64696
|
function truncateOutcome(text, limit) {
|
|
64551
64697
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
64552
64698
|
if (normalized.length <= limit)
|
|
@@ -65312,6 +65458,18 @@ Each entry is exactly one of:
|
|
|
65312
65458
|
if (matchSource === "route") {
|
|
65313
65459
|
warnings.push(`"${sc.name}" matched to journey "${matchingSuggestion.name}" by route overlap only - evidence may be weak`);
|
|
65314
65460
|
}
|
|
65461
|
+
const authCredentialPolicy = scenarioAuthCredentialPolicy(sc, steps);
|
|
65462
|
+
if (authCredentialPolicy.needsCredentialEvidence) {
|
|
65463
|
+
const hasCredentialEvidence = targetPagesHaveCredentialControls(siteMap, sc.targetPages) || journeyCoversCredentialSubmit(matchingSuggestion);
|
|
65464
|
+
if (!hasCredentialEvidence) {
|
|
65465
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} lacks observed email/password/sign-in controls or a recorded credential journey`);
|
|
65466
|
+
continue;
|
|
65467
|
+
}
|
|
65468
|
+
if (authCredentialPolicy.needsSubmitStep && !steps.some((step) => textIncludesCredentialSubmit(step.action)) && !journeyCoversCredentialSubmit(matchingSuggestion)) {
|
|
65469
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} does not include a concrete sign-in submit step`);
|
|
65470
|
+
continue;
|
|
65471
|
+
}
|
|
65472
|
+
}
|
|
65315
65473
|
const evidence = (0, discover_planner_js_1.extractScenarioEvidence)(
|
|
65316
65474
|
siteMap.pages,
|
|
65317
65475
|
sc.targetPages,
|
|
@@ -65857,6 +66015,25 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
65857
66015
|
function targetAppScreens(graph, appId) {
|
|
65858
66016
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
65859
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
|
+
}
|
|
65860
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.
|
|
65861
66038
|
|
|
65862
66039
|
## Output Discipline
|
|
@@ -65876,8 +66053,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65876
66053
|
5. CRUD screens for the same entity belong together
|
|
65877
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).
|
|
65878
66055
|
7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
|
|
65879
|
-
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.
|
|
65880
|
-
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.
|
|
65881
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.
|
|
65882
66059
|
|
|
65883
66060
|
## Output JSON Schema
|
|
@@ -65977,6 +66154,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65977
66154
|
## Coverage Strategy
|
|
65978
66155
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 12) + 1 EDGE_CASE (empty/invalid submission)
|
|
65979
66156
|
- For auth flows: sign-in happy path (verify the target screen AND a concrete signed-in/session signal per rule 13) + validation (wrong password, empty fields)
|
|
66157
|
+
- For credential sign-in flows: only plan credential scenarios when evidence shows the credential path itself \u2014 an email/username field, a password field, and a native Sign In/Login/Submit control on the target screen, or an Explorer-Recorded Journey that typed those fields and submitted them. Do NOT treat Google/Apple/Facebook/SSO buttons, magic-link entry, logout menus, profile menus, or already-signed-in screens as evidence for credential sign-in. If the explorer only observed signed-in menu/logout/session surfaces, plan session/logout scenarios only; surface missing credential coverage as a gap instead of inventing credential sign-in variants.
|
|
65980
66158
|
- For CRUD: each scenario MUST own the entity it operates on. Read/search/update/delete/cancel-on-existing/view-existing scenarios MUST include a create step earlier in the same scenario (or declare an immutable seed fixture by name). Never assume a record from prior runs or sibling tests exists. When create/delete are tightly coupled, bundle into a single USER_JOURNEY: create \u2192 verify \u2192 delete \u2192 verify removal.
|
|
65981
66159
|
- For AI/async features (Generate, Analyze, Process, Import, Sync): happy path MUST complete the flow per rule 12 - never stop at "button tapped" or "sheet opened"
|
|
65982
66160
|
- For navigation: multi-screen user journeys within the feature scope (do NOT drift into unrelated features)
|
|
@@ -66143,7 +66321,7 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66143
66321
|
sections.push(`## Feature: ${context.featureName}
|
|
66144
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.`);
|
|
66145
66323
|
}
|
|
66146
|
-
const orderedScreens =
|
|
66324
|
+
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66147
66325
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66148
66326
|
sections.push(`## Screens Discovered (${orderedScreens.length})`);
|
|
66149
66327
|
for (const state2 of orderedScreens) {
|
|
@@ -66252,7 +66430,7 @@ ${constraints.join("\n")}`);
|
|
|
66252
66430
|
const now = /* @__PURE__ */ new Date();
|
|
66253
66431
|
const idByScreen = /* @__PURE__ */ new Map();
|
|
66254
66432
|
let pageSeq = 0;
|
|
66255
|
-
const orderedScreens =
|
|
66433
|
+
const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66256
66434
|
for (const state2 of orderedScreens) {
|
|
66257
66435
|
idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
|
|
66258
66436
|
}
|
|
@@ -66313,7 +66491,7 @@ ${constraints.join("\n")}`);
|
|
|
66313
66491
|
}
|
|
66314
66492
|
function buildMobileFeatureGroupsFromGraph(graph, appId) {
|
|
66315
66493
|
const groups = /* @__PURE__ */ new Map();
|
|
66316
|
-
const orderedScreens =
|
|
66494
|
+
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66317
66495
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66318
66496
|
for (const state2 of orderedScreens) {
|
|
66319
66497
|
const screenTypeLower = state2.screenType?.toLowerCase();
|
|
@@ -66360,7 +66538,7 @@ ${constraints.join("\n")}`);
|
|
|
66360
66538
|
return [];
|
|
66361
66539
|
if (input.featureName?.trim())
|
|
66362
66540
|
return [];
|
|
66363
|
-
const orderedScreens =
|
|
66541
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66364
66542
|
if (orderedScreens.length === 0)
|
|
66365
66543
|
return [];
|
|
66366
66544
|
const entryScreenId = orderedScreens[0].screenId;
|
|
@@ -66562,9 +66740,11 @@ var require_mobile_generator = __commonJS({
|
|
|
66562
66740
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
66563
66741
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
66564
66742
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
66743
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
66565
66744
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
66566
66745
|
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
66567
66746
|
var MAX_RECENT_EXCHANGES = 24;
|
|
66747
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
66568
66748
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
66569
66749
|
function finalizeAction(input) {
|
|
66570
66750
|
return { ...input, url: "", selector: input.selector ?? "" };
|
|
@@ -66980,10 +67160,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
66980
67160
|
externalOwner: boundary.owner
|
|
66981
67161
|
};
|
|
66982
67162
|
}
|
|
66983
|
-
|
|
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.`);
|
|
66984
67201
|
return {
|
|
66985
|
-
resolved:
|
|
66986
|
-
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)`,
|
|
66987
67204
|
externalBlocked: true,
|
|
66988
67205
|
externalOwner: boundary.owner
|
|
66989
67206
|
};
|
|
@@ -67197,7 +67414,19 @@ ${statePacket}` },
|
|
|
67197
67414
|
return unknownRef(toolArgs.ref);
|
|
67198
67415
|
const beforeScreenName = screenName();
|
|
67199
67416
|
const before = latest();
|
|
67200
|
-
|
|
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
|
+
}
|
|
67201
67430
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67202
67431
|
if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
|
|
67203
67432
|
recordAction({
|
|
@@ -67453,7 +67682,7 @@ ${statePacket}` },
|
|
|
67453
67682
|
"react-root",
|
|
67454
67683
|
"application"
|
|
67455
67684
|
]);
|
|
67456
|
-
var STRUCTURAL_ASSERT_ID_RE = /^(?:step[-_]\d+|(?:auth|form|wizard)[-_]?step(?:[-_]\d+)?|(?:main[-_]?content|root|app[-_]?root|__next|react[-_]?root|application|content|container|wrapper|webview|layout|view|screen|page)(?:[-_]\d+)?)$/i;
|
|
67685
|
+
var STRUCTURAL_ASSERT_ID_RE = /^(?:skip\s*to\s*main\s*content|skip[-_]?to[-_]?main[-_]?content|step[-_]\d+|(?:auth|form|wizard)[-_]?step(?:[-_]\d+)?|(?:main[-_]?content|root|app[-_]?root|__next|react[-_]?root|application|content|container|wrapper|webview|layout|view|screen|page)(?:[-_]\d+)?)$/i;
|
|
67457
67686
|
function locatorLocalName(value) {
|
|
67458
67687
|
const trimmed = value.trim();
|
|
67459
67688
|
const withoutStrategy = trimmed.startsWith("id=") ? trimmed.slice(3) : trimmed.startsWith("~") ? trimmed.slice(1) : trimmed;
|
|
@@ -67533,6 +67762,88 @@ ${statePacket}` },
|
|
|
67533
67762
|
}));
|
|
67534
67763
|
return true;
|
|
67535
67764
|
}
|
|
67765
|
+
function scenarioText(scenario) {
|
|
67766
|
+
return [
|
|
67767
|
+
scenario.name,
|
|
67768
|
+
scenario.goal,
|
|
67769
|
+
...scenario.steps.flatMap((step) => [step.action, step.target, step.hint]),
|
|
67770
|
+
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67771
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67772
|
+
}
|
|
67773
|
+
function actionText(action) {
|
|
67774
|
+
const bundle = action.metadata.locatorBundle;
|
|
67775
|
+
return [
|
|
67776
|
+
action.type,
|
|
67777
|
+
action.selector,
|
|
67778
|
+
action.value,
|
|
67779
|
+
action.innerText,
|
|
67780
|
+
action.metadata.targetLabel,
|
|
67781
|
+
bundle?.text,
|
|
67782
|
+
bundle?.contentDesc,
|
|
67783
|
+
bundle?.accessibilityId,
|
|
67784
|
+
bundle?.resourceId,
|
|
67785
|
+
bundle?.className,
|
|
67786
|
+
bundle?.xpath
|
|
67787
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67788
|
+
}
|
|
67789
|
+
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67790
|
+
const text = scenarioText(scenario);
|
|
67791
|
+
const isCredentialSignIn = /\b(sign\s*in|log\s*in|login|authenticate|auth)\b/.test(text) && /\b(email|e-mail|password|credential|valid credentials?)\b/.test(text) && !/\b(reset|forgot|recover|logout|log\s*out|sign\s*out)\b/.test(text);
|
|
67792
|
+
const typedEmailIndexes = [];
|
|
67793
|
+
const typedPasswordIndexes = [];
|
|
67794
|
+
actions.forEach((action, index) => {
|
|
67795
|
+
if (action.type !== "TYPE")
|
|
67796
|
+
return;
|
|
67797
|
+
const textForAction = actionText(action);
|
|
67798
|
+
if (/\b(email|e-mail|username|user name|login)\b|\{\{test_user_email\}\}/i.test(textForAction)) {
|
|
67799
|
+
typedEmailIndexes.push(index);
|
|
67800
|
+
}
|
|
67801
|
+
if (/\b(password|passcode|pin)\b|\{\{test_user_password\}\}/i.test(textForAction)) {
|
|
67802
|
+
typedPasswordIndexes.push(index);
|
|
67803
|
+
}
|
|
67804
|
+
});
|
|
67805
|
+
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67806
|
+
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67807
|
+
const submitAfterCredentials = actions.some((action, index) => {
|
|
67808
|
+
if (action.type !== "TAP" || index <= lastCredentialIndex)
|
|
67809
|
+
return false;
|
|
67810
|
+
const textForAction = actionText(action);
|
|
67811
|
+
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
67812
|
+
return false;
|
|
67813
|
+
return !/\b(with\s+(email|e-mail|google|facebook|apple)|email\s+form|showemailformbtn)\b/i.test(textForAction);
|
|
67814
|
+
});
|
|
67815
|
+
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67816
|
+
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67817
|
+
return "credential fields were filled but no sign-in submit tap was recorded after them";
|
|
67818
|
+
}
|
|
67819
|
+
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67820
|
+
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67821
|
+
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67822
|
+
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67823
|
+
if (action.type !== "TAP")
|
|
67824
|
+
return false;
|
|
67825
|
+
const textForAction = actionText(action);
|
|
67826
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67827
|
+
return false;
|
|
67828
|
+
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67829
|
+
});
|
|
67830
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67831
|
+
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67832
|
+
}
|
|
67833
|
+
if (!isCredentialSignIn)
|
|
67834
|
+
return null;
|
|
67835
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
67836
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
67837
|
+
const tappedSubmit = submitAfterCredentials;
|
|
67838
|
+
if (typedEmail && typedPassword && tappedSubmit)
|
|
67839
|
+
return null;
|
|
67840
|
+
const missing = [
|
|
67841
|
+
typedEmail ? null : "email/username entry",
|
|
67842
|
+
typedPassword ? null : "password entry",
|
|
67843
|
+
tappedSubmit ? null : "sign-in submit tap"
|
|
67844
|
+
].filter((value) => value !== null);
|
|
67845
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67846
|
+
}
|
|
67536
67847
|
function actionCycleKey(action) {
|
|
67537
67848
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67538
67849
|
}
|
|
@@ -67586,6 +67897,11 @@ ${statePacket}` },
|
|
|
67586
67897
|
if (actions.length < beforeContainerDrop) {
|
|
67587
67898
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
67588
67899
|
}
|
|
67900
|
+
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
67901
|
+
if (intentMismatch) {
|
|
67902
|
+
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
67903
|
+
return null;
|
|
67904
|
+
}
|
|
67589
67905
|
const floorAppended = appendHybridFloor(actions, recording, platform3);
|
|
67590
67906
|
if (floorAppended) {
|
|
67591
67907
|
log2(" appended hybrid durable-outcome floor (landing-screen ASSERT_VISIBLE).");
|
|
@@ -68283,7 +68599,8 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
68283
68599
|
2. STEP RESULTS: which steps passed before the failure.
|
|
68284
68600
|
3. TREE: does the current screen match what the failed step expects?
|
|
68285
68601
|
3b. STALE EMPTY-STATE ASSERT: a failing assert on one-time / data-dependent UI (empty-state CTAs, first-run prompts, onboarding banners) is NOT a locator problem \u2014 the element is gone because the account now has data. REPLACE the assertion with a durable landing-screen anchor (tab label, header, nav bar) that proves the same outcome (verdict fix).
|
|
68286
|
-
4.
|
|
68602
|
+
4. EXTERNAL AUTH HANDOFF: if the failure says the foreground app changed outside the target app after a Login / Sign In tap, and the steps never enter email/password credentials, do NOT heal by choosing Google/Apple/Facebook/SSO or another external app. If the test later expects Logout/session state, return test_invalid or prereq_unmet: the recorded test lacks a real credential setup.
|
|
68603
|
+
5. ENTRY-SCREEN DRIFT ("unexpected screen"/"screen drift"): the app started on a DIFFERENT screen than recorded; the fix is usually a MISSING NAVIGATION step (e.g. tap the onboarding/welcome CTA present in the tree before the recorded first step). Author it from the tree if you can (verdict fix); if a single static tree is insufficient, return needs_device.
|
|
68287
68604
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68288
68605
|
{"verdict":"fix","steps":[<FULL healed MobileTestStep array, all steps, your changes applied, order/metadata preserved>],"reason":"what changed and why","confidence":0.0-1.0}
|
|
68289
68606
|
{"verdict":"app_bug","reason":"app crashed/blank/5xx (NOT rate-limit)"}
|
|
@@ -68340,6 +68657,104 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68340
68657
|
function isDrift(error2) {
|
|
68341
68658
|
return /unexpected screen|screen drift/i.test(error2 ?? "");
|
|
68342
68659
|
}
|
|
68660
|
+
function mobileStepSearchText(step) {
|
|
68661
|
+
const rawTarget = step.target;
|
|
68662
|
+
const structuredTarget = rawTarget && typeof rawTarget === "object" ? rawTarget : void 0;
|
|
68663
|
+
const bundle = structuredTarget?.locatorBundle;
|
|
68664
|
+
const stepValue = step.value;
|
|
68665
|
+
return [
|
|
68666
|
+
step.action,
|
|
68667
|
+
step.description,
|
|
68668
|
+
typeof stepValue === "string" ? stepValue : void 0,
|
|
68669
|
+
typeof rawTarget === "string" ? rawTarget : void 0,
|
|
68670
|
+
structuredTarget?.primary,
|
|
68671
|
+
bundle?.text,
|
|
68672
|
+
bundle?.contentDesc,
|
|
68673
|
+
bundle?.accessibilityId,
|
|
68674
|
+
bundle?.resourceId,
|
|
68675
|
+
bundle?.className,
|
|
68676
|
+
bundle?.xpath
|
|
68677
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
68678
|
+
}
|
|
68679
|
+
function detectInvalidExternalAuthSetup(steps, error2) {
|
|
68680
|
+
if (!/foreground app changed|outside target app|com\.google\.android\.gms|external app/i.test(error2 ?? "")) {
|
|
68681
|
+
return null;
|
|
68682
|
+
}
|
|
68683
|
+
const hasBareLoginTap = steps.some((step) => {
|
|
68684
|
+
if (!/\btap\b/i.test(step.action))
|
|
68685
|
+
return false;
|
|
68686
|
+
const text = mobileStepSearchText(step);
|
|
68687
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(text))
|
|
68688
|
+
return false;
|
|
68689
|
+
return /\b(sign\s*in|signin|log\s*in|login)\b/.test(text);
|
|
68690
|
+
});
|
|
68691
|
+
if (!hasBareLoginTap)
|
|
68692
|
+
return null;
|
|
68693
|
+
const hasCredentialEntry = steps.some((step) => {
|
|
68694
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
68695
|
+
return false;
|
|
68696
|
+
const text = mobileStepSearchText(step);
|
|
68697
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68698
|
+
});
|
|
68699
|
+
if (hasCredentialEntry)
|
|
68700
|
+
return null;
|
|
68701
|
+
return "bare Login/Sign In tap left the target app without any recorded credential setup; auth test is invalid until discovery records a real credential path";
|
|
68702
|
+
}
|
|
68703
|
+
function isCredentialFieldStep(step) {
|
|
68704
|
+
const text = mobileStepSearchText(step);
|
|
68705
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68706
|
+
}
|
|
68707
|
+
function isCredentialSubmitTap(step) {
|
|
68708
|
+
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68709
|
+
return false;
|
|
68710
|
+
const text = mobileStepSearchText(step);
|
|
68711
|
+
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|google|apple|facebook|github|sso|showemailform|with\s+email|email\s+form)\b/.test(text)) {
|
|
68712
|
+
return false;
|
|
68713
|
+
}
|
|
68714
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit)\b/.test(text);
|
|
68715
|
+
}
|
|
68716
|
+
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68717
|
+
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
68718
|
+
return null;
|
|
68719
|
+
}
|
|
68720
|
+
const allText = steps.map(mobileStepSearchText).join(" ");
|
|
68721
|
+
if (!/\b(email|e-mail|username|user\s*name|password|passcode|pin)\b/.test(allText))
|
|
68722
|
+
return null;
|
|
68723
|
+
const uiOnlyCredentialScenario = /\b(mask|masked|unmask|visibility|visible password|show password|hide password|keyboard|ime|input type|max length|maxlength|character limit)\b/.test(allText);
|
|
68724
|
+
let lastCredentialEditOrder = -1;
|
|
68725
|
+
for (const step of steps) {
|
|
68726
|
+
if (!/\b(type|fill|input|enter|clear)\b/i.test(step.action))
|
|
68727
|
+
continue;
|
|
68728
|
+
if (!isCredentialFieldStep(step))
|
|
68729
|
+
continue;
|
|
68730
|
+
lastCredentialEditOrder = Math.max(lastCredentialEditOrder, step.order ?? -1);
|
|
68731
|
+
}
|
|
68732
|
+
if (lastCredentialEditOrder < 0)
|
|
68733
|
+
return null;
|
|
68734
|
+
if (uiOnlyCredentialScenario) {
|
|
68735
|
+
const hasCredentialFormEvidence = steps.some((step) => {
|
|
68736
|
+
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68737
|
+
return false;
|
|
68738
|
+
const text = mobileStepSearchText(step);
|
|
68739
|
+
return /\b(showemailformbtn|with\s+email|email\s+form|sign in with email|loginform|login form|email|e-mail|username|user\s*name)\b/.test(text);
|
|
68740
|
+
});
|
|
68741
|
+
if (hasCredentialFormEvidence)
|
|
68742
|
+
return null;
|
|
68743
|
+
return "credential-control test edits a credential field without recording a path to the email/password form; stale auth-control test is invalid until generation records a complete form path";
|
|
68744
|
+
}
|
|
68745
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isCredentialSubmitTap(step));
|
|
68746
|
+
if (hasSubmitAfterCredentialEdit)
|
|
68747
|
+
return null;
|
|
68748
|
+
const hasValidationAssertion = steps.some((step) => {
|
|
68749
|
+
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68750
|
+
return false;
|
|
68751
|
+
const text = mobileStepSearchText(step);
|
|
68752
|
+
return /\b(invalid|required|error|empty|missing|incorrect|wrong|try again|validation|must enter|enter (an )?(email|password)|email is required|password is required)\b/.test(text);
|
|
68753
|
+
});
|
|
68754
|
+
if (hasValidationAssertion)
|
|
68755
|
+
return null;
|
|
68756
|
+
return "credential field was edited but the test never submits the form or asserts a validation result; stale auth-validation test is invalid until generation records a complete flow";
|
|
68757
|
+
}
|
|
68343
68758
|
function stripCodeFences(text) {
|
|
68344
68759
|
const trimmed = text.trim();
|
|
68345
68760
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -68598,6 +69013,24 @@ ${args.directive}`);
|
|
|
68598
69013
|
const registryPresent = options.selectorRegistryContext != null;
|
|
68599
69014
|
const platform3 = target.platform;
|
|
68600
69015
|
const trimmedLogs = tailLimit(ctx.logs ?? "", MAX_LOG_CHARS);
|
|
69016
|
+
const invalidExternalAuthSetup = detectInvalidExternalAuthSetup(steps, ctx.error);
|
|
69017
|
+
if (invalidExternalAuthSetup) {
|
|
69018
|
+
return {
|
|
69019
|
+
passed: false,
|
|
69020
|
+
verdict: "test_invalid",
|
|
69021
|
+
healReason: invalidExternalAuthSetup,
|
|
69022
|
+
attemptsUsed: 0
|
|
69023
|
+
};
|
|
69024
|
+
}
|
|
69025
|
+
const invalidAuthValidationSetup = detectInvalidAuthValidationSetup(steps, ctx.error);
|
|
69026
|
+
if (invalidAuthValidationSetup) {
|
|
69027
|
+
return {
|
|
69028
|
+
passed: false,
|
|
69029
|
+
verdict: "test_invalid",
|
|
69030
|
+
healReason: invalidAuthValidationSetup,
|
|
69031
|
+
attemptsUsed: 0
|
|
69032
|
+
};
|
|
69033
|
+
}
|
|
68601
69034
|
let attemptCap = base2;
|
|
68602
69035
|
let recoverableUsed = 0;
|
|
68603
69036
|
let bonusUsed = 0;
|
|
@@ -68783,7 +69216,7 @@ ${args.directive}`);
|
|
|
68783
69216
|
removeTags: script.removeTags
|
|
68784
69217
|
};
|
|
68785
69218
|
}
|
|
68786
|
-
var DEFAULT_PHASE2_MAX_PASSES =
|
|
69219
|
+
var DEFAULT_PHASE2_MAX_PASSES = 4;
|
|
68787
69220
|
exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = `You are a QA engineer fixing a failing Appium (mobile UI automation) test with LIVE device access (Phase 2). You receive: the failing test STEPS as JSON, the PRIOR failure error, and the LIVE current screen accessibility tree observed on-device RIGHT NOW. Unlike a frozen failure snapshot, this LIVE tree is the REAL screen the app is on this instant.
|
|
68788
69221
|
## Key insight \u2014 ENTRY-SCREEN DRIFT
|
|
68789
69222
|
The recorded flow may have expected a DIFFERENT starting screen than the one shown. When the LIVE screen is an earlier/other screen (e.g. an onboarding / welcome / splash screen), the fix is almost always a MISSING NAVIGATION step: AUTHOR the leading step(s) (e.g. a tap on the onboarding CTA present in the tree) that reach the recorded first screen, then keep the recorded steps. This is exactly what a static failing-source cannot reveal but a LIVE screen can.
|
|
@@ -68792,7 +69225,8 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
68792
69225
|
2. If they differ, PREPEND the missing navigation step(s), authored from IDENTITY locators (resource-id, accessibility-id, or EXACT @text=) that are PRESENT in the LIVE tree \u2014 never a loose contains().
|
|
68793
69226
|
3. If a locator is simply wrong, swap it for a stronger identity locator present in the LIVE tree.
|
|
68794
69227
|
3b. If the failing step ASSERTS one-time / data-dependent UI (an empty-state CTA or first-run prompt) that is absent from the LIVE tree while the screen is otherwise correct, the assert is STALE \u2014 replace it with a durable element of the same screen (tab label, header, nav bar) and keep the rest.
|
|
68795
|
-
4. If
|
|
69228
|
+
4. If the prior failure says the foreground app changed outside the target app after a Login / Sign In tap, and the steps never enter email/password credentials, do NOT observe/tap Google/Apple/Facebook/SSO. For logout/session tests, return test_invalid or prereq_unmet because the recorded test lacks a real credential setup.
|
|
69229
|
+
5. If you cannot yet SEE the screen you need, use "observe" to advance ONE step, then you will be re-shown the new live screen.
|
|
68796
69230
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68797
69231
|
{"verdict":"fix","steps":[<FULL healed MobileTestStep array, all steps incl. any prepended navigation, order/metadata coherent>],"reason":"what changed and why","confidence":0.0-1.0}
|
|
68798
69232
|
{"verdict":"observe","action":{"type":"tap","target":"mobile.text('<exact label visible in the live tree>')"}}
|
|
@@ -315127,8 +315561,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315127
315561
|
};
|
|
315128
315562
|
}
|
|
315129
315563
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
315130
|
-
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", log2);
|
|
315131
|
-
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.");
|
|
315132
315566
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
315133
315567
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
315134
315568
|
const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
|
|
@@ -315370,10 +315804,11 @@ Fatal error: ${msg}`);
|
|
|
315370
315804
|
};
|
|
315371
315805
|
}
|
|
315372
315806
|
}
|
|
315373
|
-
async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
|
|
315807
|
+
async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
|
|
315374
315808
|
void phase;
|
|
315375
315809
|
const resetPolicy = target?.resetPolicy ?? "AUTO";
|
|
315376
|
-
|
|
315810
|
+
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
315811
|
+
if (shouldPreserveAppData) {
|
|
315377
315812
|
try {
|
|
315378
315813
|
await driver.relaunch();
|
|
315379
315814
|
} catch (err) {
|
|
@@ -329894,7 +330329,7 @@ var require_package4 = __commonJS({
|
|
|
329894
330329
|
"package.json"(exports, module) {
|
|
329895
330330
|
module.exports = {
|
|
329896
330331
|
name: "@validate.qa/runner",
|
|
329897
|
-
version: "1.0.
|
|
330332
|
+
version: "1.0.32",
|
|
329898
330333
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
329899
330334
|
bin: {
|
|
329900
330335
|
"validate-runner": "dist/cli.js"
|
|
@@ -330043,6 +330478,8 @@ var MAX_RETRIES = 3;
|
|
|
330043
330478
|
var STARTUP_TIMEOUT_MS = 3e4;
|
|
330044
330479
|
var HEALTH_CHECK_INTERVAL_MS = 5e3;
|
|
330045
330480
|
var STABLE_AFTER_MS = 6e4;
|
|
330481
|
+
var DEFAULT_APPIUM_LOG_LEVEL = "warn";
|
|
330482
|
+
var MAX_APPIUM_LOG_LINE_CHARS = 4e3;
|
|
330046
330483
|
var appiumProcess = null;
|
|
330047
330484
|
var serverState = {
|
|
330048
330485
|
running: false,
|
|
@@ -330102,6 +330539,23 @@ function appendLog(line) {
|
|
|
330102
330539
|
logs = logs.slice(-MAX_LOG_LINES);
|
|
330103
330540
|
}
|
|
330104
330541
|
}
|
|
330542
|
+
function resolveAppiumLogLevel() {
|
|
330543
|
+
const raw = process.env.APPIUM_LOG_LEVEL?.trim();
|
|
330544
|
+
if (raw && /^[a-z]+(?::[a-z]+)?$/i.test(raw)) return raw;
|
|
330545
|
+
return DEFAULT_APPIUM_LOG_LEVEL;
|
|
330546
|
+
}
|
|
330547
|
+
function buildAppiumArgs(port, drivers) {
|
|
330548
|
+
const args = ["--port", String(port), "--log-level", resolveAppiumLogLevel()];
|
|
330549
|
+
if (drivers && drivers.length > 0) {
|
|
330550
|
+
args.push("--use-drivers", drivers.join(","));
|
|
330551
|
+
}
|
|
330552
|
+
return args;
|
|
330553
|
+
}
|
|
330554
|
+
function sanitizeAppiumLogLine(line) {
|
|
330555
|
+
const redacted = line.replace(/"text"\s*:\s*"[^"]*"/g, '"text":"[REDACTED]"').replace(/"password"\s*:\s*"[^"]*"/gi, '"password":"[REDACTED]"').replace(/"token"\s*:\s*"[^"]*"/gi, '"token":"[REDACTED]"').replace(/"value"\s*:\s*\[(?:"[^"]*",?){8,}\]/g, '"value":["[REDACTED]"]').replace(/"value"\s*:\s*"[A-Za-z0-9+/=]{512,}"/g, '"value":"[BASE64_REDACTED]"');
|
|
330556
|
+
if (redacted.length <= MAX_APPIUM_LOG_LINE_CHARS) return redacted;
|
|
330557
|
+
return `${redacted.slice(0, MAX_APPIUM_LOG_LINE_CHARS)}\u2026 [truncated ${redacted.length - MAX_APPIUM_LOG_LINE_CHARS} chars]`;
|
|
330558
|
+
}
|
|
330105
330559
|
async function checkAppiumHealth(port = serverState.port) {
|
|
330106
330560
|
try {
|
|
330107
330561
|
const controller = new AbortController();
|
|
@@ -330134,10 +330588,7 @@ async function startAppiumServer(options) {
|
|
|
330134
330588
|
return serverState;
|
|
330135
330589
|
}
|
|
330136
330590
|
const port = options?.port ?? APPIUM_PORT;
|
|
330137
|
-
const args =
|
|
330138
|
-
if (options?.drivers && options.drivers.length > 0) {
|
|
330139
|
-
args.push("--use-drivers", options.drivers.join(","));
|
|
330140
|
-
}
|
|
330591
|
+
const args = buildAppiumArgs(port, options?.drivers);
|
|
330141
330592
|
intentionalStop = false;
|
|
330142
330593
|
lastStartOptions = { ...lastStartOptions, ...options };
|
|
330143
330594
|
if (appiumGaveUp) {
|
|
@@ -330156,14 +330607,14 @@ async function startAppiumServer(options) {
|
|
|
330156
330607
|
serverState.pid = appiumProcess.pid ?? null;
|
|
330157
330608
|
serverState.port = port;
|
|
330158
330609
|
appiumProcess.stdout?.on("data", (data) => {
|
|
330159
|
-
const line = data.toString().trim();
|
|
330610
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330160
330611
|
if (line) {
|
|
330161
330612
|
appendLog(`[appium:stdout] ${line}`);
|
|
330162
330613
|
options?.onLog?.(line);
|
|
330163
330614
|
}
|
|
330164
330615
|
});
|
|
330165
330616
|
appiumProcess.stderr?.on("data", (data) => {
|
|
330166
|
-
const line = data.toString().trim();
|
|
330617
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330167
330618
|
if (line) {
|
|
330168
330619
|
appendLog(`[appium:stderr] ${line}`);
|
|
330169
330620
|
options?.onLog?.(line);
|
|
@@ -330322,6 +330773,17 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
330322
330773
|
// src/services/mobile/appium-actions.ts
|
|
330323
330774
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
330324
330775
|
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
330776
|
+
var APPIUM_DRIVER_DEAD_RE = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET|invalid session id|no such session/i;
|
|
330777
|
+
var AppiumDriverDeadError = class extends Error {
|
|
330778
|
+
isAppiumDriverDead = true;
|
|
330779
|
+
constructor(message) {
|
|
330780
|
+
super(message);
|
|
330781
|
+
this.name = "AppiumDriverDeadError";
|
|
330782
|
+
}
|
|
330783
|
+
};
|
|
330784
|
+
function isAppiumDriverDeadError(err) {
|
|
330785
|
+
return err instanceof AppiumDriverDeadError || typeof err === "object" && err !== null && err.isAppiumDriverDead === true;
|
|
330786
|
+
}
|
|
330325
330787
|
async function appiumRequest(path, options) {
|
|
330326
330788
|
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
330327
330789
|
const res = await fetch(url, {
|
|
@@ -330331,6 +330793,9 @@ async function appiumRequest(path, options) {
|
|
|
330331
330793
|
});
|
|
330332
330794
|
if (!res.ok) {
|
|
330333
330795
|
const body = await res.text().catch(() => "");
|
|
330796
|
+
if (APPIUM_DRIVER_DEAD_RE.test(body)) {
|
|
330797
|
+
throw new AppiumDriverDeadError(`Appium driver session is not usable (${res.status}): ${body.slice(0, 200)}`);
|
|
330798
|
+
}
|
|
330334
330799
|
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
330335
330800
|
}
|
|
330336
330801
|
return res.json();
|
|
@@ -330345,7 +330810,8 @@ async function getScreenshot(sessionPath) {
|
|
|
330345
330810
|
try {
|
|
330346
330811
|
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
330347
330812
|
return typeof value === "string" ? value : null;
|
|
330348
|
-
} catch {
|
|
330813
|
+
} catch (err) {
|
|
330814
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330349
330815
|
return null;
|
|
330350
330816
|
}
|
|
330351
330817
|
}
|
|
@@ -330353,7 +330819,8 @@ async function getPageSource(sessionPath) {
|
|
|
330353
330819
|
try {
|
|
330354
330820
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
330355
330821
|
return typeof value === "string" ? value : null;
|
|
330356
|
-
} catch {
|
|
330822
|
+
} catch (err) {
|
|
330823
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330357
330824
|
return null;
|
|
330358
330825
|
}
|
|
330359
330826
|
}
|
|
@@ -330369,7 +330836,8 @@ async function getWindowSize(sessionPath) {
|
|
|
330369
330836
|
return { width: w, height: h };
|
|
330370
330837
|
}
|
|
330371
330838
|
}
|
|
330372
|
-
} catch {
|
|
330839
|
+
} catch (err) {
|
|
330840
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330373
330841
|
}
|
|
330374
330842
|
if (attempt < retries - 1) {
|
|
330375
330843
|
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
@@ -330525,6 +330993,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
|
|
|
330525
330993
|
async function pressBack(sessionPath) {
|
|
330526
330994
|
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
330527
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
|
+
}
|
|
330528
331027
|
async function pressHome(sessionPath, platform3) {
|
|
330529
331028
|
if (platform3 === "IOS") {
|
|
330530
331029
|
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
@@ -331713,6 +332212,7 @@ var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
|
331713
332212
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
331714
332213
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
331715
332214
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
332215
|
+
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
331716
332216
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
331717
332217
|
var execFileAsync2 = promisify2(execFile2);
|
|
331718
332218
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
@@ -331749,6 +332249,9 @@ async function wdRequest(path, options) {
|
|
|
331749
332249
|
if (res.status === 404 && /no such session|invalid session id|session is either terminated|deleted session/i.test(body)) {
|
|
331750
332250
|
throw new WebDriverTransportError(`WebDriver session is no longer available: ${body.slice(0, 200)}`);
|
|
331751
332251
|
}
|
|
332252
|
+
if (res.status === 500 && APPIUM_DRIVER_DEAD_RE2.test(body)) {
|
|
332253
|
+
throw new WebDriverTransportError(`WebDriver driver process is not usable: ${body.slice(0, 200)}`);
|
|
332254
|
+
}
|
|
331752
332255
|
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
331753
332256
|
throw new WebDriverTransportError(`Appium server unavailable (${res.status}): ${body.slice(0, 200)}`);
|
|
331754
332257
|
}
|
|
@@ -334517,6 +335020,10 @@ var MobileBridgeDriver = class {
|
|
|
334517
335020
|
async back() {
|
|
334518
335021
|
return this.runAction(() => pressBack(this.sessionPath));
|
|
334519
335022
|
}
|
|
335023
|
+
/** Platform-aware software keyboard dismissal. */
|
|
335024
|
+
async dismissKeyboard() {
|
|
335025
|
+
return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
|
|
335026
|
+
}
|
|
334520
335027
|
/** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
|
|
334521
335028
|
async home() {
|
|
334522
335029
|
return this.runAction(() => pressHome(this.sessionPath, this.platform));
|
|
@@ -334902,8 +335409,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
334902
335409
|
pageUrl: call.pageUrl ?? pageUrl
|
|
334903
335410
|
}));
|
|
334904
335411
|
}
|
|
335412
|
+
var runnerPackage = require_package4();
|
|
334905
335413
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
334906
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;
|
|
334907
335418
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
334908
335419
|
var DEFAULT_TYPE_LIMITS = {
|
|
334909
335420
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -336706,7 +337217,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
336706
337217
|
if (getBridgeState().hasActiveSession) {
|
|
336707
337218
|
throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
|
|
336708
337219
|
}
|
|
336709
|
-
const
|
|
337220
|
+
const mobileProgressLines = [];
|
|
337221
|
+
const mobileOnLog = (line) => {
|
|
337222
|
+
mobileProgressLines.push(line);
|
|
337223
|
+
if (mobileProgressLines.length > 500) {
|
|
337224
|
+
mobileProgressLines.splice(0, mobileProgressLines.length - 500);
|
|
337225
|
+
}
|
|
337226
|
+
onLog("progress", mobileProgressLines.join("\n"));
|
|
337227
|
+
};
|
|
336710
337228
|
liveBrowserHandle.patch({
|
|
336711
337229
|
mode: ctx.mode === "survey" ? "survey" : "discovery",
|
|
336712
337230
|
testName: ctx.featureName ?? "Mobile discovery",
|
|
@@ -337730,6 +338248,9 @@ async function startRunner(opts) {
|
|
|
337730
338248
|
method: "POST",
|
|
337731
338249
|
headers,
|
|
337732
338250
|
body: JSON.stringify({
|
|
338251
|
+
runnerName: RUNNER_NAME,
|
|
338252
|
+
runnerVersion: RUNNER_VERSION,
|
|
338253
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
337733
338254
|
runsCompleted,
|
|
337734
338255
|
activeRuns,
|
|
337735
338256
|
maxConcurrent: MAX_CONCURRENT,
|