@validate.qa/runner 1.0.30 → 1.0.31
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 +372 -38
- package/dist/cli.mjs +372 -38
- 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];
|
|
@@ -64542,6 +64542,78 @@ var require_discover_ai_planner = __commonJS({
|
|
|
64542
64542
|
function meaningfulTokens(name) {
|
|
64543
64543
|
return name.toLowerCase().split(/[\s\--_]+/).filter((t) => t.length > 2 && !JOURNEY_MATCH_STOPWORDS.has(t));
|
|
64544
64544
|
}
|
|
64545
|
+
function textIncludesCredentialField(text, field) {
|
|
64546
|
+
return field === "email" ? /\b(e-?mail|username|user\s*name|login\s*id)\b/i.test(text) : /\bpassword|passcode|pin\b/i.test(text);
|
|
64547
|
+
}
|
|
64548
|
+
function textIncludesCredentialSubmit(text) {
|
|
64549
|
+
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);
|
|
64550
|
+
}
|
|
64551
|
+
function targetPagesHaveCredentialControls(siteMap, targetPages) {
|
|
64552
|
+
const targetSet = new Set(targetPages.map(normalizePlannerRoute));
|
|
64553
|
+
let hasEmail = false;
|
|
64554
|
+
let hasPassword = false;
|
|
64555
|
+
let hasSubmit = false;
|
|
64556
|
+
for (const page of siteMap.pages) {
|
|
64557
|
+
if (!targetSet.has(normalizePlannerRoute(page.route)))
|
|
64558
|
+
continue;
|
|
64559
|
+
for (const element of page.elements ?? []) {
|
|
64560
|
+
const label = [
|
|
64561
|
+
element.label,
|
|
64562
|
+
element.role,
|
|
64563
|
+
element.testId,
|
|
64564
|
+
element.name
|
|
64565
|
+
].filter(Boolean).join(" ");
|
|
64566
|
+
if (!label)
|
|
64567
|
+
continue;
|
|
64568
|
+
if (textIncludesCredentialField(label, "email"))
|
|
64569
|
+
hasEmail = true;
|
|
64570
|
+
if (textIncludesCredentialField(label, "password"))
|
|
64571
|
+
hasPassword = true;
|
|
64572
|
+
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)) {
|
|
64573
|
+
hasSubmit = true;
|
|
64574
|
+
}
|
|
64575
|
+
}
|
|
64576
|
+
}
|
|
64577
|
+
return hasEmail && hasPassword && hasSubmit;
|
|
64578
|
+
}
|
|
64579
|
+
function journeyCoversCredentialSubmit(journey) {
|
|
64580
|
+
if (!journey)
|
|
64581
|
+
return false;
|
|
64582
|
+
const text = [
|
|
64583
|
+
journey.name,
|
|
64584
|
+
journey.expectedOutcome,
|
|
64585
|
+
...journey.steps ?? [],
|
|
64586
|
+
...journey.provenCommands ?? []
|
|
64587
|
+
].join(" ");
|
|
64588
|
+
const hasCredentialPair = /\bcredentials\b/i.test(text) || textIncludesCredentialField(text, "email") && textIncludesCredentialField(text, "password");
|
|
64589
|
+
return hasCredentialPair && textIncludesCredentialSubmit(text);
|
|
64590
|
+
}
|
|
64591
|
+
function scenarioAuthCredentialPolicy(scenario, steps) {
|
|
64592
|
+
const text = [
|
|
64593
|
+
scenario.name,
|
|
64594
|
+
scenario.goal,
|
|
64595
|
+
scenario.type,
|
|
64596
|
+
scenario.variant,
|
|
64597
|
+
...steps.map((step) => step.action)
|
|
64598
|
+
].filter(Boolean).join(" ");
|
|
64599
|
+
if (!/\b(auth|authentication|sign[ -]?in|signin|log[ -]?in|login|session)\b/i.test(text)) {
|
|
64600
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64601
|
+
}
|
|
64602
|
+
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)) {
|
|
64603
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64604
|
+
}
|
|
64605
|
+
if (!/\b(credential|e-?mail|username|user\s*name|password|valid|invalid|empty|required|wrong|case|special|leading|trailing)\b/i.test(text)) {
|
|
64606
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64607
|
+
}
|
|
64608
|
+
const uiOnly = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/i.test(text);
|
|
64609
|
+
const negative = /\b(invalid|empty|required|missing|wrong|error|validation|case|special|leading|trailing)\b/i.test(text);
|
|
64610
|
+
const happy = /\b(happy|valid\s+credentials|success|successful)\b/i.test(text) || scenario.type === "HAPPY_PATH" || scenario.variant === "happy";
|
|
64611
|
+
return {
|
|
64612
|
+
needsCredentialEvidence: true,
|
|
64613
|
+
needsSubmitStep: !uiOnly && (happy || negative),
|
|
64614
|
+
reason: uiOnly ? "credential UI scenario" : negative ? "credential validation scenario" : "credential sign-in scenario"
|
|
64615
|
+
};
|
|
64616
|
+
}
|
|
64545
64617
|
function truncateOutcome(text, limit) {
|
|
64546
64618
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
64547
64619
|
if (normalized.length <= limit)
|
|
@@ -65307,6 +65379,18 @@ Each entry is exactly one of:
|
|
|
65307
65379
|
if (matchSource === "route") {
|
|
65308
65380
|
warnings.push(`"${sc.name}" matched to journey "${matchingSuggestion.name}" by route overlap only - evidence may be weak`);
|
|
65309
65381
|
}
|
|
65382
|
+
const authCredentialPolicy = scenarioAuthCredentialPolicy(sc, steps);
|
|
65383
|
+
if (authCredentialPolicy.needsCredentialEvidence) {
|
|
65384
|
+
const hasCredentialEvidence = targetPagesHaveCredentialControls(siteMap, sc.targetPages) || journeyCoversCredentialSubmit(matchingSuggestion);
|
|
65385
|
+
if (!hasCredentialEvidence) {
|
|
65386
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} lacks observed email/password/sign-in controls or a recorded credential journey`);
|
|
65387
|
+
continue;
|
|
65388
|
+
}
|
|
65389
|
+
if (authCredentialPolicy.needsSubmitStep && !steps.some((step) => textIncludesCredentialSubmit(step.action)) && !journeyCoversCredentialSubmit(matchingSuggestion)) {
|
|
65390
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} does not include a concrete sign-in submit step`);
|
|
65391
|
+
continue;
|
|
65392
|
+
}
|
|
65393
|
+
}
|
|
65310
65394
|
const evidence = (0, discover_planner_js_1.extractScenarioEvidence)(
|
|
65311
65395
|
siteMap.pages,
|
|
65312
65396
|
sc.targetPages,
|
|
@@ -65972,6 +66056,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65972
66056
|
## Coverage Strategy
|
|
65973
66057
|
- 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
66058
|
- 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)
|
|
66059
|
+
- 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
66060
|
- 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
66061
|
- 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
66062
|
- For navigation: multi-screen user journeys within the feature scope (do NOT drift into unrelated features)
|
|
@@ -67448,7 +67533,7 @@ ${statePacket}` },
|
|
|
67448
67533
|
"react-root",
|
|
67449
67534
|
"application"
|
|
67450
67535
|
]);
|
|
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;
|
|
67536
|
+
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
67537
|
function locatorLocalName(value) {
|
|
67453
67538
|
const trimmed = value.trim();
|
|
67454
67539
|
const withoutStrategy = trimmed.startsWith("id=") ? trimmed.slice(3) : trimmed.startsWith("~") ? trimmed.slice(1) : trimmed;
|
|
@@ -67528,6 +67613,88 @@ ${statePacket}` },
|
|
|
67528
67613
|
}));
|
|
67529
67614
|
return true;
|
|
67530
67615
|
}
|
|
67616
|
+
function scenarioText(scenario) {
|
|
67617
|
+
return [
|
|
67618
|
+
scenario.name,
|
|
67619
|
+
scenario.goal,
|
|
67620
|
+
...scenario.steps.flatMap((step) => [step.action, step.target, step.hint]),
|
|
67621
|
+
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67622
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67623
|
+
}
|
|
67624
|
+
function actionText(action) {
|
|
67625
|
+
const bundle = action.metadata.locatorBundle;
|
|
67626
|
+
return [
|
|
67627
|
+
action.type,
|
|
67628
|
+
action.selector,
|
|
67629
|
+
action.value,
|
|
67630
|
+
action.innerText,
|
|
67631
|
+
action.metadata.targetLabel,
|
|
67632
|
+
bundle?.text,
|
|
67633
|
+
bundle?.contentDesc,
|
|
67634
|
+
bundle?.accessibilityId,
|
|
67635
|
+
bundle?.resourceId,
|
|
67636
|
+
bundle?.className,
|
|
67637
|
+
bundle?.xpath
|
|
67638
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67639
|
+
}
|
|
67640
|
+
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67641
|
+
const text = scenarioText(scenario);
|
|
67642
|
+
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);
|
|
67643
|
+
const typedEmailIndexes = [];
|
|
67644
|
+
const typedPasswordIndexes = [];
|
|
67645
|
+
actions.forEach((action, index) => {
|
|
67646
|
+
if (action.type !== "TYPE")
|
|
67647
|
+
return;
|
|
67648
|
+
const textForAction = actionText(action);
|
|
67649
|
+
if (/\b(email|e-mail|username|user name|login)\b|\{\{test_user_email\}\}/i.test(textForAction)) {
|
|
67650
|
+
typedEmailIndexes.push(index);
|
|
67651
|
+
}
|
|
67652
|
+
if (/\b(password|passcode|pin)\b|\{\{test_user_password\}\}/i.test(textForAction)) {
|
|
67653
|
+
typedPasswordIndexes.push(index);
|
|
67654
|
+
}
|
|
67655
|
+
});
|
|
67656
|
+
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67657
|
+
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67658
|
+
const submitAfterCredentials = actions.some((action, index) => {
|
|
67659
|
+
if (action.type !== "TAP" || index <= lastCredentialIndex)
|
|
67660
|
+
return false;
|
|
67661
|
+
const textForAction = actionText(action);
|
|
67662
|
+
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
67663
|
+
return false;
|
|
67664
|
+
return !/\b(with\s+(email|e-mail|google|facebook|apple)|email\s+form|showemailformbtn)\b/i.test(textForAction);
|
|
67665
|
+
});
|
|
67666
|
+
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67667
|
+
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67668
|
+
return "credential fields were filled but no sign-in submit tap was recorded after them";
|
|
67669
|
+
}
|
|
67670
|
+
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67671
|
+
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67672
|
+
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67673
|
+
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67674
|
+
if (action.type !== "TAP")
|
|
67675
|
+
return false;
|
|
67676
|
+
const textForAction = actionText(action);
|
|
67677
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67678
|
+
return false;
|
|
67679
|
+
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67680
|
+
});
|
|
67681
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67682
|
+
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67683
|
+
}
|
|
67684
|
+
if (!isCredentialSignIn)
|
|
67685
|
+
return null;
|
|
67686
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
67687
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
67688
|
+
const tappedSubmit = submitAfterCredentials;
|
|
67689
|
+
if (typedEmail && typedPassword && tappedSubmit)
|
|
67690
|
+
return null;
|
|
67691
|
+
const missing = [
|
|
67692
|
+
typedEmail ? null : "email/username entry",
|
|
67693
|
+
typedPassword ? null : "password entry",
|
|
67694
|
+
tappedSubmit ? null : "sign-in submit tap"
|
|
67695
|
+
].filter((value) => value !== null);
|
|
67696
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67697
|
+
}
|
|
67531
67698
|
function actionCycleKey(action) {
|
|
67532
67699
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67533
67700
|
}
|
|
@@ -67581,6 +67748,11 @@ ${statePacket}` },
|
|
|
67581
67748
|
if (actions.length < beforeContainerDrop) {
|
|
67582
67749
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
67583
67750
|
}
|
|
67751
|
+
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
67752
|
+
if (intentMismatch) {
|
|
67753
|
+
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
67754
|
+
return null;
|
|
67755
|
+
}
|
|
67584
67756
|
const floorAppended = appendHybridFloor(actions, recording, platform3);
|
|
67585
67757
|
if (floorAppended) {
|
|
67586
67758
|
log2(" appended hybrid durable-outcome floor (landing-screen ASSERT_VISIBLE).");
|
|
@@ -68278,7 +68450,8 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
68278
68450
|
2. STEP RESULTS: which steps passed before the failure.
|
|
68279
68451
|
3. TREE: does the current screen match what the failed step expects?
|
|
68280
68452
|
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.
|
|
68453
|
+
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.
|
|
68454
|
+
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
68455
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68283
68456
|
{"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
68457
|
{"verdict":"app_bug","reason":"app crashed/blank/5xx (NOT rate-limit)"}
|
|
@@ -68335,6 +68508,104 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68335
68508
|
function isDrift(error2) {
|
|
68336
68509
|
return /unexpected screen|screen drift/i.test(error2 ?? "");
|
|
68337
68510
|
}
|
|
68511
|
+
function mobileStepSearchText(step) {
|
|
68512
|
+
const rawTarget = step.target;
|
|
68513
|
+
const structuredTarget = rawTarget && typeof rawTarget === "object" ? rawTarget : void 0;
|
|
68514
|
+
const bundle = structuredTarget?.locatorBundle;
|
|
68515
|
+
const stepValue = step.value;
|
|
68516
|
+
return [
|
|
68517
|
+
step.action,
|
|
68518
|
+
step.description,
|
|
68519
|
+
typeof stepValue === "string" ? stepValue : void 0,
|
|
68520
|
+
typeof rawTarget === "string" ? rawTarget : void 0,
|
|
68521
|
+
structuredTarget?.primary,
|
|
68522
|
+
bundle?.text,
|
|
68523
|
+
bundle?.contentDesc,
|
|
68524
|
+
bundle?.accessibilityId,
|
|
68525
|
+
bundle?.resourceId,
|
|
68526
|
+
bundle?.className,
|
|
68527
|
+
bundle?.xpath
|
|
68528
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
68529
|
+
}
|
|
68530
|
+
function detectInvalidExternalAuthSetup(steps, error2) {
|
|
68531
|
+
if (!/foreground app changed|outside target app|com\.google\.android\.gms|external app/i.test(error2 ?? "")) {
|
|
68532
|
+
return null;
|
|
68533
|
+
}
|
|
68534
|
+
const hasBareLoginTap = steps.some((step) => {
|
|
68535
|
+
if (!/\btap\b/i.test(step.action))
|
|
68536
|
+
return false;
|
|
68537
|
+
const text = mobileStepSearchText(step);
|
|
68538
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(text))
|
|
68539
|
+
return false;
|
|
68540
|
+
return /\b(sign\s*in|signin|log\s*in|login)\b/.test(text);
|
|
68541
|
+
});
|
|
68542
|
+
if (!hasBareLoginTap)
|
|
68543
|
+
return null;
|
|
68544
|
+
const hasCredentialEntry = steps.some((step) => {
|
|
68545
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
68546
|
+
return false;
|
|
68547
|
+
const text = mobileStepSearchText(step);
|
|
68548
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68549
|
+
});
|
|
68550
|
+
if (hasCredentialEntry)
|
|
68551
|
+
return null;
|
|
68552
|
+
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";
|
|
68553
|
+
}
|
|
68554
|
+
function isCredentialFieldStep(step) {
|
|
68555
|
+
const text = mobileStepSearchText(step);
|
|
68556
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68557
|
+
}
|
|
68558
|
+
function isCredentialSubmitTap(step) {
|
|
68559
|
+
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68560
|
+
return false;
|
|
68561
|
+
const text = mobileStepSearchText(step);
|
|
68562
|
+
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)) {
|
|
68563
|
+
return false;
|
|
68564
|
+
}
|
|
68565
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit)\b/.test(text);
|
|
68566
|
+
}
|
|
68567
|
+
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68568
|
+
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
68569
|
+
return null;
|
|
68570
|
+
}
|
|
68571
|
+
const allText = steps.map(mobileStepSearchText).join(" ");
|
|
68572
|
+
if (!/\b(email|e-mail|username|user\s*name|password|passcode|pin)\b/.test(allText))
|
|
68573
|
+
return null;
|
|
68574
|
+
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);
|
|
68575
|
+
let lastCredentialEditOrder = -1;
|
|
68576
|
+
for (const step of steps) {
|
|
68577
|
+
if (!/\b(type|fill|input|enter|clear)\b/i.test(step.action))
|
|
68578
|
+
continue;
|
|
68579
|
+
if (!isCredentialFieldStep(step))
|
|
68580
|
+
continue;
|
|
68581
|
+
lastCredentialEditOrder = Math.max(lastCredentialEditOrder, step.order ?? -1);
|
|
68582
|
+
}
|
|
68583
|
+
if (lastCredentialEditOrder < 0)
|
|
68584
|
+
return null;
|
|
68585
|
+
if (uiOnlyCredentialScenario) {
|
|
68586
|
+
const hasCredentialFormEvidence = steps.some((step) => {
|
|
68587
|
+
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68588
|
+
return false;
|
|
68589
|
+
const text = mobileStepSearchText(step);
|
|
68590
|
+
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);
|
|
68591
|
+
});
|
|
68592
|
+
if (hasCredentialFormEvidence)
|
|
68593
|
+
return null;
|
|
68594
|
+
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";
|
|
68595
|
+
}
|
|
68596
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isCredentialSubmitTap(step));
|
|
68597
|
+
if (hasSubmitAfterCredentialEdit)
|
|
68598
|
+
return null;
|
|
68599
|
+
const hasValidationAssertion = steps.some((step) => {
|
|
68600
|
+
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68601
|
+
return false;
|
|
68602
|
+
const text = mobileStepSearchText(step);
|
|
68603
|
+
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);
|
|
68604
|
+
});
|
|
68605
|
+
if (hasValidationAssertion)
|
|
68606
|
+
return null;
|
|
68607
|
+
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";
|
|
68608
|
+
}
|
|
68338
68609
|
function stripCodeFences(text) {
|
|
68339
68610
|
const trimmed = text.trim();
|
|
68340
68611
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -68593,6 +68864,24 @@ ${args.directive}`);
|
|
|
68593
68864
|
const registryPresent = options.selectorRegistryContext != null;
|
|
68594
68865
|
const platform3 = target.platform;
|
|
68595
68866
|
const trimmedLogs = tailLimit(ctx.logs ?? "", MAX_LOG_CHARS);
|
|
68867
|
+
const invalidExternalAuthSetup = detectInvalidExternalAuthSetup(steps, ctx.error);
|
|
68868
|
+
if (invalidExternalAuthSetup) {
|
|
68869
|
+
return {
|
|
68870
|
+
passed: false,
|
|
68871
|
+
verdict: "test_invalid",
|
|
68872
|
+
healReason: invalidExternalAuthSetup,
|
|
68873
|
+
attemptsUsed: 0
|
|
68874
|
+
};
|
|
68875
|
+
}
|
|
68876
|
+
const invalidAuthValidationSetup = detectInvalidAuthValidationSetup(steps, ctx.error);
|
|
68877
|
+
if (invalidAuthValidationSetup) {
|
|
68878
|
+
return {
|
|
68879
|
+
passed: false,
|
|
68880
|
+
verdict: "test_invalid",
|
|
68881
|
+
healReason: invalidAuthValidationSetup,
|
|
68882
|
+
attemptsUsed: 0
|
|
68883
|
+
};
|
|
68884
|
+
}
|
|
68596
68885
|
let attemptCap = base2;
|
|
68597
68886
|
let recoverableUsed = 0;
|
|
68598
68887
|
let bonusUsed = 0;
|
|
@@ -68778,7 +69067,7 @@ ${args.directive}`);
|
|
|
68778
69067
|
removeTags: script.removeTags
|
|
68779
69068
|
};
|
|
68780
69069
|
}
|
|
68781
|
-
var DEFAULT_PHASE2_MAX_PASSES =
|
|
69070
|
+
var DEFAULT_PHASE2_MAX_PASSES = 4;
|
|
68782
69071
|
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
69072
|
## Key insight \u2014 ENTRY-SCREEN DRIFT
|
|
68784
69073
|
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 +69076,8 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
68787
69076
|
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
69077
|
3. If a locator is simply wrong, swap it for a stronger identity locator present in the LIVE tree.
|
|
68789
69078
|
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
|
|
69079
|
+
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.
|
|
69080
|
+
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
69081
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68792
69082
|
{"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
69083
|
{"verdict":"observe","action":{"type":"tap","target":"mobile.text('<exact label visible in the live tree>')"}}
|
|
@@ -329889,7 +330179,7 @@ var require_package4 = __commonJS({
|
|
|
329889
330179
|
"package.json"(exports2, module2) {
|
|
329890
330180
|
module2.exports = {
|
|
329891
330181
|
name: "@validate.qa/runner",
|
|
329892
|
-
version: "1.0.
|
|
330182
|
+
version: "1.0.31",
|
|
329893
330183
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
329894
330184
|
bin: {
|
|
329895
330185
|
"validate-runner": "dist/cli.js"
|
|
@@ -330038,6 +330328,8 @@ var MAX_RETRIES = 3;
|
|
|
330038
330328
|
var STARTUP_TIMEOUT_MS = 3e4;
|
|
330039
330329
|
var HEALTH_CHECK_INTERVAL_MS = 5e3;
|
|
330040
330330
|
var STABLE_AFTER_MS = 6e4;
|
|
330331
|
+
var DEFAULT_APPIUM_LOG_LEVEL = "warn";
|
|
330332
|
+
var MAX_APPIUM_LOG_LINE_CHARS = 4e3;
|
|
330041
330333
|
var appiumProcess = null;
|
|
330042
330334
|
var serverState = {
|
|
330043
330335
|
running: false,
|
|
@@ -330097,6 +330389,23 @@ function appendLog(line) {
|
|
|
330097
330389
|
logs = logs.slice(-MAX_LOG_LINES);
|
|
330098
330390
|
}
|
|
330099
330391
|
}
|
|
330392
|
+
function resolveAppiumLogLevel() {
|
|
330393
|
+
const raw = process.env.APPIUM_LOG_LEVEL?.trim();
|
|
330394
|
+
if (raw && /^[a-z]+(?::[a-z]+)?$/i.test(raw)) return raw;
|
|
330395
|
+
return DEFAULT_APPIUM_LOG_LEVEL;
|
|
330396
|
+
}
|
|
330397
|
+
function buildAppiumArgs(port, drivers) {
|
|
330398
|
+
const args = ["--port", String(port), "--log-level", resolveAppiumLogLevel()];
|
|
330399
|
+
if (drivers && drivers.length > 0) {
|
|
330400
|
+
args.push("--use-drivers", drivers.join(","));
|
|
330401
|
+
}
|
|
330402
|
+
return args;
|
|
330403
|
+
}
|
|
330404
|
+
function sanitizeAppiumLogLine(line) {
|
|
330405
|
+
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]"');
|
|
330406
|
+
if (redacted.length <= MAX_APPIUM_LOG_LINE_CHARS) return redacted;
|
|
330407
|
+
return `${redacted.slice(0, MAX_APPIUM_LOG_LINE_CHARS)}\u2026 [truncated ${redacted.length - MAX_APPIUM_LOG_LINE_CHARS} chars]`;
|
|
330408
|
+
}
|
|
330100
330409
|
async function checkAppiumHealth(port = serverState.port) {
|
|
330101
330410
|
try {
|
|
330102
330411
|
const controller = new AbortController();
|
|
@@ -330129,10 +330438,7 @@ async function startAppiumServer(options) {
|
|
|
330129
330438
|
return serverState;
|
|
330130
330439
|
}
|
|
330131
330440
|
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
|
-
}
|
|
330441
|
+
const args = buildAppiumArgs(port, options?.drivers);
|
|
330136
330442
|
intentionalStop = false;
|
|
330137
330443
|
lastStartOptions = { ...lastStartOptions, ...options };
|
|
330138
330444
|
if (appiumGaveUp) {
|
|
@@ -330151,14 +330457,14 @@ async function startAppiumServer(options) {
|
|
|
330151
330457
|
serverState.pid = appiumProcess.pid ?? null;
|
|
330152
330458
|
serverState.port = port;
|
|
330153
330459
|
appiumProcess.stdout?.on("data", (data) => {
|
|
330154
|
-
const line = data.toString().trim();
|
|
330460
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330155
330461
|
if (line) {
|
|
330156
330462
|
appendLog(`[appium:stdout] ${line}`);
|
|
330157
330463
|
options?.onLog?.(line);
|
|
330158
330464
|
}
|
|
330159
330465
|
});
|
|
330160
330466
|
appiumProcess.stderr?.on("data", (data) => {
|
|
330161
|
-
const line = data.toString().trim();
|
|
330467
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330162
330468
|
if (line) {
|
|
330163
330469
|
appendLog(`[appium:stderr] ${line}`);
|
|
330164
330470
|
options?.onLog?.(line);
|
|
@@ -330317,6 +330623,17 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
330317
330623
|
// src/services/mobile/appium-actions.ts
|
|
330318
330624
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
330319
330625
|
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
330626
|
+
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;
|
|
330627
|
+
var AppiumDriverDeadError = class extends Error {
|
|
330628
|
+
isAppiumDriverDead = true;
|
|
330629
|
+
constructor(message) {
|
|
330630
|
+
super(message);
|
|
330631
|
+
this.name = "AppiumDriverDeadError";
|
|
330632
|
+
}
|
|
330633
|
+
};
|
|
330634
|
+
function isAppiumDriverDeadError(err) {
|
|
330635
|
+
return err instanceof AppiumDriverDeadError || typeof err === "object" && err !== null && err.isAppiumDriverDead === true;
|
|
330636
|
+
}
|
|
330320
330637
|
async function appiumRequest(path, options) {
|
|
330321
330638
|
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
330322
330639
|
const res = await fetch(url, {
|
|
@@ -330326,6 +330643,9 @@ async function appiumRequest(path, options) {
|
|
|
330326
330643
|
});
|
|
330327
330644
|
if (!res.ok) {
|
|
330328
330645
|
const body = await res.text().catch(() => "");
|
|
330646
|
+
if (APPIUM_DRIVER_DEAD_RE.test(body)) {
|
|
330647
|
+
throw new AppiumDriverDeadError(`Appium driver session is not usable (${res.status}): ${body.slice(0, 200)}`);
|
|
330648
|
+
}
|
|
330329
330649
|
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
330330
330650
|
}
|
|
330331
330651
|
return res.json();
|
|
@@ -330340,7 +330660,8 @@ async function getScreenshot(sessionPath) {
|
|
|
330340
330660
|
try {
|
|
330341
330661
|
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
330342
330662
|
return typeof value === "string" ? value : null;
|
|
330343
|
-
} catch {
|
|
330663
|
+
} catch (err) {
|
|
330664
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330344
330665
|
return null;
|
|
330345
330666
|
}
|
|
330346
330667
|
}
|
|
@@ -330348,7 +330669,8 @@ async function getPageSource(sessionPath) {
|
|
|
330348
330669
|
try {
|
|
330349
330670
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
330350
330671
|
return typeof value === "string" ? value : null;
|
|
330351
|
-
} catch {
|
|
330672
|
+
} catch (err) {
|
|
330673
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330352
330674
|
return null;
|
|
330353
330675
|
}
|
|
330354
330676
|
}
|
|
@@ -330364,7 +330686,8 @@ async function getWindowSize(sessionPath) {
|
|
|
330364
330686
|
return { width: w, height: h };
|
|
330365
330687
|
}
|
|
330366
330688
|
}
|
|
330367
|
-
} catch {
|
|
330689
|
+
} catch (err) {
|
|
330690
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330368
330691
|
}
|
|
330369
330692
|
if (attempt < retries - 1) {
|
|
330370
330693
|
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
@@ -331705,6 +332028,7 @@ var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
|
331705
332028
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
331706
332029
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
331707
332030
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
332031
|
+
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
331708
332032
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
331709
332033
|
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
331710
332034
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
@@ -331741,6 +332065,9 @@ async function wdRequest(path, options) {
|
|
|
331741
332065
|
if (res.status === 404 && /no such session|invalid session id|session is either terminated|deleted session/i.test(body)) {
|
|
331742
332066
|
throw new WebDriverTransportError(`WebDriver session is no longer available: ${body.slice(0, 200)}`);
|
|
331743
332067
|
}
|
|
332068
|
+
if (res.status === 500 && APPIUM_DRIVER_DEAD_RE2.test(body)) {
|
|
332069
|
+
throw new WebDriverTransportError(`WebDriver driver process is not usable: ${body.slice(0, 200)}`);
|
|
332070
|
+
}
|
|
331744
332071
|
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
331745
332072
|
throw new WebDriverTransportError(`Appium server unavailable (${res.status}): ${body.slice(0, 200)}`);
|
|
331746
332073
|
}
|
|
@@ -336698,7 +337025,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
336698
337025
|
if (getBridgeState().hasActiveSession) {
|
|
336699
337026
|
throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
|
|
336700
337027
|
}
|
|
336701
|
-
const
|
|
337028
|
+
const mobileProgressLines = [];
|
|
337029
|
+
const mobileOnLog = (line) => {
|
|
337030
|
+
mobileProgressLines.push(line);
|
|
337031
|
+
if (mobileProgressLines.length > 500) {
|
|
337032
|
+
mobileProgressLines.splice(0, mobileProgressLines.length - 500);
|
|
337033
|
+
}
|
|
337034
|
+
onLog("progress", mobileProgressLines.join("\n"));
|
|
337035
|
+
};
|
|
336702
337036
|
liveBrowserHandle.patch({
|
|
336703
337037
|
mode: ctx.mode === "survey" ? "survey" : "discovery",
|
|
336704
337038
|
testName: ctx.featureName ?? "Mobile discovery",
|
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];
|
|
@@ -64547,6 +64547,78 @@ var require_discover_ai_planner = __commonJS({
|
|
|
64547
64547
|
function meaningfulTokens(name) {
|
|
64548
64548
|
return name.toLowerCase().split(/[\s\--_]+/).filter((t) => t.length > 2 && !JOURNEY_MATCH_STOPWORDS.has(t));
|
|
64549
64549
|
}
|
|
64550
|
+
function textIncludesCredentialField(text, field) {
|
|
64551
|
+
return field === "email" ? /\b(e-?mail|username|user\s*name|login\s*id)\b/i.test(text) : /\bpassword|passcode|pin\b/i.test(text);
|
|
64552
|
+
}
|
|
64553
|
+
function textIncludesCredentialSubmit(text) {
|
|
64554
|
+
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);
|
|
64555
|
+
}
|
|
64556
|
+
function targetPagesHaveCredentialControls(siteMap, targetPages) {
|
|
64557
|
+
const targetSet = new Set(targetPages.map(normalizePlannerRoute));
|
|
64558
|
+
let hasEmail = false;
|
|
64559
|
+
let hasPassword = false;
|
|
64560
|
+
let hasSubmit = false;
|
|
64561
|
+
for (const page of siteMap.pages) {
|
|
64562
|
+
if (!targetSet.has(normalizePlannerRoute(page.route)))
|
|
64563
|
+
continue;
|
|
64564
|
+
for (const element of page.elements ?? []) {
|
|
64565
|
+
const label = [
|
|
64566
|
+
element.label,
|
|
64567
|
+
element.role,
|
|
64568
|
+
element.testId,
|
|
64569
|
+
element.name
|
|
64570
|
+
].filter(Boolean).join(" ");
|
|
64571
|
+
if (!label)
|
|
64572
|
+
continue;
|
|
64573
|
+
if (textIncludesCredentialField(label, "email"))
|
|
64574
|
+
hasEmail = true;
|
|
64575
|
+
if (textIncludesCredentialField(label, "password"))
|
|
64576
|
+
hasPassword = true;
|
|
64577
|
+
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)) {
|
|
64578
|
+
hasSubmit = true;
|
|
64579
|
+
}
|
|
64580
|
+
}
|
|
64581
|
+
}
|
|
64582
|
+
return hasEmail && hasPassword && hasSubmit;
|
|
64583
|
+
}
|
|
64584
|
+
function journeyCoversCredentialSubmit(journey) {
|
|
64585
|
+
if (!journey)
|
|
64586
|
+
return false;
|
|
64587
|
+
const text = [
|
|
64588
|
+
journey.name,
|
|
64589
|
+
journey.expectedOutcome,
|
|
64590
|
+
...journey.steps ?? [],
|
|
64591
|
+
...journey.provenCommands ?? []
|
|
64592
|
+
].join(" ");
|
|
64593
|
+
const hasCredentialPair = /\bcredentials\b/i.test(text) || textIncludesCredentialField(text, "email") && textIncludesCredentialField(text, "password");
|
|
64594
|
+
return hasCredentialPair && textIncludesCredentialSubmit(text);
|
|
64595
|
+
}
|
|
64596
|
+
function scenarioAuthCredentialPolicy(scenario, steps) {
|
|
64597
|
+
const text = [
|
|
64598
|
+
scenario.name,
|
|
64599
|
+
scenario.goal,
|
|
64600
|
+
scenario.type,
|
|
64601
|
+
scenario.variant,
|
|
64602
|
+
...steps.map((step) => step.action)
|
|
64603
|
+
].filter(Boolean).join(" ");
|
|
64604
|
+
if (!/\b(auth|authentication|sign[ -]?in|signin|log[ -]?in|login|session)\b/i.test(text)) {
|
|
64605
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64606
|
+
}
|
|
64607
|
+
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)) {
|
|
64608
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64609
|
+
}
|
|
64610
|
+
if (!/\b(credential|e-?mail|username|user\s*name|password|valid|invalid|empty|required|wrong|case|special|leading|trailing)\b/i.test(text)) {
|
|
64611
|
+
return { needsCredentialEvidence: false, needsSubmitStep: false, reason: "" };
|
|
64612
|
+
}
|
|
64613
|
+
const uiOnly = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/i.test(text);
|
|
64614
|
+
const negative = /\b(invalid|empty|required|missing|wrong|error|validation|case|special|leading|trailing)\b/i.test(text);
|
|
64615
|
+
const happy = /\b(happy|valid\s+credentials|success|successful)\b/i.test(text) || scenario.type === "HAPPY_PATH" || scenario.variant === "happy";
|
|
64616
|
+
return {
|
|
64617
|
+
needsCredentialEvidence: true,
|
|
64618
|
+
needsSubmitStep: !uiOnly && (happy || negative),
|
|
64619
|
+
reason: uiOnly ? "credential UI scenario" : negative ? "credential validation scenario" : "credential sign-in scenario"
|
|
64620
|
+
};
|
|
64621
|
+
}
|
|
64550
64622
|
function truncateOutcome(text, limit) {
|
|
64551
64623
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
64552
64624
|
if (normalized.length <= limit)
|
|
@@ -65312,6 +65384,18 @@ Each entry is exactly one of:
|
|
|
65312
65384
|
if (matchSource === "route") {
|
|
65313
65385
|
warnings.push(`"${sc.name}" matched to journey "${matchingSuggestion.name}" by route overlap only - evidence may be weak`);
|
|
65314
65386
|
}
|
|
65387
|
+
const authCredentialPolicy = scenarioAuthCredentialPolicy(sc, steps);
|
|
65388
|
+
if (authCredentialPolicy.needsCredentialEvidence) {
|
|
65389
|
+
const hasCredentialEvidence = targetPagesHaveCredentialControls(siteMap, sc.targetPages) || journeyCoversCredentialSubmit(matchingSuggestion);
|
|
65390
|
+
if (!hasCredentialEvidence) {
|
|
65391
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} lacks observed email/password/sign-in controls or a recorded credential journey`);
|
|
65392
|
+
continue;
|
|
65393
|
+
}
|
|
65394
|
+
if (authCredentialPolicy.needsSubmitStep && !steps.some((step) => textIncludesCredentialSubmit(step.action)) && !journeyCoversCredentialSubmit(matchingSuggestion)) {
|
|
65395
|
+
warnings.push(`Rejected "${sc.name}": ${authCredentialPolicy.reason} does not include a concrete sign-in submit step`);
|
|
65396
|
+
continue;
|
|
65397
|
+
}
|
|
65398
|
+
}
|
|
65315
65399
|
const evidence = (0, discover_planner_js_1.extractScenarioEvidence)(
|
|
65316
65400
|
siteMap.pages,
|
|
65317
65401
|
sc.targetPages,
|
|
@@ -65977,6 +66061,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65977
66061
|
## Coverage Strategy
|
|
65978
66062
|
- 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
66063
|
- 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)
|
|
66064
|
+
- 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
66065
|
- 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
66066
|
- 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
66067
|
- For navigation: multi-screen user journeys within the feature scope (do NOT drift into unrelated features)
|
|
@@ -67453,7 +67538,7 @@ ${statePacket}` },
|
|
|
67453
67538
|
"react-root",
|
|
67454
67539
|
"application"
|
|
67455
67540
|
]);
|
|
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;
|
|
67541
|
+
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
67542
|
function locatorLocalName(value) {
|
|
67458
67543
|
const trimmed = value.trim();
|
|
67459
67544
|
const withoutStrategy = trimmed.startsWith("id=") ? trimmed.slice(3) : trimmed.startsWith("~") ? trimmed.slice(1) : trimmed;
|
|
@@ -67533,6 +67618,88 @@ ${statePacket}` },
|
|
|
67533
67618
|
}));
|
|
67534
67619
|
return true;
|
|
67535
67620
|
}
|
|
67621
|
+
function scenarioText(scenario) {
|
|
67622
|
+
return [
|
|
67623
|
+
scenario.name,
|
|
67624
|
+
scenario.goal,
|
|
67625
|
+
...scenario.steps.flatMap((step) => [step.action, step.target, step.hint]),
|
|
67626
|
+
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67627
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67628
|
+
}
|
|
67629
|
+
function actionText(action) {
|
|
67630
|
+
const bundle = action.metadata.locatorBundle;
|
|
67631
|
+
return [
|
|
67632
|
+
action.type,
|
|
67633
|
+
action.selector,
|
|
67634
|
+
action.value,
|
|
67635
|
+
action.innerText,
|
|
67636
|
+
action.metadata.targetLabel,
|
|
67637
|
+
bundle?.text,
|
|
67638
|
+
bundle?.contentDesc,
|
|
67639
|
+
bundle?.accessibilityId,
|
|
67640
|
+
bundle?.resourceId,
|
|
67641
|
+
bundle?.className,
|
|
67642
|
+
bundle?.xpath
|
|
67643
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67644
|
+
}
|
|
67645
|
+
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67646
|
+
const text = scenarioText(scenario);
|
|
67647
|
+
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);
|
|
67648
|
+
const typedEmailIndexes = [];
|
|
67649
|
+
const typedPasswordIndexes = [];
|
|
67650
|
+
actions.forEach((action, index) => {
|
|
67651
|
+
if (action.type !== "TYPE")
|
|
67652
|
+
return;
|
|
67653
|
+
const textForAction = actionText(action);
|
|
67654
|
+
if (/\b(email|e-mail|username|user name|login)\b|\{\{test_user_email\}\}/i.test(textForAction)) {
|
|
67655
|
+
typedEmailIndexes.push(index);
|
|
67656
|
+
}
|
|
67657
|
+
if (/\b(password|passcode|pin)\b|\{\{test_user_password\}\}/i.test(textForAction)) {
|
|
67658
|
+
typedPasswordIndexes.push(index);
|
|
67659
|
+
}
|
|
67660
|
+
});
|
|
67661
|
+
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67662
|
+
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67663
|
+
const submitAfterCredentials = actions.some((action, index) => {
|
|
67664
|
+
if (action.type !== "TAP" || index <= lastCredentialIndex)
|
|
67665
|
+
return false;
|
|
67666
|
+
const textForAction = actionText(action);
|
|
67667
|
+
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
67668
|
+
return false;
|
|
67669
|
+
return !/\b(with\s+(email|e-mail|google|facebook|apple)|email\s+form|showemailformbtn)\b/i.test(textForAction);
|
|
67670
|
+
});
|
|
67671
|
+
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67672
|
+
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67673
|
+
return "credential fields were filled but no sign-in submit tap was recorded after them";
|
|
67674
|
+
}
|
|
67675
|
+
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67676
|
+
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67677
|
+
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67678
|
+
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67679
|
+
if (action.type !== "TAP")
|
|
67680
|
+
return false;
|
|
67681
|
+
const textForAction = actionText(action);
|
|
67682
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67683
|
+
return false;
|
|
67684
|
+
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67685
|
+
});
|
|
67686
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && !emptyFieldValidationScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67687
|
+
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67688
|
+
}
|
|
67689
|
+
if (!isCredentialSignIn)
|
|
67690
|
+
return null;
|
|
67691
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
67692
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
67693
|
+
const tappedSubmit = submitAfterCredentials;
|
|
67694
|
+
if (typedEmail && typedPassword && tappedSubmit)
|
|
67695
|
+
return null;
|
|
67696
|
+
const missing = [
|
|
67697
|
+
typedEmail ? null : "email/username entry",
|
|
67698
|
+
typedPassword ? null : "password entry",
|
|
67699
|
+
tappedSubmit ? null : "sign-in submit tap"
|
|
67700
|
+
].filter((value) => value !== null);
|
|
67701
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67702
|
+
}
|
|
67536
67703
|
function actionCycleKey(action) {
|
|
67537
67704
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67538
67705
|
}
|
|
@@ -67586,6 +67753,11 @@ ${statePacket}` },
|
|
|
67586
67753
|
if (actions.length < beforeContainerDrop) {
|
|
67587
67754
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
67588
67755
|
}
|
|
67756
|
+
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
67757
|
+
if (intentMismatch) {
|
|
67758
|
+
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
67759
|
+
return null;
|
|
67760
|
+
}
|
|
67589
67761
|
const floorAppended = appendHybridFloor(actions, recording, platform3);
|
|
67590
67762
|
if (floorAppended) {
|
|
67591
67763
|
log2(" appended hybrid durable-outcome floor (landing-screen ASSERT_VISIBLE).");
|
|
@@ -68283,7 +68455,8 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
68283
68455
|
2. STEP RESULTS: which steps passed before the failure.
|
|
68284
68456
|
3. TREE: does the current screen match what the failed step expects?
|
|
68285
68457
|
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.
|
|
68458
|
+
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.
|
|
68459
|
+
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
68460
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68288
68461
|
{"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
68462
|
{"verdict":"app_bug","reason":"app crashed/blank/5xx (NOT rate-limit)"}
|
|
@@ -68340,6 +68513,104 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68340
68513
|
function isDrift(error2) {
|
|
68341
68514
|
return /unexpected screen|screen drift/i.test(error2 ?? "");
|
|
68342
68515
|
}
|
|
68516
|
+
function mobileStepSearchText(step) {
|
|
68517
|
+
const rawTarget = step.target;
|
|
68518
|
+
const structuredTarget = rawTarget && typeof rawTarget === "object" ? rawTarget : void 0;
|
|
68519
|
+
const bundle = structuredTarget?.locatorBundle;
|
|
68520
|
+
const stepValue = step.value;
|
|
68521
|
+
return [
|
|
68522
|
+
step.action,
|
|
68523
|
+
step.description,
|
|
68524
|
+
typeof stepValue === "string" ? stepValue : void 0,
|
|
68525
|
+
typeof rawTarget === "string" ? rawTarget : void 0,
|
|
68526
|
+
structuredTarget?.primary,
|
|
68527
|
+
bundle?.text,
|
|
68528
|
+
bundle?.contentDesc,
|
|
68529
|
+
bundle?.accessibilityId,
|
|
68530
|
+
bundle?.resourceId,
|
|
68531
|
+
bundle?.className,
|
|
68532
|
+
bundle?.xpath
|
|
68533
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
68534
|
+
}
|
|
68535
|
+
function detectInvalidExternalAuthSetup(steps, error2) {
|
|
68536
|
+
if (!/foreground app changed|outside target app|com\.google\.android\.gms|external app/i.test(error2 ?? "")) {
|
|
68537
|
+
return null;
|
|
68538
|
+
}
|
|
68539
|
+
const hasBareLoginTap = steps.some((step) => {
|
|
68540
|
+
if (!/\btap\b/i.test(step.action))
|
|
68541
|
+
return false;
|
|
68542
|
+
const text = mobileStepSearchText(step);
|
|
68543
|
+
if (/\b(logout|log\s*out|sign\s*out)\b/.test(text))
|
|
68544
|
+
return false;
|
|
68545
|
+
return /\b(sign\s*in|signin|log\s*in|login)\b/.test(text);
|
|
68546
|
+
});
|
|
68547
|
+
if (!hasBareLoginTap)
|
|
68548
|
+
return null;
|
|
68549
|
+
const hasCredentialEntry = steps.some((step) => {
|
|
68550
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
68551
|
+
return false;
|
|
68552
|
+
const text = mobileStepSearchText(step);
|
|
68553
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68554
|
+
});
|
|
68555
|
+
if (hasCredentialEntry)
|
|
68556
|
+
return null;
|
|
68557
|
+
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";
|
|
68558
|
+
}
|
|
68559
|
+
function isCredentialFieldStep(step) {
|
|
68560
|
+
const text = mobileStepSearchText(step);
|
|
68561
|
+
return /\b(email|e-mail|username|user\s*name|password|passcode|pin|\{\{test_user_email\}\}|\{\{test_user_password\}\})\b/.test(text);
|
|
68562
|
+
}
|
|
68563
|
+
function isCredentialSubmitTap(step) {
|
|
68564
|
+
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68565
|
+
return false;
|
|
68566
|
+
const text = mobileStepSearchText(step);
|
|
68567
|
+
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)) {
|
|
68568
|
+
return false;
|
|
68569
|
+
}
|
|
68570
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit)\b/.test(text);
|
|
68571
|
+
}
|
|
68572
|
+
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68573
|
+
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
68574
|
+
return null;
|
|
68575
|
+
}
|
|
68576
|
+
const allText = steps.map(mobileStepSearchText).join(" ");
|
|
68577
|
+
if (!/\b(email|e-mail|username|user\s*name|password|passcode|pin)\b/.test(allText))
|
|
68578
|
+
return null;
|
|
68579
|
+
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);
|
|
68580
|
+
let lastCredentialEditOrder = -1;
|
|
68581
|
+
for (const step of steps) {
|
|
68582
|
+
if (!/\b(type|fill|input|enter|clear)\b/i.test(step.action))
|
|
68583
|
+
continue;
|
|
68584
|
+
if (!isCredentialFieldStep(step))
|
|
68585
|
+
continue;
|
|
68586
|
+
lastCredentialEditOrder = Math.max(lastCredentialEditOrder, step.order ?? -1);
|
|
68587
|
+
}
|
|
68588
|
+
if (lastCredentialEditOrder < 0)
|
|
68589
|
+
return null;
|
|
68590
|
+
if (uiOnlyCredentialScenario) {
|
|
68591
|
+
const hasCredentialFormEvidence = steps.some((step) => {
|
|
68592
|
+
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68593
|
+
return false;
|
|
68594
|
+
const text = mobileStepSearchText(step);
|
|
68595
|
+
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);
|
|
68596
|
+
});
|
|
68597
|
+
if (hasCredentialFormEvidence)
|
|
68598
|
+
return null;
|
|
68599
|
+
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";
|
|
68600
|
+
}
|
|
68601
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isCredentialSubmitTap(step));
|
|
68602
|
+
if (hasSubmitAfterCredentialEdit)
|
|
68603
|
+
return null;
|
|
68604
|
+
const hasValidationAssertion = steps.some((step) => {
|
|
68605
|
+
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68606
|
+
return false;
|
|
68607
|
+
const text = mobileStepSearchText(step);
|
|
68608
|
+
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);
|
|
68609
|
+
});
|
|
68610
|
+
if (hasValidationAssertion)
|
|
68611
|
+
return null;
|
|
68612
|
+
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";
|
|
68613
|
+
}
|
|
68343
68614
|
function stripCodeFences(text) {
|
|
68344
68615
|
const trimmed = text.trim();
|
|
68345
68616
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -68598,6 +68869,24 @@ ${args.directive}`);
|
|
|
68598
68869
|
const registryPresent = options.selectorRegistryContext != null;
|
|
68599
68870
|
const platform3 = target.platform;
|
|
68600
68871
|
const trimmedLogs = tailLimit(ctx.logs ?? "", MAX_LOG_CHARS);
|
|
68872
|
+
const invalidExternalAuthSetup = detectInvalidExternalAuthSetup(steps, ctx.error);
|
|
68873
|
+
if (invalidExternalAuthSetup) {
|
|
68874
|
+
return {
|
|
68875
|
+
passed: false,
|
|
68876
|
+
verdict: "test_invalid",
|
|
68877
|
+
healReason: invalidExternalAuthSetup,
|
|
68878
|
+
attemptsUsed: 0
|
|
68879
|
+
};
|
|
68880
|
+
}
|
|
68881
|
+
const invalidAuthValidationSetup = detectInvalidAuthValidationSetup(steps, ctx.error);
|
|
68882
|
+
if (invalidAuthValidationSetup) {
|
|
68883
|
+
return {
|
|
68884
|
+
passed: false,
|
|
68885
|
+
verdict: "test_invalid",
|
|
68886
|
+
healReason: invalidAuthValidationSetup,
|
|
68887
|
+
attemptsUsed: 0
|
|
68888
|
+
};
|
|
68889
|
+
}
|
|
68601
68890
|
let attemptCap = base2;
|
|
68602
68891
|
let recoverableUsed = 0;
|
|
68603
68892
|
let bonusUsed = 0;
|
|
@@ -68783,7 +69072,7 @@ ${args.directive}`);
|
|
|
68783
69072
|
removeTags: script.removeTags
|
|
68784
69073
|
};
|
|
68785
69074
|
}
|
|
68786
|
-
var DEFAULT_PHASE2_MAX_PASSES =
|
|
69075
|
+
var DEFAULT_PHASE2_MAX_PASSES = 4;
|
|
68787
69076
|
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
69077
|
## Key insight \u2014 ENTRY-SCREEN DRIFT
|
|
68789
69078
|
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 +69081,8 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
68792
69081
|
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
69082
|
3. If a locator is simply wrong, swap it for a stronger identity locator present in the LIVE tree.
|
|
68794
69083
|
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
|
|
69084
|
+
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.
|
|
69085
|
+
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
69086
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
68797
69087
|
{"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
69088
|
{"verdict":"observe","action":{"type":"tap","target":"mobile.text('<exact label visible in the live tree>')"}}
|
|
@@ -329894,7 +330184,7 @@ var require_package4 = __commonJS({
|
|
|
329894
330184
|
"package.json"(exports, module) {
|
|
329895
330185
|
module.exports = {
|
|
329896
330186
|
name: "@validate.qa/runner",
|
|
329897
|
-
version: "1.0.
|
|
330187
|
+
version: "1.0.31",
|
|
329898
330188
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
329899
330189
|
bin: {
|
|
329900
330190
|
"validate-runner": "dist/cli.js"
|
|
@@ -330043,6 +330333,8 @@ var MAX_RETRIES = 3;
|
|
|
330043
330333
|
var STARTUP_TIMEOUT_MS = 3e4;
|
|
330044
330334
|
var HEALTH_CHECK_INTERVAL_MS = 5e3;
|
|
330045
330335
|
var STABLE_AFTER_MS = 6e4;
|
|
330336
|
+
var DEFAULT_APPIUM_LOG_LEVEL = "warn";
|
|
330337
|
+
var MAX_APPIUM_LOG_LINE_CHARS = 4e3;
|
|
330046
330338
|
var appiumProcess = null;
|
|
330047
330339
|
var serverState = {
|
|
330048
330340
|
running: false,
|
|
@@ -330102,6 +330394,23 @@ function appendLog(line) {
|
|
|
330102
330394
|
logs = logs.slice(-MAX_LOG_LINES);
|
|
330103
330395
|
}
|
|
330104
330396
|
}
|
|
330397
|
+
function resolveAppiumLogLevel() {
|
|
330398
|
+
const raw = process.env.APPIUM_LOG_LEVEL?.trim();
|
|
330399
|
+
if (raw && /^[a-z]+(?::[a-z]+)?$/i.test(raw)) return raw;
|
|
330400
|
+
return DEFAULT_APPIUM_LOG_LEVEL;
|
|
330401
|
+
}
|
|
330402
|
+
function buildAppiumArgs(port, drivers) {
|
|
330403
|
+
const args = ["--port", String(port), "--log-level", resolveAppiumLogLevel()];
|
|
330404
|
+
if (drivers && drivers.length > 0) {
|
|
330405
|
+
args.push("--use-drivers", drivers.join(","));
|
|
330406
|
+
}
|
|
330407
|
+
return args;
|
|
330408
|
+
}
|
|
330409
|
+
function sanitizeAppiumLogLine(line) {
|
|
330410
|
+
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]"');
|
|
330411
|
+
if (redacted.length <= MAX_APPIUM_LOG_LINE_CHARS) return redacted;
|
|
330412
|
+
return `${redacted.slice(0, MAX_APPIUM_LOG_LINE_CHARS)}\u2026 [truncated ${redacted.length - MAX_APPIUM_LOG_LINE_CHARS} chars]`;
|
|
330413
|
+
}
|
|
330105
330414
|
async function checkAppiumHealth(port = serverState.port) {
|
|
330106
330415
|
try {
|
|
330107
330416
|
const controller = new AbortController();
|
|
@@ -330134,10 +330443,7 @@ async function startAppiumServer(options) {
|
|
|
330134
330443
|
return serverState;
|
|
330135
330444
|
}
|
|
330136
330445
|
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
|
-
}
|
|
330446
|
+
const args = buildAppiumArgs(port, options?.drivers);
|
|
330141
330447
|
intentionalStop = false;
|
|
330142
330448
|
lastStartOptions = { ...lastStartOptions, ...options };
|
|
330143
330449
|
if (appiumGaveUp) {
|
|
@@ -330156,14 +330462,14 @@ async function startAppiumServer(options) {
|
|
|
330156
330462
|
serverState.pid = appiumProcess.pid ?? null;
|
|
330157
330463
|
serverState.port = port;
|
|
330158
330464
|
appiumProcess.stdout?.on("data", (data) => {
|
|
330159
|
-
const line = data.toString().trim();
|
|
330465
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330160
330466
|
if (line) {
|
|
330161
330467
|
appendLog(`[appium:stdout] ${line}`);
|
|
330162
330468
|
options?.onLog?.(line);
|
|
330163
330469
|
}
|
|
330164
330470
|
});
|
|
330165
330471
|
appiumProcess.stderr?.on("data", (data) => {
|
|
330166
|
-
const line = data.toString().trim();
|
|
330472
|
+
const line = sanitizeAppiumLogLine(data.toString().trim());
|
|
330167
330473
|
if (line) {
|
|
330168
330474
|
appendLog(`[appium:stderr] ${line}`);
|
|
330169
330475
|
options?.onLog?.(line);
|
|
@@ -330322,6 +330628,17 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
330322
330628
|
// src/services/mobile/appium-actions.ts
|
|
330323
330629
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
330324
330630
|
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
330631
|
+
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;
|
|
330632
|
+
var AppiumDriverDeadError = class extends Error {
|
|
330633
|
+
isAppiumDriverDead = true;
|
|
330634
|
+
constructor(message) {
|
|
330635
|
+
super(message);
|
|
330636
|
+
this.name = "AppiumDriverDeadError";
|
|
330637
|
+
}
|
|
330638
|
+
};
|
|
330639
|
+
function isAppiumDriverDeadError(err) {
|
|
330640
|
+
return err instanceof AppiumDriverDeadError || typeof err === "object" && err !== null && err.isAppiumDriverDead === true;
|
|
330641
|
+
}
|
|
330325
330642
|
async function appiumRequest(path, options) {
|
|
330326
330643
|
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
330327
330644
|
const res = await fetch(url, {
|
|
@@ -330331,6 +330648,9 @@ async function appiumRequest(path, options) {
|
|
|
330331
330648
|
});
|
|
330332
330649
|
if (!res.ok) {
|
|
330333
330650
|
const body = await res.text().catch(() => "");
|
|
330651
|
+
if (APPIUM_DRIVER_DEAD_RE.test(body)) {
|
|
330652
|
+
throw new AppiumDriverDeadError(`Appium driver session is not usable (${res.status}): ${body.slice(0, 200)}`);
|
|
330653
|
+
}
|
|
330334
330654
|
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
330335
330655
|
}
|
|
330336
330656
|
return res.json();
|
|
@@ -330345,7 +330665,8 @@ async function getScreenshot(sessionPath) {
|
|
|
330345
330665
|
try {
|
|
330346
330666
|
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
330347
330667
|
return typeof value === "string" ? value : null;
|
|
330348
|
-
} catch {
|
|
330668
|
+
} catch (err) {
|
|
330669
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330349
330670
|
return null;
|
|
330350
330671
|
}
|
|
330351
330672
|
}
|
|
@@ -330353,7 +330674,8 @@ async function getPageSource(sessionPath) {
|
|
|
330353
330674
|
try {
|
|
330354
330675
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
330355
330676
|
return typeof value === "string" ? value : null;
|
|
330356
|
-
} catch {
|
|
330677
|
+
} catch (err) {
|
|
330678
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330357
330679
|
return null;
|
|
330358
330680
|
}
|
|
330359
330681
|
}
|
|
@@ -330369,7 +330691,8 @@ async function getWindowSize(sessionPath) {
|
|
|
330369
330691
|
return { width: w, height: h };
|
|
330370
330692
|
}
|
|
330371
330693
|
}
|
|
330372
|
-
} catch {
|
|
330694
|
+
} catch (err) {
|
|
330695
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
330373
330696
|
}
|
|
330374
330697
|
if (attempt < retries - 1) {
|
|
330375
330698
|
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
@@ -331713,6 +332036,7 @@ var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
|
331713
332036
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
331714
332037
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
331715
332038
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
332039
|
+
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
331716
332040
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
331717
332041
|
var execFileAsync2 = promisify2(execFile2);
|
|
331718
332042
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
@@ -331749,6 +332073,9 @@ async function wdRequest(path, options) {
|
|
|
331749
332073
|
if (res.status === 404 && /no such session|invalid session id|session is either terminated|deleted session/i.test(body)) {
|
|
331750
332074
|
throw new WebDriverTransportError(`WebDriver session is no longer available: ${body.slice(0, 200)}`);
|
|
331751
332075
|
}
|
|
332076
|
+
if (res.status === 500 && APPIUM_DRIVER_DEAD_RE2.test(body)) {
|
|
332077
|
+
throw new WebDriverTransportError(`WebDriver driver process is not usable: ${body.slice(0, 200)}`);
|
|
332078
|
+
}
|
|
331752
332079
|
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
331753
332080
|
throw new WebDriverTransportError(`Appium server unavailable (${res.status}): ${body.slice(0, 200)}`);
|
|
331754
332081
|
}
|
|
@@ -336706,7 +337033,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
336706
337033
|
if (getBridgeState().hasActiveSession) {
|
|
336707
337034
|
throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
|
|
336708
337035
|
}
|
|
336709
|
-
const
|
|
337036
|
+
const mobileProgressLines = [];
|
|
337037
|
+
const mobileOnLog = (line) => {
|
|
337038
|
+
mobileProgressLines.push(line);
|
|
337039
|
+
if (mobileProgressLines.length > 500) {
|
|
337040
|
+
mobileProgressLines.splice(0, mobileProgressLines.length - 500);
|
|
337041
|
+
}
|
|
337042
|
+
onLog("progress", mobileProgressLines.join("\n"));
|
|
337043
|
+
};
|
|
336710
337044
|
liveBrowserHandle.patch({
|
|
336711
337045
|
mode: ctx.mode === "survey" ? "survey" : "discovery",
|
|
336712
337046
|
testName: ctx.featureName ?? "Mobile discovery",
|
package/package.json
CHANGED