@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.js
CHANGED
|
@@ -2344,40 +2344,40 @@ var init_plans = __esm({
|
|
|
2344
2344
|
type: "extra_projects",
|
|
2345
2345
|
label: "Extra Projects",
|
|
2346
2346
|
description: "Add more projects to your workspace",
|
|
2347
|
-
priceLabel: "$
|
|
2348
|
-
monthlyPriceUsd:
|
|
2347
|
+
priceLabel: "$24.99 / month",
|
|
2348
|
+
monthlyPriceUsd: 24.99,
|
|
2349
2349
|
limitIncrement: 1
|
|
2350
2350
|
},
|
|
2351
2351
|
extra_tests: {
|
|
2352
2352
|
type: "extra_tests",
|
|
2353
2353
|
label: "Extra Tests",
|
|
2354
2354
|
description: "Increase tests per project limit",
|
|
2355
|
-
priceLabel: "$
|
|
2356
|
-
monthlyPriceUsd:
|
|
2357
|
-
limitIncrement:
|
|
2355
|
+
priceLabel: "$12.99 / month",
|
|
2356
|
+
monthlyPriceUsd: 12.99,
|
|
2357
|
+
limitIncrement: 50
|
|
2358
2358
|
},
|
|
2359
2359
|
extra_suite_runs: {
|
|
2360
2360
|
type: "extra_suite_runs",
|
|
2361
2361
|
label: "Extra Suite Runs",
|
|
2362
2362
|
description: "Increase daily suite run limit",
|
|
2363
|
-
priceLabel: "$
|
|
2364
|
-
monthlyPriceUsd:
|
|
2363
|
+
priceLabel: "$12.99 / month",
|
|
2364
|
+
monthlyPriceUsd: 12.99,
|
|
2365
2365
|
limitIncrement: 5
|
|
2366
2366
|
},
|
|
2367
2367
|
extra_discovery_runs: {
|
|
2368
2368
|
type: "extra_discovery_runs",
|
|
2369
2369
|
label: "Extra Discovery Runs",
|
|
2370
2370
|
description: "Increase discovery run limit",
|
|
2371
|
-
priceLabel: "$
|
|
2372
|
-
monthlyPriceUsd:
|
|
2371
|
+
priceLabel: "$19.99 / month",
|
|
2372
|
+
monthlyPriceUsd: 19.99,
|
|
2373
2373
|
limitIncrement: 5
|
|
2374
2374
|
},
|
|
2375
2375
|
extra_test_executions: {
|
|
2376
2376
|
type: "extra_test_executions",
|
|
2377
2377
|
label: "Extra Test Executions",
|
|
2378
2378
|
description: "+100 manual test runs per day, on top of your plan\u2019s daily dashboard run budget",
|
|
2379
|
-
priceLabel: "$
|
|
2380
|
-
monthlyPriceUsd:
|
|
2379
|
+
priceLabel: "$14.99 / month",
|
|
2380
|
+
monthlyPriceUsd: 14.99,
|
|
2381
2381
|
limitIncrement: 100
|
|
2382
2382
|
}
|
|
2383
2383
|
};
|
|
@@ -2402,16 +2402,16 @@ var init_plans = __esm({
|
|
|
2402
2402
|
PLAN_DEFINITIONS = {
|
|
2403
2403
|
standard: {
|
|
2404
2404
|
code: "standard",
|
|
2405
|
-
label: "Standard
|
|
2405
|
+
label: "Standard",
|
|
2406
2406
|
priceLabel: "Free",
|
|
2407
2407
|
monthlyPriceUsd: 0,
|
|
2408
2408
|
billingModel: "free",
|
|
2409
|
-
summary: "For curious developers. Get the full platform - MCP, Discovery, Voice, self-heal - with
|
|
2409
|
+
summary: "For curious developers. Get the full platform - MCP, Discovery, Voice, self-heal - with a starter budget for one real project.",
|
|
2410
2410
|
maxProjects: 1,
|
|
2411
|
-
maxTestsPerProject:
|
|
2411
|
+
maxTestsPerProject: 20,
|
|
2412
2412
|
maxSuiteRunsPerDay: 1,
|
|
2413
|
-
maxTestExecutionsPerDay:
|
|
2414
|
-
//
|
|
2413
|
+
maxTestExecutionsPerDay: 10,
|
|
2414
|
+
// 5 regression / 5 manual at the default 50/50 split
|
|
2415
2415
|
maxDiscoveryRunsPerDay: 1,
|
|
2416
2416
|
// Per day
|
|
2417
2417
|
maxConcurrentDiscoveries: 1,
|
|
@@ -2493,14 +2493,14 @@ var init_plans = __esm({
|
|
|
2493
2493
|
};
|
|
2494
2494
|
TRIAL_DURATION_DAYS = 30;
|
|
2495
2495
|
CREDIT_PRICES_CENTS = {
|
|
2496
|
-
discovery_run:
|
|
2496
|
+
discovery_run: 200,
|
|
2497
|
+
// $2.00
|
|
2498
|
+
suite_run: 50,
|
|
2497
2499
|
// $0.50
|
|
2498
|
-
|
|
2499
|
-
// $0.
|
|
2500
|
-
|
|
2501
|
-
// $0.
|
|
2502
|
-
heal: 10
|
|
2503
|
-
// $0.10
|
|
2500
|
+
test_run: 15,
|
|
2501
|
+
// $0.15
|
|
2502
|
+
heal: 75
|
|
2503
|
+
// $0.75
|
|
2504
2504
|
};
|
|
2505
2505
|
AI_CREDIT_MARGIN = 1.5;
|
|
2506
2506
|
CREDIT_PACKS_CENTS = [2e3, 5e3, 1e4];
|
|
@@ -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) {
|
|
@@ -64542,6 +64616,78 @@ var require_discover_ai_planner = __commonJS({
|
|
|
64542
64616
|
function meaningfulTokens(name) {
|
|
64543
64617
|
return name.toLowerCase().split(/[\s\--_]+/).filter((t) => t.length > 2 && !JOURNEY_MATCH_STOPWORDS.has(t));
|
|
64544
64618
|
}
|
|
64619
|
+
function textIncludesCredentialField(text, field) {
|
|
64620
|
+
return field === "email" ? /\b(e-?mail|username|user\s*name|login\s*id)\b/i.test(text) : /\bpassword|passcode|pin\b/i.test(text);
|
|
64621
|
+
}
|
|
64622
|
+
function textIncludesCredentialSubmit(text) {
|
|
64623
|
+
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);
|
|
64624
|
+
}
|
|
64625
|
+
function targetPagesHaveCredentialControls(siteMap, targetPages) {
|
|
64626
|
+
const targetSet = new Set(targetPages.map(normalizePlannerRoute));
|
|
64627
|
+
let hasEmail = false;
|
|
64628
|
+
let hasPassword = false;
|
|
64629
|
+
let hasSubmit = false;
|
|
64630
|
+
for (const page of siteMap.pages) {
|
|
64631
|
+
if (!targetSet.has(normalizePlannerRoute(page.route)))
|
|
64632
|
+
continue;
|
|
64633
|
+
for (const element of page.elements ?? []) {
|
|
64634
|
+
const label = [
|
|
64635
|
+
element.label,
|
|
64636
|
+
element.role,
|
|
64637
|
+
element.testId,
|
|
64638
|
+
element.name
|
|
64639
|
+
].filter(Boolean).join(" ");
|
|
64640
|
+
if (!label)
|
|
64641
|
+
continue;
|
|
64642
|
+
if (textIncludesCredentialField(label, "email"))
|
|
64643
|
+
hasEmail = true;
|
|
64644
|
+
if (textIncludesCredentialField(label, "password"))
|
|
64645
|
+
hasPassword = true;
|
|
64646
|
+
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)) {
|
|
64647
|
+
hasSubmit = true;
|
|
64648
|
+
}
|
|
64649
|
+
}
|
|
64650
|
+
}
|
|
64651
|
+
return hasEmail && hasPassword && hasSubmit;
|
|
64652
|
+
}
|
|
64653
|
+
function journeyCoversCredentialSubmit(journey) {
|
|
64654
|
+
if (!journey)
|
|
64655
|
+
return false;
|
|
64656
|
+
const text = [
|
|
64657
|
+
journey.name,
|
|
64658
|
+
journey.expectedOutcome,
|
|
64659
|
+
...journey.steps ?? [],
|
|
64660
|
+
...journey.provenCommands ?? []
|
|
64661
|
+
].join(" ");
|
|
64662
|
+
const hasCredentialPair = /\bcredentials\b/i.test(text) || textIncludesCredentialField(text, "email") && textIncludesCredentialField(text, "password");
|
|
64663
|
+
return hasCredentialPair && textIncludesCredentialSubmit(text);
|
|
64664
|
+
}
|
|
64665
|
+
function scenarioAuthCredentialPolicy(scenario, steps) {
|
|
64666
|
+
const text = [
|
|
64667
|
+
scenario.name,
|
|
64668
|
+
scenario.goal,
|
|
64669
|
+
scenario.type,
|
|
64670
|
+
scenario.variant,
|
|
64671
|
+
...steps.map((step) => step.action)
|
|
64672
|
+
].filter(Boolean).join(" ");
|
|
64673
|
+
if (!/\b(auth|authentication|sign[ -]?in|signin|log[ -]?in|login|session)\b/i.test(text)) {
|
|
64674
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64675
|
+
}
|
|
64676
|
+
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)) {
|
|
64677
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64678
|
+
}
|
|
64679
|
+
if (!/\b(credential|e-?mail|username|user\s*name|password|valid|invalid|empty|required|wrong|case|special|leading|trailing)\b/i.test(text)) {
|
|
64680
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64681
|
+
}
|
|
64682
|
+
const uiOnly = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/i.test(text);
|
|
64683
|
+
const negative = /\b(invalid|empty|required|missing|wrong|error|validation|case|special|leading|trailing)\b/i.test(text);
|
|
64684
|
+
const happy = /\b(happy|valid\s+credentials|success|successful)\b/i.test(text) || scenario.type === "HAPPY_PATH" || scenario.variant === "happy";
|
|
64685
|
+
return {
|
|
64686
|
+
needsCredentialEvidence: true,
|
|
64687
|
+
needsSubmitStep: !uiOnly && (happy || negative),
|
|
64688
|
+
reason: uiOnly ? "credential UI scenario" : negative ? "credential validation scenario" : "credential sign-in scenario"
|
|
64689
|
+
};
|
|
64690
|
+
}
|
|
64545
64691
|
function truncateOutcome(text, limit) {
|
|
64546
64692
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
64547
64693
|
if (normalized.length <= limit)
|
|
@@ -65307,6 +65453,18 @@ Each entry is exactly one of:
|
|
|
65307
65453
|
if (matchSource === "route") {
|
|
65308
65454
|
warnings.push(`"${sc.name}" matched to journey "${matchingSuggestion.name}" by route overlap only - evidence may be weak`);
|
|
65309
65455
|
}
|
|
65456
|
+
const authCredentialPolicy = scenarioAuthCredentialPolicy(sc, steps);
|
|
65457
|
+
if (authCredentialPolicy.needsCredentialEvidence) {
|
|
65458
|
+
const hasCredentialEvidence = targetPagesHaveCredentialControls(siteMap, sc.targetPages) || journeyCoversCredentialSubmit(matchingSuggestion);
|
|
65459
|
+
if (!hasCredentialEvidence) {
|
|
65460
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} lacks observed email/password/sign-in controls or a recorded credential journey`);
|
|
65461
|
+
continue;
|
|
65462
|
+
}
|
|
65463
|
+
if (authCredentialPolicy.needsSubmitStep && !steps.some((step) => textIncludesCredentialSubmit(step.action)) && !journeyCoversCredentialSubmit(matchingSuggestion)) {
|
|
65464
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} does not include a concrete sign-in submit step`);
|
|
65465
|
+
continue;
|
|
65466
|
+
}
|
|
65467
|
+
}
|
|
65310
65468
|
const evidence = (0, discover_planner_js_1.extractScenarioEvidence)(
|
|
65311
65469
|
siteMap.pages,
|
|
65312
65470
|
sc.targetPages,
|
|
@@ -65852,6 +66010,25 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
65852
66010
|
function targetAppScreens(graph, appId) {
|
|
65853
66011
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
65854
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
|
+
}
|
|
65855
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.
|
|
65856
66033
|
|
|
65857
66034
|
## Output Discipline
|
|
@@ -65871,8 +66048,8 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65871
66048
|
5. CRUD screens for the same entity belong together
|
|
65872
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).
|
|
65873
66050
|
7. Feature descriptions MUST reference actual element labels / accessibility-ids from the evidence
|
|
65874
|
-
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.
|
|
65875
|
-
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.
|
|
65876
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.
|
|
65877
66054
|
|
|
65878
66055
|
## Output JSON Schema
|
|
@@ -65972,6 +66149,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65972
66149
|
## Coverage Strategy
|
|
65973
66150
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 12) + 1 EDGE_CASE (empty/invalid submission)
|
|
65974
66151
|
- 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)
|
|
66152
|
+
- 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.
|
|
65975
66153
|
- 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.
|
|
65976
66154
|
- 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"
|
|
65977
66155
|
- For navigation: multi-screen user journeys within the feature scope (do NOT drift into unrelated features)
|
|
@@ -66138,7 +66316,7 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66138
66316
|
sections.push(`## Feature: ${context.featureName}
|
|
66139
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.`);
|
|
66140
66318
|
}
|
|
66141
|
-
const orderedScreens =
|
|
66319
|
+
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66142
66320
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66143
66321
|
sections.push(`## Screens Discovered (${orderedScreens.length})`);
|
|
66144
66322
|
for (const state2 of orderedScreens) {
|
|
@@ -66247,7 +66425,7 @@ ${constraints.join("\n")}`);
|
|
|
66247
66425
|
const now = /* @__PURE__ */ new Date();
|
|
66248
66426
|
const idByScreen = /* @__PURE__ */ new Map();
|
|
66249
66427
|
let pageSeq = 0;
|
|
66250
|
-
const orderedScreens =
|
|
66428
|
+
const orderedScreens = plannableTargetAppScreens(graph, meta.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66251
66429
|
for (const state2 of orderedScreens) {
|
|
66252
66430
|
idByScreen.set(state2.screenId, `mpage-${pageSeq++}`);
|
|
66253
66431
|
}
|
|
@@ -66308,7 +66486,7 @@ ${constraints.join("\n")}`);
|
|
|
66308
66486
|
}
|
|
66309
66487
|
function buildMobileFeatureGroupsFromGraph(graph, appId) {
|
|
66310
66488
|
const groups = /* @__PURE__ */ new Map();
|
|
66311
|
-
const orderedScreens =
|
|
66489
|
+
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66312
66490
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
66313
66491
|
for (const state2 of orderedScreens) {
|
|
66314
66492
|
const screenTypeLower = state2.screenType?.toLowerCase();
|
|
@@ -66355,7 +66533,7 @@ ${constraints.join("\n")}`);
|
|
|
66355
66533
|
return [];
|
|
66356
66534
|
if (input.featureName?.trim())
|
|
66357
66535
|
return [];
|
|
66358
|
-
const orderedScreens =
|
|
66536
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66359
66537
|
if (orderedScreens.length === 0)
|
|
66360
66538
|
return [];
|
|
66361
66539
|
const entryScreenId = orderedScreens[0].screenId;
|
|
@@ -66557,9 +66735,11 @@ var require_mobile_generator = __commonJS({
|
|
|
66557
66735
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
66558
66736
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
66559
66737
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
66738
|
+
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
66560
66739
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
66561
66740
|
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
66562
66741
|
var MAX_RECENT_EXCHANGES = 24;
|
|
66742
|
+
var EXTERNAL_APP_RECOVERY_ATTEMPTS = 2;
|
|
66563
66743
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
66564
66744
|
function finalizeAction(input) {
|
|
66565
66745
|
return { ...input, url: "", selector: input.selector ?? "" };
|
|
@@ -66975,10 +67155,47 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
66975
67155
|
externalOwner: boundary.owner
|
|
66976
67156
|
};
|
|
66977
67157
|
}
|
|
66978
|
-
|
|
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.`);
|
|
66979
67196
|
return {
|
|
66980
|
-
resolved:
|
|
66981
|
-
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)`,
|
|
66982
67199
|
externalBlocked: true,
|
|
66983
67200
|
externalOwner: boundary.owner
|
|
66984
67201
|
};
|
|
@@ -67192,7 +67409,19 @@ ${statePacket}` },
|
|
|
67192
67409
|
return unknownRef(toolArgs.ref);
|
|
67193
67410
|
const beforeScreenName = screenName();
|
|
67194
67411
|
const before = latest();
|
|
67195
|
-
|
|
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
|
+
}
|
|
67196
67425
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67197
67426
|
if (shouldRecordAction(result, absorbed) && changedScreen(before, absorbed)) {
|
|
67198
67427
|
recordAction({
|
|
@@ -67448,7 +67677,7 @@ ${statePacket}` },
|
|
|
67448
67677
|
"react-root",
|
|
67449
67678
|
"application"
|
|
67450
67679
|
]);
|
|
67451
|
-
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;
|
|
67680
|
+
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;
|
|
67452
67681
|
function locatorLocalName(value) {
|
|
67453
67682
|
const trimmed = value.trim();
|
|
67454
67683
|
const withoutStrategy = trimmed.startsWith("id=") ? trimmed.slice(3) : trimmed.startsWith("~") ? trimmed.slice(1) : trimmed;
|
|
@@ -67528,6 +67757,88 @@ ${statePacket}` },
|
|
|
67528
67757
|
}));
|
|
67529
67758
|
return true;
|
|
67530
67759
|
}
|
|
67760
|
+
function scenarioText(scenario) {
|
|
67761
|
+
return [
|
|
67762
|
+
scenario.name,
|
|
67763
|
+
scenario.goal,
|
|
67764
|
+
...scenario.steps.flatMap((step) => [step.action, step.target, step.hint]),
|
|
67765
|
+
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67766
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67767
|
+
}
|
|
67768
|
+
function actionText(action) {
|
|
67769
|
+
const bundle = action.metadata.locatorBundle;
|
|
67770
|
+
return [
|
|
67771
|
+
action.type,
|
|
67772
|
+
action.selector,
|
|
67773
|
+
action.value,
|
|
67774
|
+
action.innerText,
|
|
67775
|
+
action.metadata.targetLabel,
|
|
67776
|
+
bundle?.text,
|
|
67777
|
+
bundle?.contentDesc,
|
|
67778
|
+
bundle?.accessibilityId,
|
|
67779
|
+
bundle?.resourceId,
|
|
67780
|
+
bundle?.className,
|
|
67781
|
+
bundle?.xpath
|
|
67782
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67783
|
+
}
|
|
67784
|
+
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67785
|
+
const text = scenarioText(scenario);
|
|
67786
|
+
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);
|
|
67787
|
+
const typedEmailIndexes = [];
|
|
67788
|
+
const typedPasswordIndexes = [];
|
|
67789
|
+
actions.forEach((action, index) => {
|
|
67790
|
+
if (action.type !== "TYPE")
|
|
67791
|
+
return;
|
|
67792
|
+
const textForAction = actionText(action);
|
|
67793
|
+
if (/\b(email|e-mail|username|user name|login)\b|\{\{test_user_email\}\}/i.test(textForAction)) {
|
|
67794
|
+
typedEmailIndexes.push(index);
|
|
67795
|
+
}
|
|
67796
|
+
if (/\b(password|passcode|pin)\b|\{\{test_user_password\}\}/i.test(textForAction)) {
|
|
67797
|
+
typedPasswordIndexes.push(index);
|
|
67798
|
+
}
|
|
67799
|
+
});
|
|
67800
|
+
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67801
|
+
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67802
|
+
const submitAfterCredentials = actions.some((action, index) => {
|
|
67803
|
+
if (action.type !== "TAP" || index <= lastCredentialIndex)
|
|
67804
|
+
return false;
|
|
67805
|
+
const textForAction = actionText(action);
|
|
67806
|
+
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
67807
|
+
return false;
|
|
67808
|
+
return !/\b(with\s+(email|e-mail|google|facebook|apple)|email\s+form|showemailformbtn)\b/i.test(textForAction);
|
|
67809
|
+
});
|
|
67810
|
+
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67811
|
+
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67812
|
+
return "credential fields were filled but no sign-in submit tap was recorded after them";
|
|
67813
|
+
}
|
|
67814
|
+
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67815
|
+
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67816
|
+
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67817
|
+
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67818
|
+
if (action.type !== "TAP")
|
|
67819
|
+
return false;
|
|
67820
|
+
const textForAction = actionText(action);
|
|
67821
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67822
|
+
return false;
|
|
67823
|
+
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67824
|
+
});
|
|
67825
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67826
|
+
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67827
|
+
}
|
|
67828
|
+
if (!isCredentialSignIn)
|
|
67829
|
+
return null;
|
|
67830
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
67831
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
67832
|
+
const tappedSubmit = submitAfterCredentials;
|
|
67833
|
+
if (typedEmail && typedPassword && tappedSubmit)
|
|
67834
|
+
return null;
|
|
67835
|
+
const missing = [
|
|
67836
|
+
typedEmail ? null : "email/username entry",
|
|
67837
|
+
typedPassword ? null : "password entry",
|
|
67838
|
+
tappedSubmit ? null : "sign-in submit tap"
|
|
67839
|
+
].filter((value) => value !== null);
|
|
67840
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67841
|
+
}
|
|
67531
67842
|
function actionCycleKey(action) {
|
|
67532
67843
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67533
67844
|
}
|
|
@@ -67581,6 +67892,11 @@ ${statePacket}` },
|
|
|
67581
67892
|
if (actions.length < beforeContainerDrop) {
|
|
67582
67893
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
67583
67894
|
}
|
|
67895
|
+
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
67896
|
+
if (intentMismatch) {
|
|
67897
|
+
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
67898
|
+
return null;
|
|
67899
|
+
}
|
|
67584
67900
|
const floorAppended = appendHybridFloor(actions, recording, platform3);
|
|
67585
67901
|
if (floorAppended) {
|
|
67586
67902
|
log2(" appended hybrid durable-outcome floor (landing-screen ASSERT_VISIBLE).");
|
|
@@ -68278,7 +68594,8 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
68278
68594
|
2. STEP RESULTS: which steps passed before the failure.
|
|
68279
68595
|
3. TREE: does the current screen match what the failed step expects?
|
|
68280
68596
|
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).
|
|
68281
|
-
4.
|
|
68597
|
+
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.
|
|
68598
|
+
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.
|
|
68282
68599
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68283
68600
|
{"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}
|
|
68284
68601
|
{"verdict":"app_bug","reason":"app crashed/blank/5xx (NOT rate-limit)"}
|
|
@@ -68335,6 +68652,104 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68335
68652
|
function isDrift(error2) {
|
|
68336
68653
|
return /unexpected screen|screen drift/i.test(error2 ?? "");
|
|
68337
68654
|
}
|
|
68655
|
+
function mobileStepSearchText(step) {
|
|
68656
|
+
const rawTarget = step.target;
|
|
68657
|
+
const structuredTarget = rawTarget && typeof rawTarget === "object" ? rawTarget : void 0;
|
|
68658
|
+
const bundle = structuredTarget?.locatorBundle;
|
|
68659
|
+
const stepValue = step.value;
|
|
68660
|
+
return [
|
|
68661
|
+
step.action,
|
|
68662
|
+
step.description,
|
|
68663
|
+
typeof stepValue === "string" ? stepValue : void 0,
|
|
68664
|
+
typeof rawTarget === "string" ? rawTarget : void 0,
|
|
68665
|
+
structuredTarget?.primary,
|
|
68666
|
+
bundle?.text,
|
|
68667
|
+
bundle?.contentDesc,
|
|
68668
|
+
bundle?.accessibilityId,
|
|
68669
|
+
bundle?.resourceId,
|
|
68670
|
+
bundle?.className,
|
|
68671
|
+
bundle?.xpath
|
|
68672
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
68673
|
+
}
|
|
68674
|
+
function detectInvalidExternalAuthSetup(steps, error2) {
|
|
68675
|
+
if (!/foreground app changed|outside target app|com\.google\.android\.gms|external app/i.test(error2 ?? "")) {
|
|
68676
|
+
return null;
|
|
68677
|
+
}
|
|
68678
|
+
const hasBareLoginTap = steps.some((step) => {
|
|
68679
|
+
if (!/\btap\b/i.test(step.action))
|
|
68680
|
+
return false;
|
|
68681
|
+
const text = mobileStepSearchText(step);
|
|
68682
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(text))
|
|
68683
|
+
return false;
|
|
68684
|
+
return /\b(sign\s*in|signin|log\s*in|login)\b/.test(text);
|
|
68685
|
+
});
|
|
68686
|
+
if (!hasBareLoginTap)
|
|
68687
|
+
return null;
|
|
68688
|
+
const hasCredentialEntry = steps.some((step) => {
|
|
68689
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
68690
|
+
return false;
|
|
68691
|
+
const text = mobileStepSearchText(step);
|
|
68692
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68693
|
+
});
|
|
68694
|
+
if (hasCredentialEntry)
|
|
68695
|
+
return null;
|
|
68696
|
+
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";
|
|
68697
|
+
}
|
|
68698
|
+
function isCredentialFieldStep(step) {
|
|
68699
|
+
const text = mobileStepSearchText(step);
|
|
68700
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68701
|
+
}
|
|
68702
|
+
function isCredentialSubmitTap(step) {
|
|
68703
|
+
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68704
|
+
return false;
|
|
68705
|
+
const text = mobileStepSearchText(step);
|
|
68706
|
+
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)) {
|
|
68707
|
+
return false;
|
|
68708
|
+
}
|
|
68709
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit)\b/.test(text);
|
|
68710
|
+
}
|
|
68711
|
+
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68712
|
+
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
68713
|
+
return null;
|
|
68714
|
+
}
|
|
68715
|
+
const allText = steps.map(mobileStepSearchText).join(" ");
|
|
68716
|
+
if (!/\b(email|e-mail|username|user\s*name|password|passcode|pin)\b/.test(allText))
|
|
68717
|
+
return null;
|
|
68718
|
+
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);
|
|
68719
|
+
let lastCredentialEditOrder = -1;
|
|
68720
|
+
for (const step of steps) {
|
|
68721
|
+
if (!/\b(type|fill|input|enter|clear)\b/i.test(step.action))
|
|
68722
|
+
continue;
|
|
68723
|
+
if (!isCredentialFieldStep(step))
|
|
68724
|
+
continue;
|
|
68725
|
+
lastCredentialEditOrder = Math.max(lastCredentialEditOrder, step.order ?? -1);
|
|
68726
|
+
}
|
|
68727
|
+
if (lastCredentialEditOrder < 0)
|
|
68728
|
+
return null;
|
|
68729
|
+
if (uiOnlyCredentialScenario) {
|
|
68730
|
+
const hasCredentialFormEvidence = steps.some((step) => {
|
|
68731
|
+
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68732
|
+
return false;
|
|
68733
|
+
const text = mobileStepSearchText(step);
|
|
68734
|
+
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);
|
|
68735
|
+
});
|
|
68736
|
+
if (hasCredentialFormEvidence)
|
|
68737
|
+
return null;
|
|
68738
|
+
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";
|
|
68739
|
+
}
|
|
68740
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isCredentialSubmitTap(step));
|
|
68741
|
+
if (hasSubmitAfterCredentialEdit)
|
|
68742
|
+
return null;
|
|
68743
|
+
const hasValidationAssertion = steps.some((step) => {
|
|
68744
|
+
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68745
|
+
return false;
|
|
68746
|
+
const text = mobileStepSearchText(step);
|
|
68747
|
+
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);
|
|
68748
|
+
});
|
|
68749
|
+
if (hasValidationAssertion)
|
|
68750
|
+
return null;
|
|
68751
|
+
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";
|
|
68752
|
+
}
|
|
68338
68753
|
function stripCodeFences(text) {
|
|
68339
68754
|
const trimmed = text.trim();
|
|
68340
68755
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -68593,6 +69008,24 @@ ${args.directive}`);
|
|
|
68593
69008
|
const registryPresent = options.selectorRegistryContext != null;
|
|
68594
69009
|
const platform3 = target.platform;
|
|
68595
69010
|
const trimmedLogs = tailLimit(ctx.logs ?? "", MAX_LOG_CHARS);
|
|
69011
|
+
const invalidExternalAuthSetup = detectInvalidExternalAuthSetup(steps, ctx.error);
|
|
69012
|
+
if (invalidExternalAuthSetup) {
|
|
69013
|
+
return {
|
|
69014
|
+
passed: false,
|
|
69015
|
+
verdict: "test_invalid",
|
|
69016
|
+
healReason: invalidExternalAuthSetup,
|
|
69017
|
+
attemptsUsed: 0
|
|
69018
|
+
};
|
|
69019
|
+
}
|
|
69020
|
+
const invalidAuthValidationSetup = detectInvalidAuthValidationSetup(steps, ctx.error);
|
|
69021
|
+
if (invalidAuthValidationSetup) {
|
|
69022
|
+
return {
|
|
69023
|
+
passed: false,
|
|
69024
|
+
verdict: "test_invalid",
|
|
69025
|
+
healReason: invalidAuthValidationSetup,
|
|
69026
|
+
attemptsUsed: 0
|
|
69027
|
+
};
|
|
69028
|
+
}
|
|
68596
69029
|
let attemptCap = base2;
|
|
68597
69030
|
let recoverableUsed = 0;
|
|
68598
69031
|
let bonusUsed = 0;
|
|
@@ -68778,7 +69211,7 @@ ${args.directive}`);
|
|
|
68778
69211
|
removeTags: script.removeTags
|
|
68779
69212
|
};
|
|
68780
69213
|
}
|
|
68781
|
-
var DEFAULT_PHASE2_MAX_PASSES =
|
|
69214
|
+
var DEFAULT_PHASE2_MAX_PASSES = 4;
|
|
68782
69215
|
exports2.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.
|
|
68783
69216
|
## Key insight \u2014 ENTRY-SCREEN DRIFT
|
|
68784
69217
|
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.
|
|
@@ -68787,7 +69220,8 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
68787
69220
|
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().
|
|
68788
69221
|
3. If a locator is simply wrong, swap it for a stronger identity locator present in the LIVE tree.
|
|
68789
69222
|
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.
|
|
68790
|
-
4. If
|
|
69223
|
+
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.
|
|
69224
|
+
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.
|
|
68791
69225
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68792
69226
|
{"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}
|
|
68793
69227
|
{"verdict":"observe","action":{"type":"tap","target":"mobile.text('<exact label visible in the live tree>')"}}
|
|
@@ -315122,8 +315556,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315122
315556
|
};
|
|
315123
315557
|
}
|
|
315124
315558
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
315125
|
-
const exploreAppDataCleared = await prepareMobileAppStateForDiscovery(driver, target, "explore", log2);
|
|
315126
|
-
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.");
|
|
315127
315561
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
315128
315562
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
315129
315563
|
const existingScreens = isSurvey && ctx.incremental ? compactExistingScreens(ctx.existingSiteMap) : void 0;
|
|
@@ -315365,10 +315799,11 @@ Fatal error: ${msg}`);
|
|
|
315365
315799
|
};
|
|
315366
315800
|
}
|
|
315367
315801
|
}
|
|
315368
|
-
async function prepareMobileAppStateForDiscovery(driver, target, phase, log2) {
|
|
315802
|
+
async function prepareMobileAppStateForDiscovery(driver, target, phase, mode, log2) {
|
|
315369
315803
|
void phase;
|
|
315370
315804
|
const resetPolicy = target?.resetPolicy ?? "AUTO";
|
|
315371
|
-
|
|
315805
|
+
const shouldPreserveAppData = resetPolicy === "PRESERVE_APP_DATA" || mode === "survey" && resetPolicy === "AUTO";
|
|
315806
|
+
if (shouldPreserveAppData) {
|
|
315372
315807
|
try {
|
|
315373
315808
|
await driver.relaunch();
|
|
315374
315809
|
} catch (err) {
|
|
@@ -329889,7 +330324,7 @@ var require_package4 = __commonJS({
|
|
|
329889
330324
|
"package.json"(exports2, module2) {
|
|
329890
330325
|
module2.exports = {
|
|
329891
330326
|
name: "@validate.qa/runner",
|
|
329892
|
-
version: "1.0.
|
|
330327
|
+
version: "1.0.32",
|
|
329893
330328
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
329894
330329
|
bin: {
|
|
329895
330330
|
"validate-runner": "dist/cli.js"
|
|
@@ -330038,6 +330473,8 @@ var MAX_RETRIES = 3;
|
|
|
330038
330473
|
var STARTUP_TIMEOUT_MS = 3e4;
|
|
330039
330474
|
var HEALTH_CHECK_INTERVAL_MS = 5e3;
|
|
330040
330475
|
var STABLE_AFTER_MS = 6e4;
|
|
330476
|
+
var DEFAULT_APPIUM_LOG_LEVEL = "warn";
|
|
330477
|
+
var MAX_APPIUM_LOG_LINE_CHARS = 4e3;
|
|
330041
330478
|
var appiumProcess = null;
|
|
330042
330479
|
var serverState = {
|
|
330043
330480
|
running: false,
|
|
@@ -330097,6 +330534,23 @@ function appendLog(line) {
|
|
|
330097
330534
|
logs = logs.slice(-MAX_LOG_LINES);
|
|
330098
330535
|
}
|
|
330099
330536
|
}
|
|
330537
|
+
function resolveAppiumLogLevel() {
|
|
330538
|
+
const raw = process.env.APPIUM_LOG_LEVEL?.trim();
|
|
330539
|
+
if (raw && /^[a-z]+(?::[a-z]+)?$/i.test(raw)) return raw;
|
|
330540
|
+
return DEFAULT_APPIUM_LOG_LEVEL;
|
|
330541
|
+
}
|
|
330542
|
+
function buildAppiumArgs(port, drivers) {
|
|
330543
|
+
const args = ["--port", String(port), "--log-level", resolveAppiumLogLevel()];
|
|
330544
|
+
if (drivers && drivers.length > 0) {
|
|
330545
|
+
args.push("--use-drivers", drivers.join(","));
|
|
330546
|
+
}
|
|
330547
|
+
return args;
|
|
330548
|
+
}
|
|
330549
|
+
function sanitizeAppiumLogLine(line) {
|
|
330550
|
+
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]"');
|
|
330551
|
+
if (redacted.length <= MAX_APPIUM_LOG_LINE_CHARS) return redacted;
|
|
330552
|
+
return `${redacted.slice(0, MAX_APPIUM_LOG_LINE_CHARS)}\u2026 [truncated ${redacted.length - MAX_APPIUM_LOG_LINE_CHARS} chars]`;
|
|
330553
|
+
}
|
|
330100
330554
|
async function checkAppiumHealth(port = serverState.port) {
|
|
330101
330555
|
try {
|
|
330102
330556
|
const controller = new AbortController();
|
|
@@ -330129,10 +330583,7 @@ async function startAppiumServer(options) {
|
|
|
330129
330583
|
return serverState;
|
|
330130
330584
|
}
|
|
330131
330585
|
const port = options?.port ?? APPIUM_PORT;
|
|
330132
|
-
const args =
|
|
330133
|
-
if (options?.drivers && options.drivers.length > 0) {
|
|
330134
|
-
args.push("--use-drivers", options.drivers.join(","));
|
|
330135
|
-
}
|
|
330586
|
+
const args = buildAppiumArgs(port, options?.drivers);
|
|
330136
330587
|
intentionalStop = false;
|
|
330137
330588
|
lastStartOptions = { ...lastStartOptions, ...options };
|
|
330138
330589
|
if (appiumGaveUp) {
|
|
@@ -330151,14 +330602,14 @@ async function startAppiumServer(options) {
|
|
|
330151
330602
|
serverState.pid = appiumProcess.pid ?? null;
|
|
330152
330603
|
serverState.port = port;
|
|
330153
330604
|
appiumProcess.stdout?.on("data", (data) => {
|
|
330154
|
-
const line = data.toString().trim();
|
|
330605
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330155
330606
|
if (line) {
|
|
330156
330607
|
appendLog(`[appium:stdout] ${line}`);
|
|
330157
330608
|
options?.onLog?.(line);
|
|
330158
330609
|
}
|
|
330159
330610
|
});
|
|
330160
330611
|
appiumProcess.stderr?.on("data", (data) => {
|
|
330161
|
-
const line = data.toString().trim();
|
|
330612
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330162
330613
|
if (line) {
|
|
330163
330614
|
appendLog(`[appium:stderr] ${line}`);
|
|
330164
330615
|
options?.onLog?.(line);
|
|
@@ -330317,6 +330768,17 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
330317
330768
|
// src/services/mobile/appium-actions.ts
|
|
330318
330769
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
330319
330770
|
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
330771
|
+
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;
|
|
330772
|
+
var AppiumDriverDeadError = class extends Error {
|
|
330773
|
+
isAppiumDriverDead = true;
|
|
330774
|
+
constructor(message) {
|
|
330775
|
+
super(message);
|
|
330776
|
+
this.name = "AppiumDriverDeadError";
|
|
330777
|
+
}
|
|
330778
|
+
};
|
|
330779
|
+
function isAppiumDriverDeadError(err) {
|
|
330780
|
+
return err instanceof AppiumDriverDeadError || typeof err === "object" && err !== null && err.isAppiumDriverDead === true;
|
|
330781
|
+
}
|
|
330320
330782
|
async function appiumRequest(path, options) {
|
|
330321
330783
|
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
330322
330784
|
const res = await fetch(url, {
|
|
@@ -330326,6 +330788,9 @@ async function appiumRequest(path, options) {
|
|
|
330326
330788
|
});
|
|
330327
330789
|
if (!res.ok) {
|
|
330328
330790
|
const body = await res.text().catch(() => "");
|
|
330791
|
+
if (APPIUM_DRIVER_DEAD_RE.test(body)) {
|
|
330792
|
+
throw new AppiumDriverDeadError(`Appium driver session is not usable (${res.status}): ${body.slice(0, 200)}`);
|
|
330793
|
+
}
|
|
330329
330794
|
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
330330
330795
|
}
|
|
330331
330796
|
return res.json();
|
|
@@ -330340,7 +330805,8 @@ async function getScreenshot(sessionPath) {
|
|
|
330340
330805
|
try {
|
|
330341
330806
|
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
330342
330807
|
return typeof value === "string" ? value : null;
|
|
330343
|
-
} catch {
|
|
330808
|
+
} catch (err) {
|
|
330809
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330344
330810
|
return null;
|
|
330345
330811
|
}
|
|
330346
330812
|
}
|
|
@@ -330348,7 +330814,8 @@ async function getPageSource(sessionPath) {
|
|
|
330348
330814
|
try {
|
|
330349
330815
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
330350
330816
|
return typeof value === "string" ? value : null;
|
|
330351
|
-
} catch {
|
|
330817
|
+
} catch (err) {
|
|
330818
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330352
330819
|
return null;
|
|
330353
330820
|
}
|
|
330354
330821
|
}
|
|
@@ -330364,7 +330831,8 @@ async function getWindowSize(sessionPath) {
|
|
|
330364
330831
|
return { width: w, height: h };
|
|
330365
330832
|
}
|
|
330366
330833
|
}
|
|
330367
|
-
} catch {
|
|
330834
|
+
} catch (err) {
|
|
330835
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330368
330836
|
}
|
|
330369
330837
|
if (attempt < retries - 1) {
|
|
330370
330838
|
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
@@ -330520,6 +330988,37 @@ async function swipe(sessionPath, direction, distance, windowSize) {
|
|
|
330520
330988
|
async function pressBack(sessionPath) {
|
|
330521
330989
|
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
330522
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
|
+
}
|
|
330523
331022
|
async function pressHome(sessionPath, platform3) {
|
|
330524
331023
|
if (platform3 === "IOS") {
|
|
330525
331024
|
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
@@ -331705,6 +332204,7 @@ var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
|
331705
332204
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
331706
332205
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
331707
332206
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
332207
|
+
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
331708
332208
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
331709
332209
|
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
331710
332210
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
@@ -331741,6 +332241,9 @@ async function wdRequest(path, options) {
|
|
|
331741
332241
|
if (res.status === 404 && /no such session|invalid session id|session is either terminated|deleted session/i.test(body)) {
|
|
331742
332242
|
throw new WebDriverTransportError(`WebDriver session is no longer available: ${body.slice(0, 200)}`);
|
|
331743
332243
|
}
|
|
332244
|
+
if (res.status === 500 && APPIUM_DRIVER_DEAD_RE2.test(body)) {
|
|
332245
|
+
throw new WebDriverTransportError(`WebDriver driver process is not usable: ${body.slice(0, 200)}`);
|
|
332246
|
+
}
|
|
331744
332247
|
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
331745
332248
|
throw new WebDriverTransportError(`Appium server unavailable (${res.status}): ${body.slice(0, 200)}`);
|
|
331746
332249
|
}
|
|
@@ -334509,6 +335012,10 @@ var MobileBridgeDriver = class {
|
|
|
334509
335012
|
async back() {
|
|
334510
335013
|
return this.runAction(() => pressBack(this.sessionPath));
|
|
334511
335014
|
}
|
|
335015
|
+
/** Platform-aware software keyboard dismissal. */
|
|
335016
|
+
async dismissKeyboard() {
|
|
335017
|
+
return this.runAction(() => dismissKeyboard(this.sessionPath, this.platform));
|
|
335018
|
+
}
|
|
334512
335019
|
/** System Home — backgrounds the app only (use `relaunch` for hermetic reset). */
|
|
334513
335020
|
async home() {
|
|
334514
335021
|
return this.runAction(() => pressHome(this.sessionPath, this.platform));
|
|
@@ -334894,8 +335401,12 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
334894
335401
|
pageUrl: call.pageUrl ?? pageUrl
|
|
334895
335402
|
}));
|
|
334896
335403
|
}
|
|
335404
|
+
var runnerPackage = require_package4();
|
|
334897
335405
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
334898
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;
|
|
334899
335410
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
334900
335411
|
var DEFAULT_TYPE_LIMITS = {
|
|
334901
335412
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -336698,7 +337209,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
336698
337209
|
if (getBridgeState().hasActiveSession) {
|
|
336699
337210
|
throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
|
|
336700
337211
|
}
|
|
336701
|
-
const
|
|
337212
|
+
const mobileProgressLines = [];
|
|
337213
|
+
const mobileOnLog = (line) => {
|
|
337214
|
+
mobileProgressLines.push(line);
|
|
337215
|
+
if (mobileProgressLines.length > 500) {
|
|
337216
|
+
mobileProgressLines.splice(0, mobileProgressLines.length - 500);
|
|
337217
|
+
}
|
|
337218
|
+
onLog("progress", mobileProgressLines.join("\n"));
|
|
337219
|
+
};
|
|
336702
337220
|
liveBrowserHandle.patch({
|
|
336703
337221
|
mode: ctx.mode === "survey" ? "survey" : "discovery",
|
|
336704
337222
|
testName: ctx.featureName ?? "Mobile discovery",
|
|
@@ -337722,6 +338240,9 @@ async function startRunner(opts) {
|
|
|
337722
338240
|
method: "POST",
|
|
337723
338241
|
headers,
|
|
337724
338242
|
body: JSON.stringify({
|
|
338243
|
+
runnerName: RUNNER_NAME,
|
|
338244
|
+
runnerVersion: RUNNER_VERSION,
|
|
338245
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
337725
338246
|
runsCompleted,
|
|
337726
338247
|
activeRuns,
|
|
337727
338248
|
maxConcurrent: MAX_CONCURRENT,
|