@validate.qa/runner 1.0.32 → 1.0.34
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 +693 -178
- package/dist/cli.mjs +693 -178
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -62290,7 +62290,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
62290
62290
|
return { screen, signal, refMap, observation };
|
|
62291
62291
|
}
|
|
62292
62292
|
function buildStatePacket(input) {
|
|
62293
|
-
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
|
|
62293
|
+
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
|
|
62294
62294
|
const sections = [];
|
|
62295
62295
|
sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
|
|
62296
62296
|
sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
|
|
@@ -62303,10 +62303,46 @@ var require_mobile_explorer = __commonJS({
|
|
|
62303
62303
|
sections.push("");
|
|
62304
62304
|
if (mode === "survey") {
|
|
62305
62305
|
sections.push("**Mission:** Map every reachable screen. For each new screen: mobile_snapshot -> capture_screen -> record_transition. Get PAST the entry gate into the app's real main content \u2014 the login/landing screens are the ENTRANCE, not the app. If test credentials are provided (see the Test Credentials section), SIGN IN with them FIRST and map the authenticated app; that is the real product surface. Prefer a real login when credentials are provided; only if login genuinely cannot be completed after honest attempts (persistent error, the form will not accept the credential, no valid account) fall back to any guest/demo/skip entry the app offers and map the app fully from there \u2014 reaching the real feature surface that way is far better coverage than getting stuck on the login wall. Do NOT submit forms with junk data or mutate real data. Call finish_exploration ONLY when every reachable screen \u2014 including the authenticated content \u2014 has been captured and no unvisited frontier screens remain.");
|
|
62306
|
+
} else if (featureName?.trim()) {
|
|
62307
|
+
sections.push(`**Mission:** Feature-scoped deep exploration of **"${featureName.trim()}"**. Drive the app like a real user through THIS feature only: tap, type, submit, and observe outcomes. After a meaningful flow, call record_journey. Do NOT deep-dive unrelated features. Call finish_exploration when this feature's flows are covered.`);
|
|
62306
62308
|
} else {
|
|
62307
62309
|
sections.push("**Mission:** Use the focus features like a real user. Tap, type, submit, and observe outcomes. After a meaningful flow, call record_journey. Call finish_exploration when the flows are covered.");
|
|
62308
62310
|
}
|
|
62309
62311
|
sections.push("");
|
|
62312
|
+
if (mode === "deep" && featureName?.trim()) {
|
|
62313
|
+
sections.push(`## Feature: ${featureName.trim()}`);
|
|
62314
|
+
if (featureContext?.plannerDescription?.trim()) {
|
|
62315
|
+
sections.push(featureContext.plannerDescription.trim());
|
|
62316
|
+
}
|
|
62317
|
+
if (featureContext?.priorInsight?.trim()) {
|
|
62318
|
+
sections.push(`Prior insight: ${featureContext.priorInsight.trim()}`);
|
|
62319
|
+
}
|
|
62320
|
+
if (featureContext?.pageIntel && featureContext.pageIntel.length > 0) {
|
|
62321
|
+
sections.push("### Target screens / routes for this feature");
|
|
62322
|
+
for (const page of featureContext.pageIntel.slice(0, 12)) {
|
|
62323
|
+
sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
|
|
62324
|
+
}
|
|
62325
|
+
}
|
|
62326
|
+
if (featureContext?.flowIntel && featureContext.flowIntel.length > 0) {
|
|
62327
|
+
sections.push("### Known flows");
|
|
62328
|
+
for (const flow of featureContext.flowIntel.slice(0, 8)) {
|
|
62329
|
+
sections.push(`- ${flow}`);
|
|
62330
|
+
}
|
|
62331
|
+
}
|
|
62332
|
+
if (featureContext?.otherFeatures && featureContext.otherFeatures.length > 0) {
|
|
62333
|
+
sections.push("### Other features \u2014 do NOT explore these (navigation context only)");
|
|
62334
|
+
for (const other of featureContext.otherFeatures.slice(0, 12)) {
|
|
62335
|
+
const pages = other.pages?.length ? ` [${other.pages.slice(0, 4).join(", ")}]` : "";
|
|
62336
|
+
sections.push(`- ${other.name}${pages}`);
|
|
62337
|
+
}
|
|
62338
|
+
}
|
|
62339
|
+
sections.push("");
|
|
62340
|
+
}
|
|
62341
|
+
if (userNotes?.trim()) {
|
|
62342
|
+
sections.push(`## Operator notes
|
|
62343
|
+
${userNotes.trim()}`);
|
|
62344
|
+
sections.push("");
|
|
62345
|
+
}
|
|
62310
62346
|
const screenCount = state2.getScreenCount();
|
|
62311
62347
|
sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
|
|
62312
62348
|
const currentScreenId = state2.getCurrentScreenId();
|
|
@@ -62463,7 +62499,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62463
62499
|
} catch {
|
|
62464
62500
|
}
|
|
62465
62501
|
};
|
|
62466
|
-
const mode = ctx.mode === "
|
|
62502
|
+
const mode = ctx.mode === "survey" ? "survey" : "deep";
|
|
62467
62503
|
const platform3 = platformForMedium(ctx.medium);
|
|
62468
62504
|
const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
|
|
62469
62505
|
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
@@ -62751,7 +62787,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62751
62787
|
maxIterations,
|
|
62752
62788
|
latestObservation: getLatest()?.observation ?? null,
|
|
62753
62789
|
journeys,
|
|
62754
|
-
transitions: collectedTransitions
|
|
62790
|
+
transitions: collectedTransitions,
|
|
62791
|
+
featureName: ctx.featureName,
|
|
62792
|
+
featureContext: ctx.featureContext,
|
|
62793
|
+
userNotes: ctx.userNotes
|
|
62755
62794
|
});
|
|
62756
62795
|
const messages = [
|
|
62757
62796
|
{ role: "system", content: systemPrompt },
|
|
@@ -65129,7 +65168,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65129
65168
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
65130
65169
|
Example - GOOD happy path (generic, async): \`["Navigate to landing and log in", "Navigate to the feature page", "Note the current count of items on the list", "Click the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the item count increased by 1", "Open the newly created item", "Verify its content is non-empty and contains the input I provided"]\`
|
|
65131
65170
|
Example - GOOD happy path (sync CRUD): \`["Navigate to landing and log in", "Navigate to the list page", "Click New", "Fill required fields", "Click Save", "Verify the new record appears in the list", "Click the record and verify its detail view shows the values I entered"]\`
|
|
65132
|
-
12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "
|
|
65171
|
+
12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "Sign out", "Account", "Profile", or the user's own displayed name/email from evidence) over generic phrases like "personalized content." If the explorer only observed a redirect and no signed-in signal, keep the route outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
|
|
65133
65172
|
13. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and in parallel against an environment with no relevant feature data except explicitly guaranteed immutable fixtures. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
|
|
65134
65173
|
14. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation in the demo account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the dashboard heading is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Click New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
|
|
65135
65174
|
15. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first item", "the latest message", "the last row", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs in a fresh environment or against a different account.
|
|
@@ -66089,7 +66128,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
66089
66128
|
9. **No duplicates** - check existing test names before adding
|
|
66090
66129
|
10. **All tests start from the app launch entry** - every generated test starts by launching the target app. The runner clears app data when the device/reset policy safely supports it; otherwise it performs a terminate + activate relaunch, which can preserve app session data. Therefore scenarios MUST navigate through the app's own UI from whatever launch state is valid, and MUST NOT rely on mutable data from a prior scenario disappearing. Steps must reflect this: say "Launch the app and tap Sign In" not "deep-link to the login screen". Do NOT navigate by deep-link; the app's launch + tab/drawer navigation is the only entry path. \`targetPages\` lists the screen-ids involved in the flow.
|
|
66091
66130
|
11. **targetPages must be copied CHARACTER-FOR-CHARACTER from the screen-ids in the evidence** (they look like \`com.example.app.MainActivity#ab12cd34\`). NEVER invent, prettify, or abbreviate them \u2014 no "Landing", "AUTH", "Home Screen", or any name that is not literally in the provided screens. A scenario whose targetPages contains even ONE id not present in the evidence is REJECTED WHOLESALE by the validator, so a single made-up route silently destroys the entire scenario. If you are unsure which screen-id a step touches, use the screen-id of the screen where the flow STARTS.
|
|
66092
|
-
12. **Happy paths must verify the DURABLE OUTCOME, not just the trigger.** A HAPPY_PATH is NOT complete until it verifies the feature's real output. "Button tapped" / "Sheet opened" / "Spinner appeared" is NOT a happy path - that is a UI interaction test. Apply this to any feature where an action produces a result the user then sees or uses: CRUD creates, photo/file uploads, searches that return results, imports, exports, payments, messages sent, AI/ML outputs, generated documents, sync runs, analysis runs, conversion, data processing. The scenario MUST include:
|
|
66131
|
+
12. **Happy paths must verify the DURABLE OUTCOME, not just the trigger.** A HAPPY_PATH is NOT complete until it verifies the feature's real output. "Button tapped" / "Sheet opened" / "Spinner appeared" / "Settings screen opened" is NOT a happy path - that is a UI interaction test. Form-mutation happy paths (change password, create/edit/save a record, send a message, checkout, etc.) MUST include typing into the relevant fields AND committing the form (submit/save/send or keyboard Enter/Done) \u2014 navigating to the form screen alone is insufficient. Apply this to any feature where an action produces a result the user then sees or uses: CRUD creates, photo/file uploads, searches that return results, imports, exports, payments, messages sent, AI/ML outputs, generated documents, sync runs, analysis runs, conversion, data processing. The scenario MUST include:
|
|
66093
66132
|
- The trigger step (Tap Submit / Generate / Send / Upload / Run / Analyze / Save / Pay / etc.)
|
|
66094
66133
|
- A wait step for the operation to complete. Phrase it by SIGNAL, not wall-clock ("Wait for the result to appear - up to 60 seconds"). Prefer signals the app actually uses: spinner disappears, status badge flips to a success state, a new row appears, an item count increments, a success toast/snackbar shows, the screen transitions to a result screen.
|
|
66095
66134
|
- A verification step on the DURABLE RESULT - the thing that outlives the transient UI: a new list row, a persisted record, a received message, a committed state change, visible output text. Do NOT assert on the spinner disappearing alone; assert on what replaced it.
|
|
@@ -66320,6 +66359,34 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66320
66359
|
if (context.featureName) {
|
|
66321
66360
|
sections.push(`## Feature: ${context.featureName}
|
|
66322
66361
|
SCOPE: this is a feature-focused deep run. Plan scenarios ONLY for the "${context.featureName}" feature \u2014 its screens, forms, and flows. Screens belonging to other features are navigation context / prerequisites only; do NOT plan scenarios whose primary goal exercises another feature.`);
|
|
66362
|
+
if (context.featureContext?.plannerDescription?.trim()) {
|
|
66363
|
+
sections.push(context.featureContext.plannerDescription.trim());
|
|
66364
|
+
}
|
|
66365
|
+
if (context.featureContext?.priorInsight?.trim()) {
|
|
66366
|
+
sections.push(`Prior insight: ${context.featureContext.priorInsight.trim()}`);
|
|
66367
|
+
}
|
|
66368
|
+
if (context.featureContext?.pageIntel && context.featureContext.pageIntel.length > 0) {
|
|
66369
|
+
sections.push("### Feature page intel");
|
|
66370
|
+
for (const page of context.featureContext.pageIntel.slice(0, 12)) {
|
|
66371
|
+
sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
|
|
66372
|
+
}
|
|
66373
|
+
}
|
|
66374
|
+
if (context.featureContext?.flowIntel && context.featureContext.flowIntel.length > 0) {
|
|
66375
|
+
sections.push("### Known flows");
|
|
66376
|
+
for (const flow of context.featureContext.flowIntel.slice(0, 8)) {
|
|
66377
|
+
sections.push(`- ${flow}`);
|
|
66378
|
+
}
|
|
66379
|
+
}
|
|
66380
|
+
if (context.featureContext?.otherFeatures && context.featureContext.otherFeatures.length > 0) {
|
|
66381
|
+
sections.push("### Other features (do not plan primary scenarios for these)");
|
|
66382
|
+
for (const other of context.featureContext.otherFeatures.slice(0, 12)) {
|
|
66383
|
+
sections.push(`- ${other.name}`);
|
|
66384
|
+
}
|
|
66385
|
+
}
|
|
66386
|
+
}
|
|
66387
|
+
if (context.userNotes?.trim()) {
|
|
66388
|
+
sections.push(`## Operator notes
|
|
66389
|
+
${context.userNotes.trim()}`);
|
|
66323
66390
|
}
|
|
66324
66391
|
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66325
66392
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
@@ -66446,7 +66513,7 @@ ${constraints.join("\n")}`);
|
|
|
66446
66513
|
routePattern: null,
|
|
66447
66514
|
title: state2.screenName ?? null,
|
|
66448
66515
|
pageType: state2.screenType ?? "UNKNOWN",
|
|
66449
|
-
requiresAuth:
|
|
66516
|
+
requiresAuth: state2.screenType?.toLowerCase() === "auth",
|
|
66450
66517
|
visitCount: state2.visited ? 1 : 0,
|
|
66451
66518
|
lastVisitedAt: state2.visited ? now : null,
|
|
66452
66519
|
elementsCovered: 0,
|
|
@@ -66466,6 +66533,8 @@ ${constraints.join("\n")}`);
|
|
|
66466
66533
|
const toPageId = idByScreen.get(t.toScreenId);
|
|
66467
66534
|
if (!fromPageId || !toPageId)
|
|
66468
66535
|
continue;
|
|
66536
|
+
const fromScreen = graph.screens.get(t.fromScreenId);
|
|
66537
|
+
const toScreen = graph.screens.get(t.toScreenId);
|
|
66469
66538
|
transitions.push({
|
|
66470
66539
|
id: `mtx-${txSeq++}`,
|
|
66471
66540
|
fromPageId,
|
|
@@ -66473,7 +66542,7 @@ ${constraints.join("\n")}`);
|
|
|
66473
66542
|
triggerType: t.action.toUpperCase(),
|
|
66474
66543
|
triggerLabel: t.viaRef ?? null,
|
|
66475
66544
|
observedCount: 1,
|
|
66476
|
-
requiresAuth:
|
|
66545
|
+
requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
|
|
66477
66546
|
});
|
|
66478
66547
|
}
|
|
66479
66548
|
return {
|
|
@@ -66676,7 +66745,9 @@ ${constraints.join("\n")}`);
|
|
|
66676
66745
|
recordedJourneys,
|
|
66677
66746
|
credentials: input.credentials,
|
|
66678
66747
|
testBudget: input.testBudget,
|
|
66679
|
-
incremental: input.incremental
|
|
66748
|
+
incremental: input.incremental,
|
|
66749
|
+
userNotes: input.userNotes,
|
|
66750
|
+
featureContext: input.featureContext
|
|
66680
66751
|
})
|
|
66681
66752
|
});
|
|
66682
66753
|
const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
|
|
@@ -66728,6 +66799,9 @@ var require_mobile_generator = __commonJS({
|
|
|
66728
66799
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
66729
66800
|
"use strict";
|
|
66730
66801
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66802
|
+
exports.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
66803
|
+
exports.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
66804
|
+
exports.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
66731
66805
|
exports.collapseRetryCycles = collapseRetryCycles;
|
|
66732
66806
|
exports.runMobileGeneratePhase = runMobileGeneratePhase;
|
|
66733
66807
|
var crypto_1 = __require("crypto");
|
|
@@ -66935,8 +67009,38 @@ var require_mobile_generator = __commonJS({
|
|
|
66935
67009
|
return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
|
|
66936
67010
|
}
|
|
66937
67011
|
var EDITABLE_ELEMENT_ROLE_RE = /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/;
|
|
67012
|
+
var EDITABLE_IDENTITY_RE = /\b(email|e-?mail|password|passwd|passcode|username|user[_-]?name|login|search|query|otp|pin|phone|mobile|input|textfield|text[_-]?field|edit|typehere)\b/i;
|
|
66938
67013
|
function isEditableMobileElement(element) {
|
|
66939
|
-
|
|
67014
|
+
if (EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim()))
|
|
67015
|
+
return true;
|
|
67016
|
+
const identity = [element.accessibilityId, element.resourceId, element.label].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
67017
|
+
if (identity && EDITABLE_IDENTITY_RE.test(identity))
|
|
67018
|
+
return true;
|
|
67019
|
+
return false;
|
|
67020
|
+
}
|
|
67021
|
+
function isIdentityBearingTapTarget(element) {
|
|
67022
|
+
if (weakAssertionTargetReason(element))
|
|
67023
|
+
return false;
|
|
67024
|
+
if (isBareContainerLikeElement(element))
|
|
67025
|
+
return false;
|
|
67026
|
+
if (isEditableMobileElement(element))
|
|
67027
|
+
return false;
|
|
67028
|
+
if (/button|link|tab|menuitem|checkbox|switch|radio/i.test(element.role))
|
|
67029
|
+
return true;
|
|
67030
|
+
const label = [
|
|
67031
|
+
element.label,
|
|
67032
|
+
element.text,
|
|
67033
|
+
element.accessibilityId,
|
|
67034
|
+
element.resourceId
|
|
67035
|
+
].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
67036
|
+
return /\b(sign\s*in|log\s*in|login|submit|save|continue|create|send|confirm|done|next|apply|checkout|pay|finish|update|register|sign\s*up)\b/i.test(label);
|
|
67037
|
+
}
|
|
67038
|
+
function isBareContainerLikeElement(element) {
|
|
67039
|
+
if (BARE_CONTAINER_CLASS_RE.test(element.role)) {
|
|
67040
|
+
const hasIdentity = Boolean(nonEmpty2(element.text) || nonEmpty2(element.label) || nonEmpty2(element.accessibilityId));
|
|
67041
|
+
return !hasIdentity;
|
|
67042
|
+
}
|
|
67043
|
+
return false;
|
|
66940
67044
|
}
|
|
66941
67045
|
function nonEmpty2(value) {
|
|
66942
67046
|
if (typeof value !== "string")
|
|
@@ -66968,7 +67072,7 @@ var require_mobile_generator = __commonJS({
|
|
|
66968
67072
|
## Your job
|
|
66969
67073
|
1. mobile_snapshot the current screen.
|
|
66970
67074
|
2. Execute the scenario's steps using the real UI: tap buttons, fill fields (mobile_type), submit, navigate.
|
|
66971
|
-
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way. If the scenario submits a form (sign in, sign up, search, create), you MUST tap the submit/primary button
|
|
67075
|
+
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way. If the scenario submits a form (sign in, sign up, search, create, change password, edit profile), you MUST commit it \u2014 tap the submit/primary button OR press the keyboard's Enter/Done action on the last field (mobile_type with a trailing newline on the focused field when no button is visible). Filling fields alone is NOT the behavior under test. Navigating to a form screen without changing its fields is also NOT enough for mutation scenarios (change/create/update/edit). If it navigates, you MUST land on the destination. A "happy path" that fills a form but never commits \u2014 or only opens the form without typing \u2014 is WRONG.
|
|
66972
67076
|
4. When you reach the target screen / complete the flow, PROVE the outcome with an assert tool:
|
|
66973
67077
|
- mobile_assert_visible({ref}) \u2014 the target screen's stable element is on screen.
|
|
66974
67078
|
- mobile_assert_text({ref, expected}) \u2014 an element contains expected text (only durable when the element has a stable accessibility-id / resource-id).
|
|
@@ -66983,6 +67087,8 @@ var require_mobile_generator = __commonJS({
|
|
|
66983
67087
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
66984
67088
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
66985
67089
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
67090
|
+
- Do NOT thrash the system Back button. One intentional back/navigation is fine; a chain of backs while lost is not a test \u2014 re-snapshot, re-orient, and take the productive path once.
|
|
67091
|
+
- Prefer product identity locators (accessibility-id, resource-id, visible label). Never assert on bare hierarchy roots or unlabeled full-screen containers \u2014 those prove nothing.
|
|
66986
67092
|
- Do ONLY the actions this scenario needs. Do NOT re-open a menu you already opened, re-select an option that is already selected, toggle a setting back and forth, or repeat a step that already succeeded \u2014 EVERY action you take is baked into the generated test verbatim, so each extra action becomes a permanent junk step.
|
|
66987
67093
|
- Each action must make NEW progress toward the goal. If an action leaves the screen unchanged, do NOT repeat it \u2014 pick a different element or move on.
|
|
66988
67094
|
- Finish promptly once the outcome is proven: the instant the expected outcome is visible, assert it and call finish_generate. Do not keep exploring after the goal is proven.
|
|
@@ -67001,7 +67107,7 @@ This app has stored login credentials under these keys:
|
|
|
67001
67107
|
${keyList}
|
|
67002
67108
|
|
|
67003
67109
|
When a scenario needs a signed-in session, complete the FULL login sequence \u2014 filling the fields alone does NOT log you in:
|
|
67004
|
-
1. Reach the email/password form. 2. Fill each credential field with \`mobile_fill_credential\` (pass the field ref and the KEY, e.g. \`{ ref: "e5", credentialKey: "TEST_USER_EMAIL" }\`) \u2014 the runner types the real value on the device and the generated test stores a \`{{KEY}}\` placeholder that replay resolves; you never see the secret. 3.
|
|
67110
|
+
1. Reach the email/password form. 2. Fill each credential field with \`mobile_fill_credential\` (pass the field ref and the KEY, e.g. \`{ ref: "e5", credentialKey: "TEST_USER_EMAIL" }\`) \u2014 the runner types the real value on the device and the generated test stores a \`{{KEY}}\` placeholder that replay resolves; you never see the secret. 3. COMMIT the form \u2014 tap the submit/primary button (Sign In / Log In / Continue) OR, if no button is visible, focus the last field and \`mobile_type\` a trailing newline (\`\\n\`) to trigger the keyboard's Enter/Done action. 4. Re-snapshot and VERIFY an authenticated screen appeared. If login fails twice, do NOT keep retrying or wander: call finish_generate with verified:false and say the login could not be completed (the credentials may be invalid for this app).
|
|
67005
67111
|
|
|
67006
67112
|
SECURITY RULES:
|
|
67007
67113
|
- NEVER call \`mobile_type\` with a real email, password, or token value \u2014 the runner will REJECT the call and you lose an iteration. Use \`mobile_fill_credential\` for every credential field.
|
|
@@ -67351,6 +67457,7 @@ ${statePacket}` },
|
|
|
67351
67457
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
67352
67458
|
assertCount++;
|
|
67353
67459
|
},
|
|
67460
|
+
recordedActions: actions,
|
|
67354
67461
|
log: log2
|
|
67355
67462
|
});
|
|
67356
67463
|
pushResult(dispatchResult);
|
|
@@ -67366,7 +67473,7 @@ ${statePacket}` },
|
|
|
67366
67473
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
67367
67474
|
}
|
|
67368
67475
|
async function dispatchGenerateTool(args) {
|
|
67369
|
-
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, log: log2 } = args;
|
|
67476
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
|
|
67370
67477
|
const unknownRef = (ref) => ({
|
|
67371
67478
|
ok: false,
|
|
67372
67479
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -67428,12 +67535,26 @@ ${statePacket}` },
|
|
|
67428
67535
|
};
|
|
67429
67536
|
}
|
|
67430
67537
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67431
|
-
|
|
67432
|
-
|
|
67538
|
+
const screenChanged = changedScreen(before, absorbed);
|
|
67539
|
+
const identityTap = isIdentityBearingTapTarget(element);
|
|
67540
|
+
if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
|
|
67541
|
+
const tapAction = {
|
|
67433
67542
|
type: "TAP",
|
|
67434
67543
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
67435
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
67436
|
-
|
|
67544
|
+
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
67545
|
+
innerText: null,
|
|
67546
|
+
url: "",
|
|
67547
|
+
value: null
|
|
67548
|
+
};
|
|
67549
|
+
const lastRecorded = recordedActions[recordedActions.length - 1];
|
|
67550
|
+
if (lastRecorded && actionCycleKey(lastRecorded) === actionCycleKey(tapAction)) {
|
|
67551
|
+
log2(` tap left content signature unchanged but duplicates the prior step \u2014 not recording again.`);
|
|
67552
|
+
} else if (!screenChanged && identityTap) {
|
|
67553
|
+
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67554
|
+
recordAction(tapAction);
|
|
67555
|
+
} else {
|
|
67556
|
+
recordAction(tapAction);
|
|
67557
|
+
}
|
|
67437
67558
|
} else if (shouldRecordAction(result, absorbed)) {
|
|
67438
67559
|
log2(" tap changed nothing on screen \u2014 not recording it as a step (dead action).");
|
|
67439
67560
|
}
|
|
@@ -67572,6 +67693,13 @@ ${statePacket}` },
|
|
|
67572
67693
|
const element = resolveRef(toolArgs.ref);
|
|
67573
67694
|
if (!element)
|
|
67574
67695
|
return unknownRef(toolArgs.ref);
|
|
67696
|
+
const weakReason = weakAssertionTargetReason(element);
|
|
67697
|
+
if (weakReason) {
|
|
67698
|
+
return {
|
|
67699
|
+
ok: false,
|
|
67700
|
+
error: `Ref ${element.ref} is not a durable assertion target (${weakReason}). Re-snapshot and assert a human-visible label, text, or stable control that proves the screen outcome.`
|
|
67701
|
+
};
|
|
67702
|
+
}
|
|
67575
67703
|
const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
|
|
67576
67704
|
if (!expected)
|
|
67577
67705
|
return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
|
|
@@ -67770,6 +67898,105 @@ ${statePacket}` },
|
|
|
67770
67898
|
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67771
67899
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67772
67900
|
}
|
|
67901
|
+
var INSPECTION_OUTCOME_TYPES = /* @__PURE__ */ new Set([
|
|
67902
|
+
"element-visible",
|
|
67903
|
+
"text-visible",
|
|
67904
|
+
"element-hidden",
|
|
67905
|
+
"screen-visible"
|
|
67906
|
+
]);
|
|
67907
|
+
function isInspectionOnlyScenario(scenario, actions) {
|
|
67908
|
+
if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
|
|
67909
|
+
return false;
|
|
67910
|
+
if (!scenario.expectedOutcomes.every((o) => INSPECTION_OUTCOME_TYPES.has(o.type))) {
|
|
67911
|
+
return false;
|
|
67912
|
+
}
|
|
67913
|
+
if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
|
|
67914
|
+
return false;
|
|
67915
|
+
}
|
|
67916
|
+
const text = scenarioText(scenario);
|
|
67917
|
+
const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|options?|provider\s+buttons?)\b/.test(text) || /\b(show|display)\s+(all|the)?\s*(provider|option|button|link|entry)/.test(text) || /\b(all\s+(three\s+)?providers?)\b/.test(text)) && !/\b(change|update|create|save|send|register|edit|with\s+valid|successful\s+(sign|log)|complete\s+(the\s+)?(login|sign[- ]?in)|submit\s+(the\s+)?(form|credentials)|enter\s+(an?\s+)?(email|password))\b/.test(text);
|
|
67918
|
+
if (visibilityOnlyGoal) {
|
|
67919
|
+
const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67920
|
+
return assertCount2 > 0;
|
|
67921
|
+
}
|
|
67922
|
+
const completionIntent = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|with\s+valid|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout|purchase))\b/.test(text);
|
|
67923
|
+
const formIntent = /\b(sign\s*in|log\s*in|login|authenticate|register|sign\s*up|checkout|create|save|submit|send|change|update|edit)\b/.test(text);
|
|
67924
|
+
if (completionIntent && formIntent)
|
|
67925
|
+
return false;
|
|
67926
|
+
const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67927
|
+
return assertCount > 0;
|
|
67928
|
+
}
|
|
67929
|
+
function isNavigationFocusedScenario(scenario) {
|
|
67930
|
+
const text = scenarioText(scenario);
|
|
67931
|
+
const navigates = /\b(back|return|go\s+home|navigate\s+back|leave|exit|cancel\s+out|dismiss|previous\s+screen)\b/.test(text);
|
|
67932
|
+
const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
|
|
67933
|
+
return navigates && !completesFlow;
|
|
67934
|
+
}
|
|
67935
|
+
function isFormValidationScenario(scenario) {
|
|
67936
|
+
if (scenario.variant === "validation")
|
|
67937
|
+
return true;
|
|
67938
|
+
if (scenario.type !== "EDGE_CASE")
|
|
67939
|
+
return false;
|
|
67940
|
+
const text = scenarioText(scenario);
|
|
67941
|
+
return /\b(empty|required|missing|blank|invalid|reject|error|validation|wrong|incorrect|mismatch)\b/.test(text);
|
|
67942
|
+
}
|
|
67943
|
+
function isFormMutationHappyPath(scenario) {
|
|
67944
|
+
if (isNavigationFocusedScenario(scenario))
|
|
67945
|
+
return false;
|
|
67946
|
+
if (isFormValidationScenario(scenario))
|
|
67947
|
+
return false;
|
|
67948
|
+
const text = scenarioText(scenario);
|
|
67949
|
+
const isHappy = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|success)\b/.test(text);
|
|
67950
|
+
if (!isHappy)
|
|
67951
|
+
return false;
|
|
67952
|
+
if (/\b(sign\s*in|log\s*in|login|authenticate|sign\s*out|log\s*out)\b/.test(text) && !/\b(change|update|reset)\s+(the\s+)?password\b/.test(text)) {
|
|
67953
|
+
return false;
|
|
67954
|
+
}
|
|
67955
|
+
if (/\b(show|display|visible|options?|providers?|links?)\b/.test(text) && !/\b(change|update|create|save|send|register|edit|post|write|compose|delete|add|set)\b/.test(text)) {
|
|
67956
|
+
return false;
|
|
67957
|
+
}
|
|
67958
|
+
const mutates = /\b(change|update|create|save|send|register|sign\s*up|checkout|edit|post|write|compose|set|add|delete|remove)\b/.test(text);
|
|
67959
|
+
const hasObject = /\b(password|email|profile|message|post|item|record|form|settings|account|name|title|description|comment|bio|phone|address|payment|card|user|note|task|entry)\b/.test(text);
|
|
67960
|
+
return mutates && hasObject;
|
|
67961
|
+
}
|
|
67962
|
+
function isNavigationChromeTap(action) {
|
|
67963
|
+
if (action.type !== "TAP")
|
|
67964
|
+
return false;
|
|
67965
|
+
const textForAction = actionText(action);
|
|
67966
|
+
return /\b(go\s+back|back\s+to|←|return\s+to|cancel|close|dismiss|not\s+now|skip|maybe\s+later)\b/.test(textForAction);
|
|
67967
|
+
}
|
|
67968
|
+
function isAuthMethodChooserTap(action) {
|
|
67969
|
+
if (action.type !== "TAP")
|
|
67970
|
+
return false;
|
|
67971
|
+
const textForAction = actionText(action);
|
|
67972
|
+
if (/\b(submit|confirm|sign\s*in|log\s*in|login)\b/.test(textForAction) && !/\b(with|using|via)\b/.test(textForAction)) {
|
|
67973
|
+
return false;
|
|
67974
|
+
}
|
|
67975
|
+
return /\b(continue\s+with|sign\s+in\s+with|log\s+in\s+with|use\s+(email|phone)|choose|pick|select)\b/.test(textForAction);
|
|
67976
|
+
}
|
|
67977
|
+
function isCredentialSubmitTap(action) {
|
|
67978
|
+
if (action.type !== "TAP")
|
|
67979
|
+
return false;
|
|
67980
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67981
|
+
return false;
|
|
67982
|
+
const textForAction = actionText(action);
|
|
67983
|
+
return /\b(sign\s*in|log\s*in|login|continue|submit|done|next|confirm)\b/.test(textForAction);
|
|
67984
|
+
}
|
|
67985
|
+
function isKeyboardFormCommit(action) {
|
|
67986
|
+
return action.type === "TYPE" && (action.value ?? "").endsWith("\n");
|
|
67987
|
+
}
|
|
67988
|
+
function isFormCommitAction(action) {
|
|
67989
|
+
if (isKeyboardFormCommit(action))
|
|
67990
|
+
return true;
|
|
67991
|
+
if (isCredentialSubmitTap(action))
|
|
67992
|
+
return true;
|
|
67993
|
+
if (action.type !== "TAP")
|
|
67994
|
+
return false;
|
|
67995
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67996
|
+
return false;
|
|
67997
|
+
const textForAction = actionText(action);
|
|
67998
|
+
return /\b(save|create|send|confirm|next|apply|post|checkout|pay|finish|update|register|sign\s*up|done|continue|submit)\b/i.test(textForAction);
|
|
67999
|
+
}
|
|
67773
68000
|
function actionText(action) {
|
|
67774
68001
|
const bundle = action.metadata.locatorBundle;
|
|
67775
68002
|
return [
|
|
@@ -67786,9 +68013,80 @@ ${statePacket}` },
|
|
|
67786
68013
|
bundle?.xpath
|
|
67787
68014
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67788
68015
|
}
|
|
68016
|
+
function isStructuralOnlyAssert(action) {
|
|
68017
|
+
if (action.type !== "ASSERT_VISIBLE" && action.type !== "ASSERT_TEXT")
|
|
68018
|
+
return false;
|
|
68019
|
+
const sel = (action.selector ?? "").trim();
|
|
68020
|
+
if (/^\/hierarchy(\[\d+\])?(\/android\.widget\.FrameLayout\[\d+\])*$/i.test(sel))
|
|
68021
|
+
return true;
|
|
68022
|
+
if (isStructuralAssertionIdentity(sel) || isStructuralAssertionIdentity(action.metadata.targetLabel)) {
|
|
68023
|
+
const bundle = action.metadata.locatorBundle;
|
|
68024
|
+
const human = Boolean(nonEmpty2(action.innerText) || nonEmpty2(action.metadata.targetLabel) || nonEmpty2(bundle?.text) || nonEmpty2(bundle?.contentDesc));
|
|
68025
|
+
if (!human)
|
|
68026
|
+
return true;
|
|
68027
|
+
}
|
|
68028
|
+
if (/^\/hierarchy/i.test(sel)) {
|
|
68029
|
+
const bundle = action.metadata.locatorBundle;
|
|
68030
|
+
const hasIdentity = Boolean(nonEmpty2(action.metadata.targetLabel) || nonEmpty2(action.innerText) || nonEmpty2(bundle?.text) || nonEmpty2(bundle?.contentDesc) || nonEmpty2(bundle?.accessibilityId) || nonEmpty2(bundle?.resourceId) && !isStructuralAssertionIdentity(bundle?.resourceId));
|
|
68031
|
+
if (!hasIdentity)
|
|
68032
|
+
return true;
|
|
68033
|
+
}
|
|
68034
|
+
return false;
|
|
68035
|
+
}
|
|
68036
|
+
function collapseConsecutiveBacks(actions) {
|
|
68037
|
+
if (actions.length <= 1)
|
|
68038
|
+
return actions;
|
|
68039
|
+
const out = [actions[0]];
|
|
68040
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68041
|
+
if (actions[i].type === "BACK" && out[out.length - 1].type === "BACK")
|
|
68042
|
+
continue;
|
|
68043
|
+
out.push(actions[i]);
|
|
68044
|
+
}
|
|
68045
|
+
return out;
|
|
68046
|
+
}
|
|
67789
68047
|
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67790
68048
|
const text = scenarioText(scenario);
|
|
67791
|
-
|
|
68049
|
+
if (isInspectionOnlyScenario(scenario, actions)) {
|
|
68050
|
+
return null;
|
|
68051
|
+
}
|
|
68052
|
+
if (isNavigationFocusedScenario(scenario)) {
|
|
68053
|
+
return null;
|
|
68054
|
+
}
|
|
68055
|
+
if (isFormValidationScenario(scenario)) {
|
|
68056
|
+
const typedFields = actions.some((a) => a.type === "TYPE");
|
|
68057
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68058
|
+
const hasValidationAssert = actions.some((a) => {
|
|
68059
|
+
if (a.type !== "ASSERT_VISIBLE" && a.type !== "ASSERT_TEXT")
|
|
68060
|
+
return false;
|
|
68061
|
+
return /\b(error|required|invalid|empty|missing|incorrect|wrong|try again|mismatch|does not match|must match)\b/.test(actionText(a));
|
|
68062
|
+
});
|
|
68063
|
+
const emptyFieldIntent = /\b(empty|required|missing|blank)\b/.test(text);
|
|
68064
|
+
const invalidValueIntent = /\b(wrong|incorrect|invalid|mismatch|reject)\b/.test(text);
|
|
68065
|
+
if (emptyFieldIntent && !invalidValueIntent) {
|
|
68066
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68067
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68068
|
+
}
|
|
68069
|
+
return null;
|
|
68070
|
+
}
|
|
68071
|
+
if (typedFields && !hasCommit) {
|
|
68072
|
+
return "validation scenario filled fields but never committed the form";
|
|
68073
|
+
}
|
|
68074
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68075
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68076
|
+
}
|
|
68077
|
+
return null;
|
|
68078
|
+
}
|
|
68079
|
+
if (isFormMutationHappyPath(scenario)) {
|
|
68080
|
+
const hasType = actions.some((a) => a.type === "TYPE");
|
|
68081
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68082
|
+
if (!hasType) {
|
|
68083
|
+
return "form-mutation happy path did not record any field entry";
|
|
68084
|
+
}
|
|
68085
|
+
if (!hasCommit) {
|
|
68086
|
+
return "form-mutation happy path did not record a form commit";
|
|
68087
|
+
}
|
|
68088
|
+
}
|
|
68089
|
+
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|change\s+password|update\s+password)\b/.test(text);
|
|
67792
68090
|
const typedEmailIndexes = [];
|
|
67793
68091
|
const typedPasswordIndexes = [];
|
|
67794
68092
|
actions.forEach((action, index) => {
|
|
@@ -67805,19 +68103,19 @@ ${statePacket}` },
|
|
|
67805
68103
|
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67806
68104
|
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67807
68105
|
const submitAfterCredentials = actions.some((action, index) => {
|
|
67808
|
-
if (
|
|
68106
|
+
if (index < lastCredentialIndex)
|
|
67809
68107
|
return false;
|
|
67810
|
-
|
|
67811
|
-
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
68108
|
+
if (!isFormCommitAction(action))
|
|
67812
68109
|
return false;
|
|
67813
|
-
|
|
68110
|
+
if (action.type === "TYPE" && index === lastCredentialIndex)
|
|
68111
|
+
return true;
|
|
68112
|
+
return index > lastCredentialIndex;
|
|
67814
68113
|
});
|
|
67815
68114
|
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67816
68115
|
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67817
|
-
return "credential fields were filled but no
|
|
68116
|
+
return "credential fields were filled but no form commit was recorded after them";
|
|
67818
68117
|
}
|
|
67819
68118
|
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67820
|
-
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67821
68119
|
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67822
68120
|
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67823
68121
|
if (action.type !== "TAP")
|
|
@@ -67825,29 +68123,70 @@ ${statePacket}` },
|
|
|
67825
68123
|
const textForAction = actionText(action);
|
|
67826
68124
|
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67827
68125
|
return false;
|
|
68126
|
+
if (isAuthMethodChooserTap(action) || isNavigationChromeTap(action))
|
|
68127
|
+
return false;
|
|
67828
68128
|
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67829
68129
|
});
|
|
67830
|
-
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario &&
|
|
68130
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67831
68131
|
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67832
68132
|
}
|
|
67833
|
-
if (
|
|
68133
|
+
if (isCredentialSignIn) {
|
|
68134
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
68135
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
68136
|
+
const tappedSubmit = submitAfterCredentials;
|
|
68137
|
+
if (typedEmail && typedPassword && tappedSubmit) {
|
|
68138
|
+
} else {
|
|
68139
|
+
const missing = [
|
|
68140
|
+
typedEmail ? null : "email/username entry",
|
|
68141
|
+
typedPassword ? null : "password entry",
|
|
68142
|
+
tappedSubmit ? null : "form commit (submit tap or keyboard Enter)"
|
|
68143
|
+
].filter((value) => value !== null);
|
|
68144
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
68145
|
+
}
|
|
68146
|
+
}
|
|
68147
|
+
const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
|
|
68148
|
+
if (!isHappyPath)
|
|
67834
68149
|
return null;
|
|
67835
|
-
|
|
67836
|
-
|
|
67837
|
-
|
|
67838
|
-
|
|
68150
|
+
let lastTypeIndex = -1;
|
|
68151
|
+
let lastAssertIndex = -1;
|
|
68152
|
+
actions.forEach((action, index) => {
|
|
68153
|
+
if (action.type === "TYPE")
|
|
68154
|
+
lastTypeIndex = index;
|
|
68155
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
68156
|
+
lastAssertIndex = index;
|
|
68157
|
+
});
|
|
68158
|
+
if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
|
|
67839
68159
|
return null;
|
|
67840
|
-
const
|
|
67841
|
-
|
|
67842
|
-
|
|
67843
|
-
|
|
67844
|
-
|
|
67845
|
-
|
|
68160
|
+
const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
|
|
68161
|
+
const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
|
|
68162
|
+
const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
|
|
68163
|
+
const lastTypeAction = actions[lastTypeIndex];
|
|
68164
|
+
const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
|
|
68165
|
+
const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
|
|
68166
|
+
if (index <= lastTypeIndex || index >= lastAssertIndex)
|
|
68167
|
+
return false;
|
|
68168
|
+
return isFormCommitAction(action);
|
|
68169
|
+
});
|
|
68170
|
+
if (screensDiffer && !commitAfterType) {
|
|
68171
|
+
return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
|
|
68172
|
+
}
|
|
68173
|
+
return null;
|
|
67846
68174
|
}
|
|
67847
68175
|
function actionCycleKey(action) {
|
|
67848
68176
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67849
68177
|
}
|
|
67850
68178
|
var MAX_RETRY_CYCLE_LENGTH = 8;
|
|
68179
|
+
function dedupeConsecutiveIdenticalActions(actions) {
|
|
68180
|
+
if (actions.length <= 1)
|
|
68181
|
+
return actions;
|
|
68182
|
+
const out = [actions[0]];
|
|
68183
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68184
|
+
if (actionCycleKey(actions[i]) === actionCycleKey(out[out.length - 1]))
|
|
68185
|
+
continue;
|
|
68186
|
+
out.push(actions[i]);
|
|
68187
|
+
}
|
|
68188
|
+
return out;
|
|
68189
|
+
}
|
|
67851
68190
|
function collapseRetryCycles(actions) {
|
|
67852
68191
|
const out = [...actions];
|
|
67853
68192
|
let changed = true;
|
|
@@ -67888,15 +68227,30 @@ ${statePacket}` },
|
|
|
67888
68227
|
}
|
|
67889
68228
|
function assembleTest(args) {
|
|
67890
68229
|
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
67891
|
-
let actions =
|
|
68230
|
+
let actions = dedupeConsecutiveIdenticalActions(recording.actions);
|
|
67892
68231
|
if (actions.length < recording.actions.length) {
|
|
67893
|
-
log2(`
|
|
68232
|
+
log2(` deduped ${recording.actions.length - actions.length} consecutive identical step(s) (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
68233
|
+
}
|
|
68234
|
+
const beforeBackCollapse = actions.length;
|
|
68235
|
+
actions = collapseConsecutiveBacks(actions);
|
|
68236
|
+
if (actions.length < beforeBackCollapse) {
|
|
68237
|
+
log2(` collapsed ${beforeBackCollapse - actions.length} consecutive BACK step(s) (${beforeBackCollapse} \u2192 ${actions.length}).`);
|
|
68238
|
+
}
|
|
68239
|
+
const beforeCycleCollapse = actions.length;
|
|
68240
|
+
actions = collapseRetryCycles(actions);
|
|
68241
|
+
if (actions.length < beforeCycleCollapse) {
|
|
68242
|
+
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
67894
68243
|
}
|
|
67895
68244
|
const beforeContainerDrop = actions.length;
|
|
67896
68245
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
67897
68246
|
if (actions.length < beforeContainerDrop) {
|
|
67898
68247
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
67899
68248
|
}
|
|
68249
|
+
const beforeWeakAssertDrop = actions.length;
|
|
68250
|
+
actions = actions.filter((a) => !isStructuralOnlyAssert(a));
|
|
68251
|
+
if (actions.length < beforeWeakAssertDrop) {
|
|
68252
|
+
log2(` dropped ${beforeWeakAssertDrop - actions.length} structural-only assert step(s) (no product identity).`);
|
|
68253
|
+
}
|
|
67900
68254
|
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
67901
68255
|
if (intentMismatch) {
|
|
67902
68256
|
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
@@ -68708,10 +69062,22 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68708
69062
|
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68709
69063
|
return false;
|
|
68710
69064
|
const text = mobileStepSearchText(step);
|
|
68711
|
-
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|
|
|
69065
|
+
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|go\s+back|back\s+to|cancel|close|dismiss)\b/.test(text)) {
|
|
68712
69066
|
return false;
|
|
68713
69067
|
}
|
|
68714
|
-
|
|
69068
|
+
if (/\b(continue\s+with|sign\s+in\s+with|log\s+in\s+with|use\s+(email|phone)|choose|pick|select)\b/.test(text) && !/\b(submit|confirm)\b/.test(text)) {
|
|
69069
|
+
return false;
|
|
69070
|
+
}
|
|
69071
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit|done|next|confirm)\b/.test(text);
|
|
69072
|
+
}
|
|
69073
|
+
function isKeyboardFormCommitStep(step) {
|
|
69074
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
69075
|
+
return false;
|
|
69076
|
+
const value = typeof step.value === "string" ? step.value : "";
|
|
69077
|
+
return value.endsWith("\n");
|
|
69078
|
+
}
|
|
69079
|
+
function isFormCommitStep(step) {
|
|
69080
|
+
return isCredentialSubmitTap(step) || isKeyboardFormCommitStep(step);
|
|
68715
69081
|
}
|
|
68716
69082
|
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68717
69083
|
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
@@ -68736,20 +69102,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68736
69102
|
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68737
69103
|
return false;
|
|
68738
69104
|
const text = mobileStepSearchText(step);
|
|
68739
|
-
return /\b(
|
|
69105
|
+
return /\b(sign\s+in\s+with\s+email|log\s+in\s+with\s+email|email\s+and\s+password|login\s+form|credential\s+form|email|e-mail|username|user\s*name)\b/.test(text);
|
|
68740
69106
|
});
|
|
68741
69107
|
if (hasCredentialFormEvidence)
|
|
68742
69108
|
return null;
|
|
68743
69109
|
return "credential-control test edits a credential field without recording a path to the email/password form; stale auth-control test is invalid until generation records a complete form path";
|
|
68744
69110
|
}
|
|
68745
|
-
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder &&
|
|
69111
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isFormCommitStep(step));
|
|
68746
69112
|
if (hasSubmitAfterCredentialEdit)
|
|
68747
69113
|
return null;
|
|
69114
|
+
const hasKeyboardCommitOnCredential = steps.some((step) => (step.order ?? -1) === lastCredentialEditOrder && isKeyboardFormCommitStep(step));
|
|
69115
|
+
if (hasKeyboardCommitOnCredential)
|
|
69116
|
+
return null;
|
|
68748
69117
|
const hasValidationAssertion = steps.some((step) => {
|
|
68749
69118
|
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68750
69119
|
return false;
|
|
68751
69120
|
const text = mobileStepSearchText(step);
|
|
68752
|
-
return /\b(invalid|required|error|empty|missing|incorrect|wrong|try again|validation|must enter|enter (an )?(email|password)|email is required|password is required)\b/.test(text);
|
|
69121
|
+
return /\b(invalid|required|error|empty|missing|incorrect|wrong|try again|validation|must enter|enter (an )?(email|password)|email is required|password is required|mismatch|does not match)\b/.test(text);
|
|
68753
69122
|
});
|
|
68754
69123
|
if (hasValidationAssertion)
|
|
68755
69124
|
return null;
|
|
@@ -68777,8 +69146,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68777
69146
|
function coerceSteps(value) {
|
|
68778
69147
|
if (!Array.isArray(value) || value.length === 0)
|
|
68779
69148
|
return void 0;
|
|
68780
|
-
const
|
|
68781
|
-
|
|
69149
|
+
const steps = [];
|
|
69150
|
+
for (let i = 0; i < value.length; i++) {
|
|
69151
|
+
const step = value[i];
|
|
69152
|
+
if (!isRecord(step) || typeof step.action !== "string" || !step.action.trim()) {
|
|
69153
|
+
return void 0;
|
|
69154
|
+
}
|
|
69155
|
+
const orderRaw = step.order;
|
|
69156
|
+
const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : i;
|
|
69157
|
+
steps.push({
|
|
69158
|
+
...step,
|
|
69159
|
+
order,
|
|
69160
|
+
action: step.action
|
|
69161
|
+
});
|
|
69162
|
+
}
|
|
69163
|
+
return steps.map((step, order) => ({ ...step, order }));
|
|
68782
69164
|
}
|
|
68783
69165
|
function parseHealVerdict(content) {
|
|
68784
69166
|
const cleaned = stripCodeFences(content);
|
|
@@ -72999,6 +73381,48 @@ var require_snapshot_observation = __commonJS({
|
|
|
72999
73381
|
var PROMOTED_FORM_FIELD_CAP = 12;
|
|
73000
73382
|
var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
|
|
73001
73383
|
var DATA_GRID_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:table|grid|rowgroup|row|gridcell|cell)\b/i;
|
|
73384
|
+
var DEEP_NONNAV_CAP_CENSUS = 16;
|
|
73385
|
+
var CENSUS_NAV_LINK_CAP = 60;
|
|
73386
|
+
var CENSUS_MIN_ACTION_GROUP = 3;
|
|
73387
|
+
var DESTRUCTIVE_ACTION_VERB_RE = /^(?:delete|remove|destroy|suspend|deactivate|disable|revoke|ban|block|archive|terminate|reject|cancel|reset|purge|wipe|unpublish|unsubscribe)$/i;
|
|
73388
|
+
function leadingActionVerb(label) {
|
|
73389
|
+
if (!label)
|
|
73390
|
+
return null;
|
|
73391
|
+
const m = label.trim().match(/[A-Za-z]+/);
|
|
73392
|
+
return m ? m[0].toLowerCase() : null;
|
|
73393
|
+
}
|
|
73394
|
+
function isNavLinkEntry(entry) {
|
|
73395
|
+
return entry.element.elementType === "LINK" && !!entry.element.triggersNavigation;
|
|
73396
|
+
}
|
|
73397
|
+
function isRowRepeatedControl(element) {
|
|
73398
|
+
return !!element.enrichment?.rowContext || !!element.containerPath && DATA_GRID_CONTAINER_SEGMENT_RE.test(element.containerPath);
|
|
73399
|
+
}
|
|
73400
|
+
function rowContainerScope(containerPath) {
|
|
73401
|
+
if (!containerPath)
|
|
73402
|
+
return null;
|
|
73403
|
+
const m = containerPath.match(/^(.*?)\s*>\s*row\b/i);
|
|
73404
|
+
return m ? m[1].trim() : containerPath.trim();
|
|
73405
|
+
}
|
|
73406
|
+
function containerScopeLabel(scope) {
|
|
73407
|
+
if (!scope)
|
|
73408
|
+
return null;
|
|
73409
|
+
const landmarks = [...scope.matchAll(/(?:table|grid|treegrid|list|listbox|tree|rowgroup):([^>]+)/gi)];
|
|
73410
|
+
if (landmarks.length > 0)
|
|
73411
|
+
return landmarks[landmarks.length - 1][1].trim();
|
|
73412
|
+
const segs = scope.split(">").map((s) => s.trim()).filter(Boolean);
|
|
73413
|
+
return segs.length ? segs[segs.length - 1] : null;
|
|
73414
|
+
}
|
|
73415
|
+
function rowIdentityKey(element) {
|
|
73416
|
+
const rc = element.enrichment?.rowContext;
|
|
73417
|
+
if (rc)
|
|
73418
|
+
return `rc:${rc}`;
|
|
73419
|
+
const cp = element.containerPath;
|
|
73420
|
+
if (cp) {
|
|
73421
|
+
const m = cp.match(/>\s*row:(.+)$/i);
|
|
73422
|
+
return m ? `row:${m[1].trim()}` : `cp:${cp}`;
|
|
73423
|
+
}
|
|
73424
|
+
return element.ref ? `ref:${element.ref}` : "unknown";
|
|
73425
|
+
}
|
|
73002
73426
|
function isGroupedFormField(element) {
|
|
73003
73427
|
if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
|
|
73004
73428
|
return false;
|
|
@@ -73014,6 +73438,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
73014
73438
|
const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
|
|
73015
73439
|
const artifact = page.snapshotArtifact;
|
|
73016
73440
|
const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
|
|
73441
|
+
const rowActionCensus = richRender && options.mode !== "survey" && (options.rowActionCensus ?? process.env.DISCOVERY_SNAPSHOT_ROW_CENSUS === "true");
|
|
73017
73442
|
const activeScope = inferActiveSnapshotScope(page);
|
|
73018
73443
|
const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
|
|
73019
73444
|
const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
|
|
@@ -73027,10 +73452,17 @@ var require_snapshot_observation = __commonJS({
|
|
|
73027
73452
|
});
|
|
73028
73453
|
let interactive;
|
|
73029
73454
|
if (options.mode === "survey") {
|
|
73030
|
-
const navLinks = allInteractive.filter(
|
|
73031
|
-
const others = allInteractive.filter((
|
|
73455
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73456
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
73032
73457
|
const othersCap = richRender ? 6 : 4;
|
|
73033
73458
|
interactive = [...navLinks, ...others.slice(0, othersCap)];
|
|
73459
|
+
} else if (rowActionCensus) {
|
|
73460
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73461
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
73462
|
+
interactive = [
|
|
73463
|
+
...navLinks.slice(0, CENSUS_NAV_LINK_CAP),
|
|
73464
|
+
...others.slice(0, options.elementLimit ?? DEEP_NONNAV_CAP_CENSUS)
|
|
73465
|
+
];
|
|
73034
73466
|
} else {
|
|
73035
73467
|
interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
|
|
73036
73468
|
}
|
|
@@ -73043,6 +73475,36 @@ var require_snapshot_observation = __commonJS({
|
|
|
73043
73475
|
}
|
|
73044
73476
|
}
|
|
73045
73477
|
}
|
|
73478
|
+
if (rowActionCensus && interactive.length > 0) {
|
|
73479
|
+
const shownVerbs = new Set(interactive.filter((entry) => !isNavLinkEntry(entry) && isRowRepeatedControl(entry.element)).map(({ element }) => leadingActionVerb(qualifyElementLabel(element))).filter(Boolean));
|
|
73480
|
+
const bestByVerb = /* @__PURE__ */ new Map();
|
|
73481
|
+
for (const entry of allInteractive) {
|
|
73482
|
+
if (isNavLinkEntry(entry) || !isRowRepeatedControl(entry.element))
|
|
73483
|
+
continue;
|
|
73484
|
+
const verb = leadingActionVerb(qualifyElementLabel(entry.element));
|
|
73485
|
+
if (!verb || !DESTRUCTIVE_ACTION_VERB_RE.test(verb))
|
|
73486
|
+
continue;
|
|
73487
|
+
if (shownVerbs.has(verb))
|
|
73488
|
+
continue;
|
|
73489
|
+
if (interactive.includes(entry))
|
|
73490
|
+
continue;
|
|
73491
|
+
const existing = bestByVerb.get(verb);
|
|
73492
|
+
if (!existing) {
|
|
73493
|
+
bestByVerb.set(verb, entry);
|
|
73494
|
+
continue;
|
|
73495
|
+
}
|
|
73496
|
+
if (existing.element.isDisabled && !entry.element.isDisabled)
|
|
73497
|
+
bestByVerb.set(verb, entry);
|
|
73498
|
+
}
|
|
73499
|
+
const swapIns = [...bestByVerb.values()];
|
|
73500
|
+
if (swapIns.length > 0) {
|
|
73501
|
+
const navShown = interactive.filter(isNavLinkEntry);
|
|
73502
|
+
const nonNavShown = interactive.filter((entry) => !isNavLinkEntry(entry));
|
|
73503
|
+
const evictable = Math.min(swapIns.length, nonNavShown.length);
|
|
73504
|
+
const trimmed = nonNavShown.slice(0, nonNavShown.length - evictable);
|
|
73505
|
+
interactive = [...navShown, ...trimmed, ...swapIns.slice(0, evictable)];
|
|
73506
|
+
}
|
|
73507
|
+
}
|
|
73046
73508
|
const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
|
|
73047
73509
|
const allForms = artifact?.formGroups ?? page.formGroups ?? [];
|
|
73048
73510
|
const forms = allForms.slice(0, options.formLimit ?? 3);
|
|
@@ -73342,6 +73804,54 @@ var require_snapshot_observation = __commonJS({
|
|
|
73342
73804
|
const hidden = allInteractive.length - interactive.length;
|
|
73343
73805
|
if (hidden > 0) {
|
|
73344
73806
|
lines.push(` # +${hidden} more interactive element(s) not individually listed (capped) \u2014 some may appear in the duplicates block above; if your target isn't here, narrow the scope or act on it by ref.`);
|
|
73807
|
+
if (rowActionCensus) {
|
|
73808
|
+
const shownSet = new Set(interactive.map((entry) => entry.element));
|
|
73809
|
+
const groups = /* @__PURE__ */ new Map();
|
|
73810
|
+
for (const entry of allInteractive) {
|
|
73811
|
+
const el = entry.element;
|
|
73812
|
+
if (!isRowRepeatedControl(el))
|
|
73813
|
+
continue;
|
|
73814
|
+
const verb = leadingActionVerb(qualifyElementLabel(el));
|
|
73815
|
+
if (!verb)
|
|
73816
|
+
continue;
|
|
73817
|
+
const role = (el.role ?? el.elementType ?? "control").toLowerCase();
|
|
73818
|
+
const scope = rowContainerScope(el.containerPath);
|
|
73819
|
+
const key = JSON.stringify([scope ?? "", role, verb]);
|
|
73820
|
+
let group = groups.get(key);
|
|
73821
|
+
if (!group) {
|
|
73822
|
+
group = { verb, role, scopeLabel: containerScopeLabel(scope), rows: /* @__PURE__ */ new Set(), shownRows: /* @__PURE__ */ new Set(), disabledRows: /* @__PURE__ */ new Set(), controls: 0 };
|
|
73823
|
+
groups.set(key, group);
|
|
73824
|
+
}
|
|
73825
|
+
const rowKey = rowIdentityKey(el);
|
|
73826
|
+
group.rows.add(rowKey);
|
|
73827
|
+
group.controls += 1;
|
|
73828
|
+
if (shownSet.has(el))
|
|
73829
|
+
group.shownRows.add(rowKey);
|
|
73830
|
+
if (el.isDisabled)
|
|
73831
|
+
group.disabledRows.add(rowKey);
|
|
73832
|
+
}
|
|
73833
|
+
const collapsed = [...groups.values()].map((g) => ({
|
|
73834
|
+
verb: g.verb,
|
|
73835
|
+
role: g.role,
|
|
73836
|
+
scopeLabel: g.scopeLabel,
|
|
73837
|
+
total: g.rows.size,
|
|
73838
|
+
shown: g.shownRows.size,
|
|
73839
|
+
disabled: g.disabledRows.size,
|
|
73840
|
+
controls: g.controls
|
|
73841
|
+
})).filter((g) => g.total >= CENSUS_MIN_ACTION_GROUP && g.total > g.shown).sort((a, b) => {
|
|
73842
|
+
const aDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(a.verb) ? 0 : 1;
|
|
73843
|
+
const bDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(b.verb) ? 0 : 1;
|
|
73844
|
+
return aDestructive - bDestructive || b.total - b.shown - (a.total - a.shown);
|
|
73845
|
+
});
|
|
73846
|
+
for (const g of collapsed) {
|
|
73847
|
+
const verbLabel = g.verb.charAt(0).toUpperCase() + g.verb.slice(1);
|
|
73848
|
+
const destructiveTag = DESTRUCTIVE_ACTION_VERB_RE.test(g.verb) ? " [destructive]" : "";
|
|
73849
|
+
const where = g.scopeLabel ? ` in ${yamlScalar(g.scopeLabel)}` : "";
|
|
73850
|
+
const controlsNote = g.controls > g.total ? ` (${g.controls} controls across ${g.total} rows)` : "";
|
|
73851
|
+
const disabledNote = g.disabled > 0 ? `; ${g.disabled} disabled at capture` : "";
|
|
73852
|
+
lines.push(` # \u21B3 "${verbLabel}" ${g.role}${destructiveTag}${where}: present on ${g.total} rows${controlsNote} \u2014 ${g.shown} listed above, ${g.total - g.shown} collapsed${disabledNote}; the same action repeats down these rows \u2014 target a specific row by its row identity (the row's Edit link / row_context above), not a bare "${verbLabel}" locator.`);
|
|
73853
|
+
}
|
|
73854
|
+
}
|
|
73345
73855
|
}
|
|
73346
73856
|
}
|
|
73347
73857
|
}
|
|
@@ -74364,13 +74874,14 @@ var require_auth_recipe = __commonJS({
|
|
|
74364
74874
|
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
74365
74875
|
"use strict";
|
|
74366
74876
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74367
|
-
exports.AUTH_MARKER_VERSION = void 0;
|
|
74877
|
+
exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
|
|
74368
74878
|
exports.computeRecipeHash = computeRecipeHash;
|
|
74369
74879
|
exports.buildAuthRecipe = buildAuthRecipe;
|
|
74370
74880
|
exports.authProofLocator = authProofLocator;
|
|
74371
74881
|
exports.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
74372
74882
|
exports.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
74373
74883
|
exports.renderAuthFn = renderAuthFn;
|
|
74884
|
+
exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
74374
74885
|
exports.absoluteRoute = absoluteRoute;
|
|
74375
74886
|
exports.renderReplayClickArrow = renderReplayClickArrow;
|
|
74376
74887
|
exports.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
@@ -74489,6 +75000,17 @@ var require_auth_recipe = __commonJS({
|
|
|
74489
75000
|
lines.push(END_MARKER);
|
|
74490
75001
|
return lines.join("\n");
|
|
74491
75002
|
}
|
|
75003
|
+
exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
75004
|
+
"AUTH_FILE",
|
|
75005
|
+
"SHARED_AUTH_INJECT",
|
|
75006
|
+
"VALIDATE_RUN_ID",
|
|
75007
|
+
"VALIDATE_TASK_ARN",
|
|
75008
|
+
"VALIDATE_RUNNER_AUTH",
|
|
75009
|
+
"VALIDATE_SERVER_URL"
|
|
75010
|
+
]);
|
|
75011
|
+
function isReservedRunnerEnvVar(key) {
|
|
75012
|
+
return key.startsWith("VALIDATE_") || exports.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
75013
|
+
}
|
|
74492
75014
|
function absoluteRoute(route2, baseUrl) {
|
|
74493
75015
|
try {
|
|
74494
75016
|
return new URL(route2, baseUrl).href;
|
|
@@ -315593,9 +316115,15 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315593
316115
|
log2(`
|
|
315594
316116
|
Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
|
|
315595
316117
|
if (callbacks?.onExploreComplete) {
|
|
315596
|
-
|
|
315597
|
-
|
|
315598
|
-
})
|
|
316118
|
+
try {
|
|
316119
|
+
await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions));
|
|
316120
|
+
} catch (err) {
|
|
316121
|
+
const message = checkpointErrorMessage(err);
|
|
316122
|
+
log2(`[stream] explore-complete callback failed: ${message}`);
|
|
316123
|
+
if (isTerminalDiscoveryCheckpointError(err)) {
|
|
316124
|
+
throw err;
|
|
316125
|
+
}
|
|
316126
|
+
}
|
|
315599
316127
|
}
|
|
315600
316128
|
if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
|
|
315601
316129
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
@@ -315623,8 +316151,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315623
316151
|
pausePhase: "explore"
|
|
315624
316152
|
};
|
|
315625
316153
|
}
|
|
315626
|
-
|
|
315627
|
-
|
|
316154
|
+
if (!isSurvey) {
|
|
316155
|
+
log2("\nRelaunching app for a clean state before planning/generation...");
|
|
316156
|
+
await driver.relaunch();
|
|
316157
|
+
}
|
|
315628
316158
|
log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
|
|
315629
316159
|
const graph = state2.toScreenGraph();
|
|
315630
316160
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
@@ -315647,7 +316177,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315647
316177
|
hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
|
|
315648
316178
|
},
|
|
315649
316179
|
incremental: ctx.incremental,
|
|
315650
|
-
testBudget
|
|
316180
|
+
testBudget,
|
|
316181
|
+
userNotes: ctx.userNotes,
|
|
316182
|
+
featureContext: ctx.featureContext
|
|
315651
316183
|
}, {
|
|
315652
316184
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
315653
316185
|
onAICall
|
|
@@ -315820,10 +316352,20 @@ Fatal error: ${msg}`);
|
|
|
315820
316352
|
log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
315821
316353
|
return false;
|
|
315822
316354
|
}) ?? false;
|
|
315823
|
-
if (
|
|
315824
|
-
|
|
316355
|
+
if (cleared)
|
|
316356
|
+
return true;
|
|
316357
|
+
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
316358
|
+
log2("CLEAR_APP_DATA requested but app-data reset was unavailable; a post-explore relaunch will reset state before planning.");
|
|
316359
|
+
}
|
|
316360
|
+
if (mode !== "survey") {
|
|
316361
|
+
return false;
|
|
315825
316362
|
}
|
|
315826
|
-
|
|
316363
|
+
try {
|
|
316364
|
+
await driver.relaunch();
|
|
316365
|
+
} catch (err) {
|
|
316366
|
+
log2(`App relaunch failed after unsuccessful app-data reset: ${err instanceof Error ? err.message : String(err)}`);
|
|
316367
|
+
}
|
|
316368
|
+
return false;
|
|
315827
316369
|
}
|
|
315828
316370
|
function shouldPauseAfterTruncatedExplore(explore) {
|
|
315829
316371
|
return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
|
|
@@ -315870,6 +316412,7 @@ var require_discover_agent = __commonJS({
|
|
|
315870
316412
|
var claude_discovery_runner_js_1 = require_claude_discovery_runner();
|
|
315871
316413
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
315872
316414
|
var canvas_detector_js_1 = require_canvas_detector();
|
|
316415
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
315873
316416
|
async function runCanvasPreflight(ctx, sharedBrowser, log2) {
|
|
315874
316417
|
const cached = ctx.appRenderType ?? null;
|
|
315875
316418
|
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
@@ -316565,7 +317108,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
316565
317108
|
}
|
|
316566
317109
|
if (envRefs.size > 0) {
|
|
316567
317110
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
316568
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
317111
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
316569
317112
|
if (missing.length > 0) {
|
|
316570
317113
|
autoVariableDefaults = {};
|
|
316571
317114
|
for (const key of missing) {
|
|
@@ -317081,7 +317624,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
317081
317624
|
envRefs.add(match[1]);
|
|
317082
317625
|
if (envRefs.size > 0) {
|
|
317083
317626
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
317084
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
317627
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
317085
317628
|
if (missing.length > 0) {
|
|
317086
317629
|
autoVariableDefaults = {};
|
|
317087
317630
|
for (const key of missing) {
|
|
@@ -329963,9 +330506,9 @@ var require_dist2 = __commonJS({
|
|
|
329963
330506
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
329964
330507
|
};
|
|
329965
330508
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
329966
|
-
exports.
|
|
329967
|
-
exports.
|
|
329968
|
-
exports.UNSUPPORTED_RENDERER_PREFIX = void 0;
|
|
330509
|
+
exports.buildImportContractMd = exports.MAX_ARCHIVE_BYTES = exports.isUnsafeEntryName = exports.extractZipSafely = exports.downloadArchiveCapped = exports.detectArchiveKind = exports.acquireRepoForImport = exports.runFrameworkImportOnRunner = exports.getAgenticEngine = exports.resolveRefFromSnapshot = exports.replayAuthRecipeIntoBrowser = exports.runRegisterCaptureRun = exports.runLoginCaptureRun = exports.isReservedRunnerEnvVar = exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = exports.buildHealAuthStamp = exports.extractAuthRegion = exports.upsertAuthBlock = exports.buildAuthRecipe = exports.commandToReplayStep = exports.buildReplayPlan = exports.absoluteRoute = exports.renderReplaySettleArrow = exports.renderReplayClickArrow = exports.renderAuthFn = exports.canonicalizeRoute = exports.executeGrokPerBatchHeal = exports.executeGrokPerTestHeal = exports.headerVal = exports.classifyAuthType = exports.isTextExcerptable = exports.isStaticAssetByExt = exports.isApiCall = exports.buildApiCallSummary = exports.computeMobileHealOutcome = exports.detectMobileStepSecretLeak = exports.redactMobileSecretsFromText = exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports.executeMobileLiveHeal = exports.executeMobileTestWithHealing = exports.executeMobileScriptDrivenHeal = exports.runMobileGeneratePhase = exports.runMobileAIPlanner = exports.buildMobileSiteMap = exports.formatMobileEvidenceForPlanner = exports.MOBILE_SCENARIO_SYSTEM_PROMPT = exports.MOBILE_SURVEY_SYSTEM_PROMPT = exports.runMobileExplorePhase = void 0;
|
|
330510
|
+
exports.buildUnsupportedRendererMessage = exports.modelSupportsVision = exports.decideCanvasStrategy = exports.decideAppRenderType = exports.detectCanvasApp = exports.detectRateLimitBlock = exports.isSafeCanonicalRedirect = exports.resolveBaseUrlPreflight = exports.checkBaseUrlRedirect = exports.stripJsonFences = exports.scrubCredentials = exports.withAIRetry = exports.snapshotMatchingChildren = exports.findNewChromiumChildPid = exports.killProcessTree = exports.gracefulCloseWithTreeKill = exports.liveBrowserRegistry = exports.REPO_SWITCH_REMOVED_FRACTION = exports.MAX_MANIFEST_FILES = exports.toDiffSummary = exports.isLikelyTestFile = exports.sanitizeRepoRelPath = exports.sha256Hex = exports.hashFiles = exports.enumerateTestFiles = exports.buildNextManifest = exports.detectRepoSwitch = exports.classifyImportDiff = exports.parseImportManifest = exports.REPO_ANALYSIS_SCHEMA_VERSION = exports.validateSourceEndpoints = exports.validateAuthoredTest = exports.validateRepoMapFile = exports.validateRepoMemoryFile = exports.buildTestingStandardsSection = exports.buildRepoAuthorSystemRules = exports.buildRepoAuthorKickoff = exports.buildRepoAuthorContractMd = exports.buildRepoMapSystemRules = exports.buildRepoMapKickoff = exports.buildRepoMapContractMd = exports.buildRepoMemorySystemRules = exports.buildRepoMemoryKickoff = exports.buildRepoMemoryContractMd = exports.runRepoAnalysisOnRunner = exports.IMPORT_SCHEMA_VERSION = exports.validateImportResultFile = exports.validateImportedTest = exports.buildImportSystemRules = exports.buildImportKickoff = void 0;
|
|
330511
|
+
exports.UNSUPPORTED_RENDERER_PREFIX = exports.UnsupportedRendererError = exports.buildCanvasModeAuthoringAddendum = void 0;
|
|
329969
330512
|
__exportStar(require_ai_provider(), exports);
|
|
329970
330513
|
__exportStar(require_mobile_types(), exports);
|
|
329971
330514
|
__exportStar(require_mobile_source_parser(), exports);
|
|
@@ -330102,6 +330645,12 @@ var require_dist2 = __commonJS({
|
|
|
330102
330645
|
Object.defineProperty(exports, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
|
|
330103
330646
|
return auth_recipe_js_1.AUTH_MARKER_VERSION;
|
|
330104
330647
|
} });
|
|
330648
|
+
Object.defineProperty(exports, "RESERVED_RUNNER_ENV_VARS", { enumerable: true, get: function() {
|
|
330649
|
+
return auth_recipe_js_1.RESERVED_RUNNER_ENV_VARS;
|
|
330650
|
+
} });
|
|
330651
|
+
Object.defineProperty(exports, "isReservedRunnerEnvVar", { enumerable: true, get: function() {
|
|
330652
|
+
return auth_recipe_js_1.isReservedRunnerEnvVar;
|
|
330653
|
+
} });
|
|
330105
330654
|
var auth_capture_run_js_1 = require_auth_capture_run();
|
|
330106
330655
|
Object.defineProperty(exports, "runLoginCaptureRun", { enumerable: true, get: function() {
|
|
330107
330656
|
return auth_capture_run_js_1.runLoginCaptureRun;
|
|
@@ -330329,7 +330878,7 @@ var require_package4 = __commonJS({
|
|
|
330329
330878
|
"package.json"(exports, module) {
|
|
330330
330879
|
module.exports = {
|
|
330331
330880
|
name: "@validate.qa/runner",
|
|
330332
|
-
version: "1.0.
|
|
330881
|
+
version: "1.0.34",
|
|
330333
330882
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330334
330883
|
bin: {
|
|
330335
330884
|
"validate-runner": "dist/cli.js"
|
|
@@ -330945,25 +331494,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
|
330945
331494
|
}
|
|
330946
331495
|
return elementId;
|
|
330947
331496
|
}
|
|
331497
|
+
async function sendKeysToFocusedInput(sessionPath, value) {
|
|
331498
|
+
await appiumRequest(`${sessionPath}/keys`, {
|
|
331499
|
+
method: "POST",
|
|
331500
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
331501
|
+
});
|
|
331502
|
+
}
|
|
330948
331503
|
async function typeText(sessionPath, value, bounds) {
|
|
330949
331504
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330950
|
-
if (
|
|
330951
|
-
|
|
331505
|
+
if (elementId) {
|
|
331506
|
+
try {
|
|
331507
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
331508
|
+
method: "POST",
|
|
331509
|
+
body: JSON.stringify({ text: value })
|
|
331510
|
+
});
|
|
331511
|
+
return;
|
|
331512
|
+
} catch (err) {
|
|
331513
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331514
|
+
}
|
|
330952
331515
|
}
|
|
330953
|
-
await
|
|
330954
|
-
method: "POST",
|
|
330955
|
-
body: JSON.stringify({ text: value })
|
|
330956
|
-
});
|
|
331516
|
+
await sendKeysToFocusedInput(sessionPath, value);
|
|
330957
331517
|
}
|
|
330958
331518
|
async function clearField(sessionPath, bounds) {
|
|
330959
331519
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330960
|
-
if (
|
|
330961
|
-
|
|
331520
|
+
if (elementId) {
|
|
331521
|
+
try {
|
|
331522
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
331523
|
+
method: "POST",
|
|
331524
|
+
body: JSON.stringify({})
|
|
331525
|
+
});
|
|
331526
|
+
return;
|
|
331527
|
+
} catch (err) {
|
|
331528
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331529
|
+
}
|
|
331530
|
+
}
|
|
331531
|
+
try {
|
|
331532
|
+
await appiumRequest(`${sessionPath}/keys`, {
|
|
331533
|
+
method: "POST",
|
|
331534
|
+
// Common select-all + delete sequence is platform-dependent; empty keys
|
|
331535
|
+
// after focus is the portable baseline Appium accepts for many hybrids.
|
|
331536
|
+
body: JSON.stringify({ text: "", value: [] })
|
|
331537
|
+
});
|
|
331538
|
+
} catch (err) {
|
|
331539
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331540
|
+
throw new Error(
|
|
331541
|
+
`No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
|
|
331542
|
+
);
|
|
330962
331543
|
}
|
|
330963
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
330964
|
-
method: "POST",
|
|
330965
|
-
body: JSON.stringify({})
|
|
330966
|
-
});
|
|
330967
331544
|
}
|
|
330968
331545
|
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
330969
331546
|
const dist = clampMobileSwipeDistance(distance);
|
|
@@ -332204,6 +332781,10 @@ async function startMobileNetworkCapture(options) {
|
|
|
332204
332781
|
|
|
332205
332782
|
// src/services/appium-executor.ts
|
|
332206
332783
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
332784
|
+
var ELEMENT_ID_PATTERN2 = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
332785
|
+
function validateElementId(elementId) {
|
|
332786
|
+
return typeof elementId === "string" && ELEMENT_ID_PATTERN2.test(elementId) ? elementId : null;
|
|
332787
|
+
}
|
|
332207
332788
|
var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
332208
332789
|
const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
332209
332790
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
|
|
@@ -332338,6 +332919,9 @@ async function createSession(target) {
|
|
|
332338
332919
|
});
|
|
332339
332920
|
const sessionId = result?.value?.sessionId ?? result?.sessionId;
|
|
332340
332921
|
if (!sessionId) throw new Error("Appium session creation returned no session ID");
|
|
332922
|
+
if (target.platform === "ANDROID") {
|
|
332923
|
+
await prepareAndroidInputEnvironment(target.deviceId);
|
|
332924
|
+
}
|
|
332341
332925
|
return sessionId;
|
|
332342
332926
|
}
|
|
332343
332927
|
var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
|
|
@@ -332725,7 +333309,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
332725
333309
|
method: "POST",
|
|
332726
333310
|
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
332727
333311
|
});
|
|
332728
|
-
const elementId =
|
|
333312
|
+
const elementId = validateElementId(
|
|
333313
|
+
result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
|
|
333314
|
+
);
|
|
332729
333315
|
if (elementId) return elementId;
|
|
332730
333316
|
} catch (err) {
|
|
332731
333317
|
if (isTransportError(err)) throw err;
|
|
@@ -332765,24 +333351,25 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
332765
333351
|
})
|
|
332766
333352
|
});
|
|
332767
333353
|
}
|
|
332768
|
-
async function getActiveElementId2(sessionId) {
|
|
332769
|
-
try {
|
|
332770
|
-
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
332771
|
-
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
332772
|
-
} catch {
|
|
332773
|
-
return null;
|
|
332774
|
-
}
|
|
332775
|
-
}
|
|
332776
|
-
async function sendKeysToFocusedInput(sessionId, value) {
|
|
332777
|
-
await wdRequest(`/session/${sessionId}/keys`, {
|
|
332778
|
-
method: "POST",
|
|
332779
|
-
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
332780
|
-
});
|
|
332781
|
-
}
|
|
332782
333354
|
function encodeAndroidInputText(value) {
|
|
332783
333355
|
const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
332784
333356
|
return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
|
|
332785
333357
|
}
|
|
333358
|
+
async function prepareAndroidInputEnvironment(deviceId) {
|
|
333359
|
+
if (!deviceId) return;
|
|
333360
|
+
const settingsToDisable = [
|
|
333361
|
+
["secure", "stylus_handwriting_enabled", "0"]
|
|
333362
|
+
];
|
|
333363
|
+
for (const [namespace, key, value] of settingsToDisable) {
|
|
333364
|
+
try {
|
|
333365
|
+
await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
333366
|
+
timeout: 8e3,
|
|
333367
|
+
windowsHide: true
|
|
333368
|
+
});
|
|
333369
|
+
} catch {
|
|
333370
|
+
}
|
|
333371
|
+
}
|
|
333372
|
+
}
|
|
332786
333373
|
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
332787
333374
|
if (!deviceId) {
|
|
332788
333375
|
throw new Error("Android keyboard fallback requires a device id");
|
|
@@ -332844,100 +333431,28 @@ async function executeType(sessionId, step, context) {
|
|
|
332844
333431
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
332845
333432
|
const value = String(step.value);
|
|
332846
333433
|
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
332847
|
-
|
|
332848
|
-
|
|
332849
|
-
|
|
332850
|
-
|
|
332851
|
-
|
|
332852
|
-
|
|
332853
|
-
|
|
332854
|
-
|
|
332855
|
-
|
|
332856
|
-
|
|
332857
|
-
|
|
332858
|
-
|
|
332859
|
-
|
|
332860
|
-
|
|
332861
|
-
|
|
332862
|
-
try {
|
|
332863
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
332864
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332865
|
-
if (!lastInputError) return;
|
|
332866
|
-
elementId = null;
|
|
332867
|
-
} catch (error2) {
|
|
332868
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
332869
|
-
lastInputError = error2;
|
|
332870
|
-
}
|
|
332871
|
-
}
|
|
332872
|
-
if (!elementId && bounds) {
|
|
332873
|
-
await tapCoordinates(sessionId, bounds);
|
|
332874
|
-
tappedBounds = true;
|
|
332875
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332876
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332877
|
-
}
|
|
332878
|
-
if (!elementId) {
|
|
332879
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332880
|
-
}
|
|
332881
|
-
if (elementId) {
|
|
332882
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332883
|
-
if (!lastInputError) return;
|
|
332884
|
-
elementId = null;
|
|
332885
|
-
if (!bounds) {
|
|
332886
|
-
throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
|
|
332887
|
-
}
|
|
332888
|
-
}
|
|
332889
|
-
if (bounds) {
|
|
332890
|
-
if (!tappedBounds) {
|
|
332891
|
-
await tapCoordinates(sessionId, bounds);
|
|
332892
|
-
tappedBounds = true;
|
|
332893
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332894
|
-
}
|
|
332895
|
-
try {
|
|
332896
|
-
await sendKeysToFocusedInput(sessionId, value);
|
|
332897
|
-
return;
|
|
332898
|
-
} catch (error2) {
|
|
332899
|
-
if (isTransportError(error2)) throw error2;
|
|
332900
|
-
lastInputError = error2;
|
|
332901
|
-
}
|
|
332902
|
-
}
|
|
332903
|
-
if (context.platform === "ANDROID" && tappedBounds) {
|
|
332904
|
-
try {
|
|
332905
|
-
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
332906
|
-
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
332907
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332908
|
-
return;
|
|
332909
|
-
} catch (error2) {
|
|
332910
|
-
if (isTransportError(error2)) throw error2;
|
|
332911
|
-
lastInputError = error2;
|
|
333434
|
+
const sessionPath = `/session/${sessionId}`;
|
|
333435
|
+
try {
|
|
333436
|
+
await typeText(sessionPath, value, bounds);
|
|
333437
|
+
return;
|
|
333438
|
+
} catch (error2) {
|
|
333439
|
+
if (isTransportError(error2)) throw error2;
|
|
333440
|
+
if (context.platform === "ANDROID" && bounds) {
|
|
333441
|
+
try {
|
|
333442
|
+
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
333443
|
+
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
333444
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
333445
|
+
return;
|
|
333446
|
+
} catch (androidError) {
|
|
333447
|
+
if (isTransportError(androidError)) throw androidError;
|
|
333448
|
+
}
|
|
332912
333449
|
}
|
|
333450
|
+
throw error2 instanceof Error ? error2 : new Error(String(error2));
|
|
332913
333451
|
}
|
|
332914
|
-
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
332915
333452
|
}
|
|
332916
333453
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
332917
333454
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
332918
|
-
|
|
332919
|
-
if (step.target) {
|
|
332920
|
-
try {
|
|
332921
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
332922
|
-
} catch (error2) {
|
|
332923
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
332924
|
-
}
|
|
332925
|
-
}
|
|
332926
|
-
if (!elementId && bounds) {
|
|
332927
|
-
await tapCoordinates(sessionId, bounds);
|
|
332928
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332929
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332930
|
-
}
|
|
332931
|
-
if (!elementId) {
|
|
332932
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332933
|
-
}
|
|
332934
|
-
if (!elementId) {
|
|
332935
|
-
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
332936
|
-
}
|
|
332937
|
-
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
|
|
332938
|
-
method: "POST",
|
|
332939
|
-
body: "{}"
|
|
332940
|
-
});
|
|
333455
|
+
await clearField(`/session/${sessionId}`, bounds);
|
|
332941
333456
|
}
|
|
332942
333457
|
async function executeSwipe(sessionId, step) {
|
|
332943
333458
|
const direction = step.metadata?.direction || "up";
|