@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.js
CHANGED
|
@@ -62285,7 +62285,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
62285
62285
|
return { screen, signal, refMap, observation };
|
|
62286
62286
|
}
|
|
62287
62287
|
function buildStatePacket(input) {
|
|
62288
|
-
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions } = input;
|
|
62288
|
+
const { state: state2, mode, iteration, maxIterations, latestObservation, journeys, transitions, featureName, featureContext, userNotes } = input;
|
|
62289
62289
|
const sections = [];
|
|
62290
62290
|
sections.push(`## Mobile Exploration Status (iteration ${iteration}/${maxIterations})`);
|
|
62291
62291
|
sections.push(`Mode: ${mode} | Budget remaining: ${maxIterations - iteration} iterations`);
|
|
@@ -62298,10 +62298,46 @@ var require_mobile_explorer = __commonJS({
|
|
|
62298
62298
|
sections.push("");
|
|
62299
62299
|
if (mode === "survey") {
|
|
62300
62300
|
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.");
|
|
62301
|
+
} else if (featureName?.trim()) {
|
|
62302
|
+
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.`);
|
|
62301
62303
|
} else {
|
|
62302
62304
|
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.");
|
|
62303
62305
|
}
|
|
62304
62306
|
sections.push("");
|
|
62307
|
+
if (mode === "deep" && featureName?.trim()) {
|
|
62308
|
+
sections.push(`## Feature: ${featureName.trim()}`);
|
|
62309
|
+
if (featureContext?.plannerDescription?.trim()) {
|
|
62310
|
+
sections.push(featureContext.plannerDescription.trim());
|
|
62311
|
+
}
|
|
62312
|
+
if (featureContext?.priorInsight?.trim()) {
|
|
62313
|
+
sections.push(`Prior insight: ${featureContext.priorInsight.trim()}`);
|
|
62314
|
+
}
|
|
62315
|
+
if (featureContext?.pageIntel && featureContext.pageIntel.length > 0) {
|
|
62316
|
+
sections.push("### Target screens / routes for this feature");
|
|
62317
|
+
for (const page of featureContext.pageIntel.slice(0, 12)) {
|
|
62318
|
+
sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
|
|
62319
|
+
}
|
|
62320
|
+
}
|
|
62321
|
+
if (featureContext?.flowIntel && featureContext.flowIntel.length > 0) {
|
|
62322
|
+
sections.push("### Known flows");
|
|
62323
|
+
for (const flow of featureContext.flowIntel.slice(0, 8)) {
|
|
62324
|
+
sections.push(`- ${flow}`);
|
|
62325
|
+
}
|
|
62326
|
+
}
|
|
62327
|
+
if (featureContext?.otherFeatures && featureContext.otherFeatures.length > 0) {
|
|
62328
|
+
sections.push("### Other features \u2014 do NOT explore these (navigation context only)");
|
|
62329
|
+
for (const other of featureContext.otherFeatures.slice(0, 12)) {
|
|
62330
|
+
const pages = other.pages?.length ? ` [${other.pages.slice(0, 4).join(", ")}]` : "";
|
|
62331
|
+
sections.push(`- ${other.name}${pages}`);
|
|
62332
|
+
}
|
|
62333
|
+
}
|
|
62334
|
+
sections.push("");
|
|
62335
|
+
}
|
|
62336
|
+
if (userNotes?.trim()) {
|
|
62337
|
+
sections.push(`## Operator notes
|
|
62338
|
+
${userNotes.trim()}`);
|
|
62339
|
+
sections.push("");
|
|
62340
|
+
}
|
|
62305
62341
|
const screenCount = state2.getScreenCount();
|
|
62306
62342
|
sections.push(`## Screens Mapped: ${screenCount} | Transitions: ${transitions.length} | Journeys: ${journeys.length}`);
|
|
62307
62343
|
const currentScreenId = state2.getCurrentScreenId();
|
|
@@ -62458,7 +62494,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62458
62494
|
} catch {
|
|
62459
62495
|
}
|
|
62460
62496
|
};
|
|
62461
|
-
const mode = ctx.mode === "
|
|
62497
|
+
const mode = ctx.mode === "survey" ? "survey" : "deep";
|
|
62462
62498
|
const platform3 = platformForMedium(ctx.medium);
|
|
62463
62499
|
const enableRecordJourney = config?.enableRecordJourney ?? mode !== "survey";
|
|
62464
62500
|
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
@@ -62746,7 +62782,10 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
62746
62782
|
maxIterations,
|
|
62747
62783
|
latestObservation: getLatest()?.observation ?? null,
|
|
62748
62784
|
journeys,
|
|
62749
|
-
transitions: collectedTransitions
|
|
62785
|
+
transitions: collectedTransitions,
|
|
62786
|
+
featureName: ctx.featureName,
|
|
62787
|
+
featureContext: ctx.featureContext,
|
|
62788
|
+
userNotes: ctx.userNotes
|
|
62750
62789
|
});
|
|
62751
62790
|
const messages = [
|
|
62752
62791
|
{ role: "system", content: systemPrompt },
|
|
@@ -65124,7 +65163,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65124
65163
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
65125
65164
|
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"]\`
|
|
65126
65165
|
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"]\`
|
|
65127
|
-
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", "
|
|
65166
|
+
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.
|
|
65128
65167
|
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.
|
|
65129
65168
|
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>."
|
|
65130
65169
|
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.
|
|
@@ -66084,7 +66123,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
66084
66123
|
9. **No duplicates** - check existing test names before adding
|
|
66085
66124
|
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.
|
|
66086
66125
|
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.
|
|
66087
|
-
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:
|
|
66126
|
+
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:
|
|
66088
66127
|
- The trigger step (Tap Submit / Generate / Send / Upload / Run / Analyze / Save / Pay / etc.)
|
|
66089
66128
|
- 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.
|
|
66090
66129
|
- 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.
|
|
@@ -66315,6 +66354,34 @@ ${targetBits.length > 0 ? targetBits.join(" ") : "native mobile app"}`);
|
|
|
66315
66354
|
if (context.featureName) {
|
|
66316
66355
|
sections.push(`## Feature: ${context.featureName}
|
|
66317
66356
|
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.`);
|
|
66357
|
+
if (context.featureContext?.plannerDescription?.trim()) {
|
|
66358
|
+
sections.push(context.featureContext.plannerDescription.trim());
|
|
66359
|
+
}
|
|
66360
|
+
if (context.featureContext?.priorInsight?.trim()) {
|
|
66361
|
+
sections.push(`Prior insight: ${context.featureContext.priorInsight.trim()}`);
|
|
66362
|
+
}
|
|
66363
|
+
if (context.featureContext?.pageIntel && context.featureContext.pageIntel.length > 0) {
|
|
66364
|
+
sections.push("### Feature page intel");
|
|
66365
|
+
for (const page of context.featureContext.pageIntel.slice(0, 12)) {
|
|
66366
|
+
sections.push(`- ${page.route} (${page.type}): ${page.summary}`);
|
|
66367
|
+
}
|
|
66368
|
+
}
|
|
66369
|
+
if (context.featureContext?.flowIntel && context.featureContext.flowIntel.length > 0) {
|
|
66370
|
+
sections.push("### Known flows");
|
|
66371
|
+
for (const flow of context.featureContext.flowIntel.slice(0, 8)) {
|
|
66372
|
+
sections.push(`- ${flow}`);
|
|
66373
|
+
}
|
|
66374
|
+
}
|
|
66375
|
+
if (context.featureContext?.otherFeatures && context.featureContext.otherFeatures.length > 0) {
|
|
66376
|
+
sections.push("### Other features (do not plan primary scenarios for these)");
|
|
66377
|
+
for (const other of context.featureContext.otherFeatures.slice(0, 12)) {
|
|
66378
|
+
sections.push(`- ${other.name}`);
|
|
66379
|
+
}
|
|
66380
|
+
}
|
|
66381
|
+
}
|
|
66382
|
+
if (context.userNotes?.trim()) {
|
|
66383
|
+
sections.push(`## Operator notes
|
|
66384
|
+
${context.userNotes.trim()}`);
|
|
66318
66385
|
}
|
|
66319
66386
|
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66320
66387
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
@@ -66441,7 +66508,7 @@ ${constraints.join("\n")}`);
|
|
|
66441
66508
|
routePattern: null,
|
|
66442
66509
|
title: state2.screenName ?? null,
|
|
66443
66510
|
pageType: state2.screenType ?? "UNKNOWN",
|
|
66444
|
-
requiresAuth:
|
|
66511
|
+
requiresAuth: state2.screenType?.toLowerCase() === "auth",
|
|
66445
66512
|
visitCount: state2.visited ? 1 : 0,
|
|
66446
66513
|
lastVisitedAt: state2.visited ? now : null,
|
|
66447
66514
|
elementsCovered: 0,
|
|
@@ -66461,6 +66528,8 @@ ${constraints.join("\n")}`);
|
|
|
66461
66528
|
const toPageId = idByScreen.get(t.toScreenId);
|
|
66462
66529
|
if (!fromPageId || !toPageId)
|
|
66463
66530
|
continue;
|
|
66531
|
+
const fromScreen = graph.screens.get(t.fromScreenId);
|
|
66532
|
+
const toScreen = graph.screens.get(t.toScreenId);
|
|
66464
66533
|
transitions.push({
|
|
66465
66534
|
id: `mtx-${txSeq++}`,
|
|
66466
66535
|
fromPageId,
|
|
@@ -66468,7 +66537,7 @@ ${constraints.join("\n")}`);
|
|
|
66468
66537
|
triggerType: t.action.toUpperCase(),
|
|
66469
66538
|
triggerLabel: t.viaRef ?? null,
|
|
66470
66539
|
observedCount: 1,
|
|
66471
|
-
requiresAuth:
|
|
66540
|
+
requiresAuth: fromScreen?.screenType?.toLowerCase() === "auth" || toScreen?.screenType?.toLowerCase() === "auth"
|
|
66472
66541
|
});
|
|
66473
66542
|
}
|
|
66474
66543
|
return {
|
|
@@ -66671,7 +66740,9 @@ ${constraints.join("\n")}`);
|
|
|
66671
66740
|
recordedJourneys,
|
|
66672
66741
|
credentials: input.credentials,
|
|
66673
66742
|
testBudget: input.testBudget,
|
|
66674
|
-
incremental: input.incremental
|
|
66743
|
+
incremental: input.incremental,
|
|
66744
|
+
userNotes: input.userNotes,
|
|
66745
|
+
featureContext: input.featureContext
|
|
66675
66746
|
})
|
|
66676
66747
|
});
|
|
66677
66748
|
const plannerFeatureGroups = filterFeatureGroupsToSiteMap(result.featureGroups, siteMap, input.graph, input.appId);
|
|
@@ -66723,6 +66794,9 @@ var require_mobile_generator = __commonJS({
|
|
|
66723
66794
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
66724
66795
|
"use strict";
|
|
66725
66796
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
66797
|
+
exports2.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
66798
|
+
exports2.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
66799
|
+
exports2.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
66726
66800
|
exports2.collapseRetryCycles = collapseRetryCycles;
|
|
66727
66801
|
exports2.runMobileGeneratePhase = runMobileGeneratePhase;
|
|
66728
66802
|
var crypto_1 = require("crypto");
|
|
@@ -66930,8 +67004,38 @@ var require_mobile_generator = __commonJS({
|
|
|
66930
67004
|
return (labelIsEditableValue ? void 0 : label) ?? nonEmpty2(element.accessibilityId) ?? nonEmpty2(element.resourceId) ?? (editable ? void 0 : text) ?? "element";
|
|
66931
67005
|
}
|
|
66932
67006
|
var EDITABLE_ELEMENT_ROLE_RE = /(EditText|AutoCompleteTextView|XCUIElementTypeTextField|XCUIElementTypeSecureTextField|XCUIElementTypeSearchField|XCUIElementTypeTextView)$/;
|
|
67007
|
+
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;
|
|
66933
67008
|
function isEditableMobileElement(element) {
|
|
66934
|
-
|
|
67009
|
+
if (EDITABLE_ELEMENT_ROLE_RE.test(element.role.trim()))
|
|
67010
|
+
return true;
|
|
67011
|
+
const identity = [element.accessibilityId, element.resourceId, element.label].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
67012
|
+
if (identity && EDITABLE_IDENTITY_RE.test(identity))
|
|
67013
|
+
return true;
|
|
67014
|
+
return false;
|
|
67015
|
+
}
|
|
67016
|
+
function isIdentityBearingTapTarget(element) {
|
|
67017
|
+
if (weakAssertionTargetReason(element))
|
|
67018
|
+
return false;
|
|
67019
|
+
if (isBareContainerLikeElement(element))
|
|
67020
|
+
return false;
|
|
67021
|
+
if (isEditableMobileElement(element))
|
|
67022
|
+
return false;
|
|
67023
|
+
if (/button|link|tab|menuitem|checkbox|switch|radio/i.test(element.role))
|
|
67024
|
+
return true;
|
|
67025
|
+
const label = [
|
|
67026
|
+
element.label,
|
|
67027
|
+
element.text,
|
|
67028
|
+
element.accessibilityId,
|
|
67029
|
+
element.resourceId
|
|
67030
|
+
].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
67031
|
+
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);
|
|
67032
|
+
}
|
|
67033
|
+
function isBareContainerLikeElement(element) {
|
|
67034
|
+
if (BARE_CONTAINER_CLASS_RE.test(element.role)) {
|
|
67035
|
+
const hasIdentity = Boolean(nonEmpty2(element.text) || nonEmpty2(element.label) || nonEmpty2(element.accessibilityId));
|
|
67036
|
+
return !hasIdentity;
|
|
67037
|
+
}
|
|
67038
|
+
return false;
|
|
66935
67039
|
}
|
|
66936
67040
|
function nonEmpty2(value) {
|
|
66937
67041
|
if (typeof value !== "string")
|
|
@@ -66963,7 +67067,7 @@ var require_mobile_generator = __commonJS({
|
|
|
66963
67067
|
## Your job
|
|
66964
67068
|
1. mobile_snapshot the current screen.
|
|
66965
67069
|
2. Execute the scenario's steps using the real UI: tap buttons, fill fields (mobile_type), submit, navigate.
|
|
66966
|
-
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
|
|
67070
|
+
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.
|
|
66967
67071
|
4. When you reach the target screen / complete the flow, PROVE the outcome with an assert tool:
|
|
66968
67072
|
- mobile_assert_visible({ref}) \u2014 the target screen's stable element is on screen.
|
|
66969
67073
|
- mobile_assert_text({ref, expected}) \u2014 an element contains expected text (only durable when the element has a stable accessibility-id / resource-id).
|
|
@@ -66978,6 +67082,8 @@ var require_mobile_generator = __commonJS({
|
|
|
66978
67082
|
- 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.
|
|
66979
67083
|
- 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.
|
|
66980
67084
|
- 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.
|
|
67085
|
+
- 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.
|
|
67086
|
+
- 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.
|
|
66981
67087
|
- 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.
|
|
66982
67088
|
- 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.
|
|
66983
67089
|
- 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.
|
|
@@ -66996,7 +67102,7 @@ This app has stored login credentials under these keys:
|
|
|
66996
67102
|
${keyList}
|
|
66997
67103
|
|
|
66998
67104
|
When a scenario needs a signed-in session, complete the FULL login sequence \u2014 filling the fields alone does NOT log you in:
|
|
66999
|
-
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.
|
|
67105
|
+
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).
|
|
67000
67106
|
|
|
67001
67107
|
SECURITY RULES:
|
|
67002
67108
|
- 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.
|
|
@@ -67346,6 +67452,7 @@ ${statePacket}` },
|
|
|
67346
67452
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
67347
67453
|
assertCount++;
|
|
67348
67454
|
},
|
|
67455
|
+
recordedActions: actions,
|
|
67349
67456
|
log: log2
|
|
67350
67457
|
});
|
|
67351
67458
|
pushResult(dispatchResult);
|
|
@@ -67361,7 +67468,7 @@ ${statePacket}` },
|
|
|
67361
67468
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
67362
67469
|
}
|
|
67363
67470
|
async function dispatchGenerateTool(args) {
|
|
67364
|
-
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, log: log2 } = args;
|
|
67471
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
|
|
67365
67472
|
const unknownRef = (ref) => ({
|
|
67366
67473
|
ok: false,
|
|
67367
67474
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -67423,12 +67530,26 @@ ${statePacket}` },
|
|
|
67423
67530
|
};
|
|
67424
67531
|
}
|
|
67425
67532
|
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
67426
|
-
|
|
67427
|
-
|
|
67533
|
+
const screenChanged = changedScreen(before, absorbed);
|
|
67534
|
+
const identityTap = isIdentityBearingTapTarget(element);
|
|
67535
|
+
if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
|
|
67536
|
+
const tapAction = {
|
|
67428
67537
|
type: "TAP",
|
|
67429
67538
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
67430
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
67431
|
-
|
|
67539
|
+
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
67540
|
+
innerText: null,
|
|
67541
|
+
url: "",
|
|
67542
|
+
value: null
|
|
67543
|
+
};
|
|
67544
|
+
const lastRecorded = recordedActions[recordedActions.length - 1];
|
|
67545
|
+
if (lastRecorded && actionCycleKey(lastRecorded) === actionCycleKey(tapAction)) {
|
|
67546
|
+
log2(` tap left content signature unchanged but duplicates the prior step \u2014 not recording again.`);
|
|
67547
|
+
} else if (!screenChanged && identityTap) {
|
|
67548
|
+
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67549
|
+
recordAction(tapAction);
|
|
67550
|
+
} else {
|
|
67551
|
+
recordAction(tapAction);
|
|
67552
|
+
}
|
|
67432
67553
|
} else if (shouldRecordAction(result, absorbed)) {
|
|
67433
67554
|
log2(" tap changed nothing on screen \u2014 not recording it as a step (dead action).");
|
|
67434
67555
|
}
|
|
@@ -67567,6 +67688,13 @@ ${statePacket}` },
|
|
|
67567
67688
|
const element = resolveRef(toolArgs.ref);
|
|
67568
67689
|
if (!element)
|
|
67569
67690
|
return unknownRef(toolArgs.ref);
|
|
67691
|
+
const weakReason = weakAssertionTargetReason(element);
|
|
67692
|
+
if (weakReason) {
|
|
67693
|
+
return {
|
|
67694
|
+
ok: false,
|
|
67695
|
+
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.`
|
|
67696
|
+
};
|
|
67697
|
+
}
|
|
67570
67698
|
const expected = nonEmpty2(typeof toolArgs.expected === "string" ? toolArgs.expected : String(toolArgs.expected ?? ""));
|
|
67571
67699
|
if (!expected)
|
|
67572
67700
|
return { ok: false, error: "mobile_assert_text needs a non-empty `expected` value." };
|
|
@@ -67765,6 +67893,105 @@ ${statePacket}` },
|
|
|
67765
67893
|
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67766
67894
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67767
67895
|
}
|
|
67896
|
+
var INSPECTION_OUTCOME_TYPES = /* @__PURE__ */ new Set([
|
|
67897
|
+
"element-visible",
|
|
67898
|
+
"text-visible",
|
|
67899
|
+
"element-hidden",
|
|
67900
|
+
"screen-visible"
|
|
67901
|
+
]);
|
|
67902
|
+
function isInspectionOnlyScenario(scenario, actions) {
|
|
67903
|
+
if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
|
|
67904
|
+
return false;
|
|
67905
|
+
if (!scenario.expectedOutcomes.every((o) => INSPECTION_OUTCOME_TYPES.has(o.type))) {
|
|
67906
|
+
return false;
|
|
67907
|
+
}
|
|
67908
|
+
if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
|
|
67909
|
+
return false;
|
|
67910
|
+
}
|
|
67911
|
+
const text = scenarioText(scenario);
|
|
67912
|
+
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);
|
|
67913
|
+
if (visibilityOnlyGoal) {
|
|
67914
|
+
const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67915
|
+
return assertCount2 > 0;
|
|
67916
|
+
}
|
|
67917
|
+
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);
|
|
67918
|
+
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);
|
|
67919
|
+
if (completionIntent && formIntent)
|
|
67920
|
+
return false;
|
|
67921
|
+
const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67922
|
+
return assertCount > 0;
|
|
67923
|
+
}
|
|
67924
|
+
function isNavigationFocusedScenario(scenario) {
|
|
67925
|
+
const text = scenarioText(scenario);
|
|
67926
|
+
const navigates = /\b(back|return|go\s+home|navigate\s+back|leave|exit|cancel\s+out|dismiss|previous\s+screen)\b/.test(text);
|
|
67927
|
+
const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
|
|
67928
|
+
return navigates && !completesFlow;
|
|
67929
|
+
}
|
|
67930
|
+
function isFormValidationScenario(scenario) {
|
|
67931
|
+
if (scenario.variant === "validation")
|
|
67932
|
+
return true;
|
|
67933
|
+
if (scenario.type !== "EDGE_CASE")
|
|
67934
|
+
return false;
|
|
67935
|
+
const text = scenarioText(scenario);
|
|
67936
|
+
return /\b(empty|required|missing|blank|invalid|reject|error|validation|wrong|incorrect|mismatch)\b/.test(text);
|
|
67937
|
+
}
|
|
67938
|
+
function isFormMutationHappyPath(scenario) {
|
|
67939
|
+
if (isNavigationFocusedScenario(scenario))
|
|
67940
|
+
return false;
|
|
67941
|
+
if (isFormValidationScenario(scenario))
|
|
67942
|
+
return false;
|
|
67943
|
+
const text = scenarioText(scenario);
|
|
67944
|
+
const isHappy = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|success)\b/.test(text);
|
|
67945
|
+
if (!isHappy)
|
|
67946
|
+
return false;
|
|
67947
|
+
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)) {
|
|
67948
|
+
return false;
|
|
67949
|
+
}
|
|
67950
|
+
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)) {
|
|
67951
|
+
return false;
|
|
67952
|
+
}
|
|
67953
|
+
const mutates = /\b(change|update|create|save|send|register|sign\s*up|checkout|edit|post|write|compose|set|add|delete|remove)\b/.test(text);
|
|
67954
|
+
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);
|
|
67955
|
+
return mutates && hasObject;
|
|
67956
|
+
}
|
|
67957
|
+
function isNavigationChromeTap(action) {
|
|
67958
|
+
if (action.type !== "TAP")
|
|
67959
|
+
return false;
|
|
67960
|
+
const textForAction = actionText(action);
|
|
67961
|
+
return /\b(go\s+back|back\s+to|←|return\s+to|cancel|close|dismiss|not\s+now|skip|maybe\s+later)\b/.test(textForAction);
|
|
67962
|
+
}
|
|
67963
|
+
function isAuthMethodChooserTap(action) {
|
|
67964
|
+
if (action.type !== "TAP")
|
|
67965
|
+
return false;
|
|
67966
|
+
const textForAction = actionText(action);
|
|
67967
|
+
if (/\b(submit|confirm|sign\s*in|log\s*in|login)\b/.test(textForAction) && !/\b(with|using|via)\b/.test(textForAction)) {
|
|
67968
|
+
return false;
|
|
67969
|
+
}
|
|
67970
|
+
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);
|
|
67971
|
+
}
|
|
67972
|
+
function isCredentialSubmitTap(action) {
|
|
67973
|
+
if (action.type !== "TAP")
|
|
67974
|
+
return false;
|
|
67975
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67976
|
+
return false;
|
|
67977
|
+
const textForAction = actionText(action);
|
|
67978
|
+
return /\b(sign\s*in|log\s*in|login|continue|submit|done|next|confirm)\b/.test(textForAction);
|
|
67979
|
+
}
|
|
67980
|
+
function isKeyboardFormCommit(action) {
|
|
67981
|
+
return action.type === "TYPE" && (action.value ?? "").endsWith("\n");
|
|
67982
|
+
}
|
|
67983
|
+
function isFormCommitAction(action) {
|
|
67984
|
+
if (isKeyboardFormCommit(action))
|
|
67985
|
+
return true;
|
|
67986
|
+
if (isCredentialSubmitTap(action))
|
|
67987
|
+
return true;
|
|
67988
|
+
if (action.type !== "TAP")
|
|
67989
|
+
return false;
|
|
67990
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67991
|
+
return false;
|
|
67992
|
+
const textForAction = actionText(action);
|
|
67993
|
+
return /\b(save|create|send|confirm|next|apply|post|checkout|pay|finish|update|register|sign\s*up|done|continue|submit)\b/i.test(textForAction);
|
|
67994
|
+
}
|
|
67768
67995
|
function actionText(action) {
|
|
67769
67996
|
const bundle = action.metadata.locatorBundle;
|
|
67770
67997
|
return [
|
|
@@ -67781,9 +68008,80 @@ ${statePacket}` },
|
|
|
67781
68008
|
bundle?.xpath
|
|
67782
68009
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67783
68010
|
}
|
|
68011
|
+
function isStructuralOnlyAssert(action) {
|
|
68012
|
+
if (action.type !== "ASSERT_VISIBLE" && action.type !== "ASSERT_TEXT")
|
|
68013
|
+
return false;
|
|
68014
|
+
const sel = (action.selector ?? "").trim();
|
|
68015
|
+
if (/^\/hierarchy(\[\d+\])?(\/android\.widget\.FrameLayout\[\d+\])*$/i.test(sel))
|
|
68016
|
+
return true;
|
|
68017
|
+
if (isStructuralAssertionIdentity(sel) || isStructuralAssertionIdentity(action.metadata.targetLabel)) {
|
|
68018
|
+
const bundle = action.metadata.locatorBundle;
|
|
68019
|
+
const human = Boolean(nonEmpty2(action.innerText) || nonEmpty2(action.metadata.targetLabel) || nonEmpty2(bundle?.text) || nonEmpty2(bundle?.contentDesc));
|
|
68020
|
+
if (!human)
|
|
68021
|
+
return true;
|
|
68022
|
+
}
|
|
68023
|
+
if (/^\/hierarchy/i.test(sel)) {
|
|
68024
|
+
const bundle = action.metadata.locatorBundle;
|
|
68025
|
+
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));
|
|
68026
|
+
if (!hasIdentity)
|
|
68027
|
+
return true;
|
|
68028
|
+
}
|
|
68029
|
+
return false;
|
|
68030
|
+
}
|
|
68031
|
+
function collapseConsecutiveBacks(actions) {
|
|
68032
|
+
if (actions.length <= 1)
|
|
68033
|
+
return actions;
|
|
68034
|
+
const out = [actions[0]];
|
|
68035
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68036
|
+
if (actions[i].type === "BACK" && out[out.length - 1].type === "BACK")
|
|
68037
|
+
continue;
|
|
68038
|
+
out.push(actions[i]);
|
|
68039
|
+
}
|
|
68040
|
+
return out;
|
|
68041
|
+
}
|
|
67784
68042
|
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67785
68043
|
const text = scenarioText(scenario);
|
|
67786
|
-
|
|
68044
|
+
if (isInspectionOnlyScenario(scenario, actions)) {
|
|
68045
|
+
return null;
|
|
68046
|
+
}
|
|
68047
|
+
if (isNavigationFocusedScenario(scenario)) {
|
|
68048
|
+
return null;
|
|
68049
|
+
}
|
|
68050
|
+
if (isFormValidationScenario(scenario)) {
|
|
68051
|
+
const typedFields = actions.some((a) => a.type === "TYPE");
|
|
68052
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68053
|
+
const hasValidationAssert = actions.some((a) => {
|
|
68054
|
+
if (a.type !== "ASSERT_VISIBLE" && a.type !== "ASSERT_TEXT")
|
|
68055
|
+
return false;
|
|
68056
|
+
return /\b(error|required|invalid|empty|missing|incorrect|wrong|try again|mismatch|does not match|must match)\b/.test(actionText(a));
|
|
68057
|
+
});
|
|
68058
|
+
const emptyFieldIntent = /\b(empty|required|missing|blank)\b/.test(text);
|
|
68059
|
+
const invalidValueIntent = /\b(wrong|incorrect|invalid|mismatch|reject)\b/.test(text);
|
|
68060
|
+
if (emptyFieldIntent && !invalidValueIntent) {
|
|
68061
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68062
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68063
|
+
}
|
|
68064
|
+
return null;
|
|
68065
|
+
}
|
|
68066
|
+
if (typedFields && !hasCommit) {
|
|
68067
|
+
return "validation scenario filled fields but never committed the form";
|
|
68068
|
+
}
|
|
68069
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68070
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68071
|
+
}
|
|
68072
|
+
return null;
|
|
68073
|
+
}
|
|
68074
|
+
if (isFormMutationHappyPath(scenario)) {
|
|
68075
|
+
const hasType = actions.some((a) => a.type === "TYPE");
|
|
68076
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68077
|
+
if (!hasType) {
|
|
68078
|
+
return "form-mutation happy path did not record any field entry";
|
|
68079
|
+
}
|
|
68080
|
+
if (!hasCommit) {
|
|
68081
|
+
return "form-mutation happy path did not record a form commit";
|
|
68082
|
+
}
|
|
68083
|
+
}
|
|
68084
|
+
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);
|
|
67787
68085
|
const typedEmailIndexes = [];
|
|
67788
68086
|
const typedPasswordIndexes = [];
|
|
67789
68087
|
actions.forEach((action, index) => {
|
|
@@ -67800,19 +68098,19 @@ ${statePacket}` },
|
|
|
67800
68098
|
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67801
68099
|
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67802
68100
|
const submitAfterCredentials = actions.some((action, index) => {
|
|
67803
|
-
if (
|
|
68101
|
+
if (index < lastCredentialIndex)
|
|
67804
68102
|
return false;
|
|
67805
|
-
|
|
67806
|
-
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
68103
|
+
if (!isFormCommitAction(action))
|
|
67807
68104
|
return false;
|
|
67808
|
-
|
|
68105
|
+
if (action.type === "TYPE" && index === lastCredentialIndex)
|
|
68106
|
+
return true;
|
|
68107
|
+
return index > lastCredentialIndex;
|
|
67809
68108
|
});
|
|
67810
68109
|
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67811
68110
|
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67812
|
-
return "credential fields were filled but no
|
|
68111
|
+
return "credential fields were filled but no form commit was recorded after them";
|
|
67813
68112
|
}
|
|
67814
68113
|
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67815
|
-
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67816
68114
|
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67817
68115
|
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67818
68116
|
if (action.type !== "TAP")
|
|
@@ -67820,29 +68118,70 @@ ${statePacket}` },
|
|
|
67820
68118
|
const textForAction = actionText(action);
|
|
67821
68119
|
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67822
68120
|
return false;
|
|
68121
|
+
if (isAuthMethodChooserTap(action) || isNavigationChromeTap(action))
|
|
68122
|
+
return false;
|
|
67823
68123
|
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67824
68124
|
});
|
|
67825
|
-
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario &&
|
|
68125
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67826
68126
|
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67827
68127
|
}
|
|
67828
|
-
if (
|
|
68128
|
+
if (isCredentialSignIn) {
|
|
68129
|
+
const typedEmail = typedEmailIndexes.length > 0;
|
|
68130
|
+
const typedPassword = typedPasswordIndexes.length > 0;
|
|
68131
|
+
const tappedSubmit = submitAfterCredentials;
|
|
68132
|
+
if (typedEmail && typedPassword && tappedSubmit) {
|
|
68133
|
+
} else {
|
|
68134
|
+
const missing = [
|
|
68135
|
+
typedEmail ? null : "email/username entry",
|
|
68136
|
+
typedPassword ? null : "password entry",
|
|
68137
|
+
tappedSubmit ? null : "form commit (submit tap or keyboard Enter)"
|
|
68138
|
+
].filter((value) => value !== null);
|
|
68139
|
+
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
68140
|
+
}
|
|
68141
|
+
}
|
|
68142
|
+
const isHappyPath = /\b(happy\s*path|successful|success|create|save|submit|complete|checkout|send|post)\b/.test(text) || scenario.type === "HAPPY_PATH";
|
|
68143
|
+
if (!isHappyPath)
|
|
67829
68144
|
return null;
|
|
67830
|
-
|
|
67831
|
-
|
|
67832
|
-
|
|
67833
|
-
|
|
68145
|
+
let lastTypeIndex = -1;
|
|
68146
|
+
let lastAssertIndex = -1;
|
|
68147
|
+
actions.forEach((action, index) => {
|
|
68148
|
+
if (action.type === "TYPE")
|
|
68149
|
+
lastTypeIndex = index;
|
|
68150
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
68151
|
+
lastAssertIndex = index;
|
|
68152
|
+
});
|
|
68153
|
+
if (lastTypeIndex < 0 || lastAssertIndex <= lastTypeIndex)
|
|
67834
68154
|
return null;
|
|
67835
|
-
const
|
|
67836
|
-
|
|
67837
|
-
|
|
67838
|
-
|
|
67839
|
-
|
|
67840
|
-
|
|
68155
|
+
const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
|
|
68156
|
+
const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
|
|
68157
|
+
const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
|
|
68158
|
+
const lastTypeAction = actions[lastTypeIndex];
|
|
68159
|
+
const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
|
|
68160
|
+
const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
|
|
68161
|
+
if (index <= lastTypeIndex || index >= lastAssertIndex)
|
|
68162
|
+
return false;
|
|
68163
|
+
return isFormCommitAction(action);
|
|
68164
|
+
});
|
|
68165
|
+
if (screensDiffer && !commitAfterType) {
|
|
68166
|
+
return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
|
|
68167
|
+
}
|
|
68168
|
+
return null;
|
|
67841
68169
|
}
|
|
67842
68170
|
function actionCycleKey(action) {
|
|
67843
68171
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67844
68172
|
}
|
|
67845
68173
|
var MAX_RETRY_CYCLE_LENGTH = 8;
|
|
68174
|
+
function dedupeConsecutiveIdenticalActions(actions) {
|
|
68175
|
+
if (actions.length <= 1)
|
|
68176
|
+
return actions;
|
|
68177
|
+
const out = [actions[0]];
|
|
68178
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68179
|
+
if (actionCycleKey(actions[i]) === actionCycleKey(out[out.length - 1]))
|
|
68180
|
+
continue;
|
|
68181
|
+
out.push(actions[i]);
|
|
68182
|
+
}
|
|
68183
|
+
return out;
|
|
68184
|
+
}
|
|
67846
68185
|
function collapseRetryCycles(actions) {
|
|
67847
68186
|
const out = [...actions];
|
|
67848
68187
|
let changed = true;
|
|
@@ -67883,15 +68222,30 @@ ${statePacket}` },
|
|
|
67883
68222
|
}
|
|
67884
68223
|
function assembleTest(args) {
|
|
67885
68224
|
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
67886
|
-
let actions =
|
|
68225
|
+
let actions = dedupeConsecutiveIdenticalActions(recording.actions);
|
|
67887
68226
|
if (actions.length < recording.actions.length) {
|
|
67888
|
-
log2(`
|
|
68227
|
+
log2(` deduped ${recording.actions.length - actions.length} consecutive identical step(s) (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
68228
|
+
}
|
|
68229
|
+
const beforeBackCollapse = actions.length;
|
|
68230
|
+
actions = collapseConsecutiveBacks(actions);
|
|
68231
|
+
if (actions.length < beforeBackCollapse) {
|
|
68232
|
+
log2(` collapsed ${beforeBackCollapse - actions.length} consecutive BACK step(s) (${beforeBackCollapse} \u2192 ${actions.length}).`);
|
|
68233
|
+
}
|
|
68234
|
+
const beforeCycleCollapse = actions.length;
|
|
68235
|
+
actions = collapseRetryCycles(actions);
|
|
68236
|
+
if (actions.length < beforeCycleCollapse) {
|
|
68237
|
+
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
67889
68238
|
}
|
|
67890
68239
|
const beforeContainerDrop = actions.length;
|
|
67891
68240
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
67892
68241
|
if (actions.length < beforeContainerDrop) {
|
|
67893
68242
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
67894
68243
|
}
|
|
68244
|
+
const beforeWeakAssertDrop = actions.length;
|
|
68245
|
+
actions = actions.filter((a) => !isStructuralOnlyAssert(a));
|
|
68246
|
+
if (actions.length < beforeWeakAssertDrop) {
|
|
68247
|
+
log2(` dropped ${beforeWeakAssertDrop - actions.length} structural-only assert step(s) (no product identity).`);
|
|
68248
|
+
}
|
|
67895
68249
|
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
67896
68250
|
if (intentMismatch) {
|
|
67897
68251
|
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
@@ -68703,10 +69057,22 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68703
69057
|
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68704
69058
|
return false;
|
|
68705
69059
|
const text = mobileStepSearchText(step);
|
|
68706
|
-
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|
|
|
69060
|
+
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|go\s+back|back\s+to|cancel|close|dismiss)\b/.test(text)) {
|
|
68707
69061
|
return false;
|
|
68708
69062
|
}
|
|
68709
|
-
|
|
69063
|
+
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)) {
|
|
69064
|
+
return false;
|
|
69065
|
+
}
|
|
69066
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit|done|next|confirm)\b/.test(text);
|
|
69067
|
+
}
|
|
69068
|
+
function isKeyboardFormCommitStep(step) {
|
|
69069
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
69070
|
+
return false;
|
|
69071
|
+
const value = typeof step.value === "string" ? step.value : "";
|
|
69072
|
+
return value.endsWith("\n");
|
|
69073
|
+
}
|
|
69074
|
+
function isFormCommitStep(step) {
|
|
69075
|
+
return isCredentialSubmitTap(step) || isKeyboardFormCommitStep(step);
|
|
68710
69076
|
}
|
|
68711
69077
|
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68712
69078
|
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
@@ -68731,20 +69097,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68731
69097
|
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68732
69098
|
return false;
|
|
68733
69099
|
const text = mobileStepSearchText(step);
|
|
68734
|
-
return /\b(
|
|
69100
|
+
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);
|
|
68735
69101
|
});
|
|
68736
69102
|
if (hasCredentialFormEvidence)
|
|
68737
69103
|
return null;
|
|
68738
69104
|
return "credential-control test edits a credential field without recording a path to the email/password form; stale auth-control test is invalid until generation records a complete form path";
|
|
68739
69105
|
}
|
|
68740
|
-
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder &&
|
|
69106
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isFormCommitStep(step));
|
|
68741
69107
|
if (hasSubmitAfterCredentialEdit)
|
|
68742
69108
|
return null;
|
|
69109
|
+
const hasKeyboardCommitOnCredential = steps.some((step) => (step.order ?? -1) === lastCredentialEditOrder && isKeyboardFormCommitStep(step));
|
|
69110
|
+
if (hasKeyboardCommitOnCredential)
|
|
69111
|
+
return null;
|
|
68743
69112
|
const hasValidationAssertion = steps.some((step) => {
|
|
68744
69113
|
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68745
69114
|
return false;
|
|
68746
69115
|
const text = mobileStepSearchText(step);
|
|
68747
|
-
return /\b(invalid|required|error|empty|missing|incorrect|wrong|try again|validation|must enter|enter (an )?(email|password)|email is required|password is required)\b/.test(text);
|
|
69116
|
+
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);
|
|
68748
69117
|
});
|
|
68749
69118
|
if (hasValidationAssertion)
|
|
68750
69119
|
return null;
|
|
@@ -68772,8 +69141,21 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68772
69141
|
function coerceSteps(value) {
|
|
68773
69142
|
if (!Array.isArray(value) || value.length === 0)
|
|
68774
69143
|
return void 0;
|
|
68775
|
-
const
|
|
68776
|
-
|
|
69144
|
+
const steps = [];
|
|
69145
|
+
for (let i = 0; i < value.length; i++) {
|
|
69146
|
+
const step = value[i];
|
|
69147
|
+
if (!isRecord(step) || typeof step.action !== "string" || !step.action.trim()) {
|
|
69148
|
+
return void 0;
|
|
69149
|
+
}
|
|
69150
|
+
const orderRaw = step.order;
|
|
69151
|
+
const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : i;
|
|
69152
|
+
steps.push({
|
|
69153
|
+
...step,
|
|
69154
|
+
order,
|
|
69155
|
+
action: step.action
|
|
69156
|
+
});
|
|
69157
|
+
}
|
|
69158
|
+
return steps.map((step, order) => ({ ...step, order }));
|
|
68777
69159
|
}
|
|
68778
69160
|
function parseHealVerdict(content) {
|
|
68779
69161
|
const cleaned = stripCodeFences(content);
|
|
@@ -72994,6 +73376,48 @@ var require_snapshot_observation = __commonJS({
|
|
|
72994
73376
|
var PROMOTED_FORM_FIELD_CAP = 12;
|
|
72995
73377
|
var FORM_MEMBER_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:form|group)\b/i;
|
|
72996
73378
|
var DATA_GRID_CONTAINER_SEGMENT_RE = /(?:^|>\s*)(?:table|grid|rowgroup|row|gridcell|cell)\b/i;
|
|
73379
|
+
var DEEP_NONNAV_CAP_CENSUS = 16;
|
|
73380
|
+
var CENSUS_NAV_LINK_CAP = 60;
|
|
73381
|
+
var CENSUS_MIN_ACTION_GROUP = 3;
|
|
73382
|
+
var DESTRUCTIVE_ACTION_VERB_RE = /^(?:delete|remove|destroy|suspend|deactivate|disable|revoke|ban|block|archive|terminate|reject|cancel|reset|purge|wipe|unpublish|unsubscribe)$/i;
|
|
73383
|
+
function leadingActionVerb(label) {
|
|
73384
|
+
if (!label)
|
|
73385
|
+
return null;
|
|
73386
|
+
const m = label.trim().match(/[A-Za-z]+/);
|
|
73387
|
+
return m ? m[0].toLowerCase() : null;
|
|
73388
|
+
}
|
|
73389
|
+
function isNavLinkEntry(entry) {
|
|
73390
|
+
return entry.element.elementType === "LINK" && !!entry.element.triggersNavigation;
|
|
73391
|
+
}
|
|
73392
|
+
function isRowRepeatedControl(element) {
|
|
73393
|
+
return !!element.enrichment?.rowContext || !!element.containerPath && DATA_GRID_CONTAINER_SEGMENT_RE.test(element.containerPath);
|
|
73394
|
+
}
|
|
73395
|
+
function rowContainerScope(containerPath) {
|
|
73396
|
+
if (!containerPath)
|
|
73397
|
+
return null;
|
|
73398
|
+
const m = containerPath.match(/^(.*?)\s*>\s*row\b/i);
|
|
73399
|
+
return m ? m[1].trim() : containerPath.trim();
|
|
73400
|
+
}
|
|
73401
|
+
function containerScopeLabel(scope) {
|
|
73402
|
+
if (!scope)
|
|
73403
|
+
return null;
|
|
73404
|
+
const landmarks = [...scope.matchAll(/(?:table|grid|treegrid|list|listbox|tree|rowgroup):([^>]+)/gi)];
|
|
73405
|
+
if (landmarks.length > 0)
|
|
73406
|
+
return landmarks[landmarks.length - 1][1].trim();
|
|
73407
|
+
const segs = scope.split(">").map((s) => s.trim()).filter(Boolean);
|
|
73408
|
+
return segs.length ? segs[segs.length - 1] : null;
|
|
73409
|
+
}
|
|
73410
|
+
function rowIdentityKey(element) {
|
|
73411
|
+
const rc = element.enrichment?.rowContext;
|
|
73412
|
+
if (rc)
|
|
73413
|
+
return `rc:${rc}`;
|
|
73414
|
+
const cp = element.containerPath;
|
|
73415
|
+
if (cp) {
|
|
73416
|
+
const m = cp.match(/>\s*row:(.+)$/i);
|
|
73417
|
+
return m ? `row:${m[1].trim()}` : `cp:${cp}`;
|
|
73418
|
+
}
|
|
73419
|
+
return element.ref ? `ref:${element.ref}` : "unknown";
|
|
73420
|
+
}
|
|
72997
73421
|
function isGroupedFormField(element) {
|
|
72998
73422
|
if (!FORM_CONTROL_ROLE_RE.test(element.role ?? ""))
|
|
72999
73423
|
return false;
|
|
@@ -73009,6 +73433,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
73009
73433
|
const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
|
|
73010
73434
|
const artifact = page.snapshotArtifact;
|
|
73011
73435
|
const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
|
|
73436
|
+
const rowActionCensus = richRender && options.mode !== "survey" && (options.rowActionCensus ?? process.env.DISCOVERY_SNAPSHOT_ROW_CENSUS === "true");
|
|
73012
73437
|
const activeScope = inferActiveSnapshotScope(page);
|
|
73013
73438
|
const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext({ ...context, richRank: richRender }, activeScope));
|
|
73014
73439
|
const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
|
|
@@ -73022,10 +73447,17 @@ var require_snapshot_observation = __commonJS({
|
|
|
73022
73447
|
});
|
|
73023
73448
|
let interactive;
|
|
73024
73449
|
if (options.mode === "survey") {
|
|
73025
|
-
const navLinks = allInteractive.filter(
|
|
73026
|
-
const others = allInteractive.filter((
|
|
73450
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73451
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
73027
73452
|
const othersCap = richRender ? 6 : 4;
|
|
73028
73453
|
interactive = [...navLinks, ...others.slice(0, othersCap)];
|
|
73454
|
+
} else if (rowActionCensus) {
|
|
73455
|
+
const navLinks = allInteractive.filter(isNavLinkEntry);
|
|
73456
|
+
const others = allInteractive.filter((entry) => !isNavLinkEntry(entry));
|
|
73457
|
+
interactive = [
|
|
73458
|
+
...navLinks.slice(0, CENSUS_NAV_LINK_CAP),
|
|
73459
|
+
...others.slice(0, options.elementLimit ?? DEEP_NONNAV_CAP_CENSUS)
|
|
73460
|
+
];
|
|
73029
73461
|
} else {
|
|
73030
73462
|
interactive = allInteractive.slice(0, options.elementLimit ?? (richRender ? 8 : 6));
|
|
73031
73463
|
}
|
|
@@ -73038,6 +73470,36 @@ var require_snapshot_observation = __commonJS({
|
|
|
73038
73470
|
}
|
|
73039
73471
|
}
|
|
73040
73472
|
}
|
|
73473
|
+
if (rowActionCensus && interactive.length > 0) {
|
|
73474
|
+
const shownVerbs = new Set(interactive.filter((entry) => !isNavLinkEntry(entry) && isRowRepeatedControl(entry.element)).map(({ element }) => leadingActionVerb(qualifyElementLabel(element))).filter(Boolean));
|
|
73475
|
+
const bestByVerb = /* @__PURE__ */ new Map();
|
|
73476
|
+
for (const entry of allInteractive) {
|
|
73477
|
+
if (isNavLinkEntry(entry) || !isRowRepeatedControl(entry.element))
|
|
73478
|
+
continue;
|
|
73479
|
+
const verb = leadingActionVerb(qualifyElementLabel(entry.element));
|
|
73480
|
+
if (!verb || !DESTRUCTIVE_ACTION_VERB_RE.test(verb))
|
|
73481
|
+
continue;
|
|
73482
|
+
if (shownVerbs.has(verb))
|
|
73483
|
+
continue;
|
|
73484
|
+
if (interactive.includes(entry))
|
|
73485
|
+
continue;
|
|
73486
|
+
const existing = bestByVerb.get(verb);
|
|
73487
|
+
if (!existing) {
|
|
73488
|
+
bestByVerb.set(verb, entry);
|
|
73489
|
+
continue;
|
|
73490
|
+
}
|
|
73491
|
+
if (existing.element.isDisabled && !entry.element.isDisabled)
|
|
73492
|
+
bestByVerb.set(verb, entry);
|
|
73493
|
+
}
|
|
73494
|
+
const swapIns = [...bestByVerb.values()];
|
|
73495
|
+
if (swapIns.length > 0) {
|
|
73496
|
+
const navShown = interactive.filter(isNavLinkEntry);
|
|
73497
|
+
const nonNavShown = interactive.filter((entry) => !isNavLinkEntry(entry));
|
|
73498
|
+
const evictable = Math.min(swapIns.length, nonNavShown.length);
|
|
73499
|
+
const trimmed = nonNavShown.slice(0, nonNavShown.length - evictable);
|
|
73500
|
+
interactive = [...navShown, ...trimmed, ...swapIns.slice(0, evictable)];
|
|
73501
|
+
}
|
|
73502
|
+
}
|
|
73041
73503
|
const duplicates = detectDuplicateRoleNameGroups(duplicateScanCandidates);
|
|
73042
73504
|
const allForms = artifact?.formGroups ?? page.formGroups ?? [];
|
|
73043
73505
|
const forms = allForms.slice(0, options.formLimit ?? 3);
|
|
@@ -73337,6 +73799,54 @@ var require_snapshot_observation = __commonJS({
|
|
|
73337
73799
|
const hidden = allInteractive.length - interactive.length;
|
|
73338
73800
|
if (hidden > 0) {
|
|
73339
73801
|
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.`);
|
|
73802
|
+
if (rowActionCensus) {
|
|
73803
|
+
const shownSet = new Set(interactive.map((entry) => entry.element));
|
|
73804
|
+
const groups = /* @__PURE__ */ new Map();
|
|
73805
|
+
for (const entry of allInteractive) {
|
|
73806
|
+
const el = entry.element;
|
|
73807
|
+
if (!isRowRepeatedControl(el))
|
|
73808
|
+
continue;
|
|
73809
|
+
const verb = leadingActionVerb(qualifyElementLabel(el));
|
|
73810
|
+
if (!verb)
|
|
73811
|
+
continue;
|
|
73812
|
+
const role = (el.role ?? el.elementType ?? "control").toLowerCase();
|
|
73813
|
+
const scope = rowContainerScope(el.containerPath);
|
|
73814
|
+
const key = JSON.stringify([scope ?? "", role, verb]);
|
|
73815
|
+
let group = groups.get(key);
|
|
73816
|
+
if (!group) {
|
|
73817
|
+
group = { verb, role, scopeLabel: containerScopeLabel(scope), rows: /* @__PURE__ */ new Set(), shownRows: /* @__PURE__ */ new Set(), disabledRows: /* @__PURE__ */ new Set(), controls: 0 };
|
|
73818
|
+
groups.set(key, group);
|
|
73819
|
+
}
|
|
73820
|
+
const rowKey = rowIdentityKey(el);
|
|
73821
|
+
group.rows.add(rowKey);
|
|
73822
|
+
group.controls += 1;
|
|
73823
|
+
if (shownSet.has(el))
|
|
73824
|
+
group.shownRows.add(rowKey);
|
|
73825
|
+
if (el.isDisabled)
|
|
73826
|
+
group.disabledRows.add(rowKey);
|
|
73827
|
+
}
|
|
73828
|
+
const collapsed = [...groups.values()].map((g) => ({
|
|
73829
|
+
verb: g.verb,
|
|
73830
|
+
role: g.role,
|
|
73831
|
+
scopeLabel: g.scopeLabel,
|
|
73832
|
+
total: g.rows.size,
|
|
73833
|
+
shown: g.shownRows.size,
|
|
73834
|
+
disabled: g.disabledRows.size,
|
|
73835
|
+
controls: g.controls
|
|
73836
|
+
})).filter((g) => g.total >= CENSUS_MIN_ACTION_GROUP && g.total > g.shown).sort((a, b) => {
|
|
73837
|
+
const aDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(a.verb) ? 0 : 1;
|
|
73838
|
+
const bDestructive = DESTRUCTIVE_ACTION_VERB_RE.test(b.verb) ? 0 : 1;
|
|
73839
|
+
return aDestructive - bDestructive || b.total - b.shown - (a.total - a.shown);
|
|
73840
|
+
});
|
|
73841
|
+
for (const g of collapsed) {
|
|
73842
|
+
const verbLabel = g.verb.charAt(0).toUpperCase() + g.verb.slice(1);
|
|
73843
|
+
const destructiveTag = DESTRUCTIVE_ACTION_VERB_RE.test(g.verb) ? " [destructive]" : "";
|
|
73844
|
+
const where = g.scopeLabel ? ` in ${yamlScalar(g.scopeLabel)}` : "";
|
|
73845
|
+
const controlsNote = g.controls > g.total ? ` (${g.controls} controls across ${g.total} rows)` : "";
|
|
73846
|
+
const disabledNote = g.disabled > 0 ? `; ${g.disabled} disabled at capture` : "";
|
|
73847
|
+
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.`);
|
|
73848
|
+
}
|
|
73849
|
+
}
|
|
73340
73850
|
}
|
|
73341
73851
|
}
|
|
73342
73852
|
}
|
|
@@ -74359,13 +74869,14 @@ var require_auth_recipe = __commonJS({
|
|
|
74359
74869
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
74360
74870
|
"use strict";
|
|
74361
74871
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
74362
|
-
exports2.AUTH_MARKER_VERSION = void 0;
|
|
74872
|
+
exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
|
|
74363
74873
|
exports2.computeRecipeHash = computeRecipeHash;
|
|
74364
74874
|
exports2.buildAuthRecipe = buildAuthRecipe;
|
|
74365
74875
|
exports2.authProofLocator = authProofLocator;
|
|
74366
74876
|
exports2.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
74367
74877
|
exports2.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
74368
74878
|
exports2.renderAuthFn = renderAuthFn;
|
|
74879
|
+
exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
74369
74880
|
exports2.absoluteRoute = absoluteRoute;
|
|
74370
74881
|
exports2.renderReplayClickArrow = renderReplayClickArrow;
|
|
74371
74882
|
exports2.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
@@ -74484,6 +74995,17 @@ var require_auth_recipe = __commonJS({
|
|
|
74484
74995
|
lines.push(END_MARKER);
|
|
74485
74996
|
return lines.join("\n");
|
|
74486
74997
|
}
|
|
74998
|
+
exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
74999
|
+
"AUTH_FILE",
|
|
75000
|
+
"SHARED_AUTH_INJECT",
|
|
75001
|
+
"VALIDATE_RUN_ID",
|
|
75002
|
+
"VALIDATE_TASK_ARN",
|
|
75003
|
+
"VALIDATE_RUNNER_AUTH",
|
|
75004
|
+
"VALIDATE_SERVER_URL"
|
|
75005
|
+
]);
|
|
75006
|
+
function isReservedRunnerEnvVar(key) {
|
|
75007
|
+
return key.startsWith("VALIDATE_") || exports2.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
75008
|
+
}
|
|
74487
75009
|
function absoluteRoute(route2, baseUrl) {
|
|
74488
75010
|
try {
|
|
74489
75011
|
return new URL(route2, baseUrl).href;
|
|
@@ -315588,9 +316110,15 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
315588
316110
|
log2(`
|
|
315589
316111
|
Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exploreStats.elementsFound} elements, ${explore.exploreStats.transitionsFound} transitions (${exploreElapsed}s)`);
|
|
315590
316112
|
if (callbacks?.onExploreComplete) {
|
|
315591
|
-
|
|
315592
|
-
|
|
315593
|
-
})
|
|
316113
|
+
try {
|
|
316114
|
+
await callbacks.onExploreComplete(toExploreCompleteData(explore, collectedTransitions));
|
|
316115
|
+
} catch (err) {
|
|
316116
|
+
const message = checkpointErrorMessage(err);
|
|
316117
|
+
log2(`[stream] explore-complete callback failed: ${message}`);
|
|
316118
|
+
if (isTerminalDiscoveryCheckpointError(err)) {
|
|
316119
|
+
throw err;
|
|
316120
|
+
}
|
|
316121
|
+
}
|
|
315594
316122
|
}
|
|
315595
316123
|
if (!isSurvey && shouldPauseAfterTruncatedExplore(explore)) {
|
|
315596
316124
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
@@ -315618,8 +316146,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315618
316146
|
pausePhase: "explore"
|
|
315619
316147
|
};
|
|
315620
316148
|
}
|
|
315621
|
-
|
|
315622
|
-
|
|
316149
|
+
if (!isSurvey) {
|
|
316150
|
+
log2("\nRelaunching app for a clean state before planning/generation...");
|
|
316151
|
+
await driver.relaunch();
|
|
316152
|
+
}
|
|
315623
316153
|
log2("\n\u2500\u2500 Phase 2: PLAN \u2500\u2500");
|
|
315624
316154
|
const graph = state2.toScreenGraph();
|
|
315625
316155
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
@@ -315642,7 +316172,9 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
315642
316172
|
hasPassword: !!ctx.variableContext?.allTestVariables?.TEST_USER_PASSWORD || !!ctx.variableContext?.publicTestVariables?.TEST_USER_PASSWORD
|
|
315643
316173
|
},
|
|
315644
316174
|
incremental: ctx.incremental,
|
|
315645
|
-
testBudget
|
|
316175
|
+
testBudget,
|
|
316176
|
+
userNotes: ctx.userNotes,
|
|
316177
|
+
featureContext: ctx.featureContext
|
|
315646
316178
|
}, {
|
|
315647
316179
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
315648
316180
|
onAICall
|
|
@@ -315815,10 +316347,20 @@ Fatal error: ${msg}`);
|
|
|
315815
316347
|
log2(`App-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
315816
316348
|
return false;
|
|
315817
316349
|
}) ?? false;
|
|
315818
|
-
if (
|
|
315819
|
-
|
|
316350
|
+
if (cleared)
|
|
316351
|
+
return true;
|
|
316352
|
+
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
316353
|
+
log2("CLEAR_APP_DATA requested but app-data reset was unavailable; a post-explore relaunch will reset state before planning.");
|
|
316354
|
+
}
|
|
316355
|
+
if (mode !== "survey") {
|
|
316356
|
+
return false;
|
|
315820
316357
|
}
|
|
315821
|
-
|
|
316358
|
+
try {
|
|
316359
|
+
await driver.relaunch();
|
|
316360
|
+
} catch (err) {
|
|
316361
|
+
log2(`App relaunch failed after unsuccessful app-data reset: ${err instanceof Error ? err.message : String(err)}`);
|
|
316362
|
+
}
|
|
316363
|
+
return false;
|
|
315822
316364
|
}
|
|
315823
316365
|
function shouldPauseAfterTruncatedExplore(explore) {
|
|
315824
316366
|
return explore.truncated === true && (explore.truncatedReason === "rate_limit" || explore.truncatedReason === "timeout");
|
|
@@ -315865,6 +316407,7 @@ var require_discover_agent = __commonJS({
|
|
|
315865
316407
|
var claude_discovery_runner_js_1 = require_claude_discovery_runner();
|
|
315866
316408
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
315867
316409
|
var canvas_detector_js_1 = require_canvas_detector();
|
|
316410
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
315868
316411
|
async function runCanvasPreflight(ctx, sharedBrowser, log2) {
|
|
315869
316412
|
const cached = ctx.appRenderType ?? null;
|
|
315870
316413
|
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
@@ -316560,7 +317103,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
316560
317103
|
}
|
|
316561
317104
|
if (envRefs.size > 0) {
|
|
316562
317105
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
316563
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
317106
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
316564
317107
|
if (missing.length > 0) {
|
|
316565
317108
|
autoVariableDefaults = {};
|
|
316566
317109
|
for (const key of missing) {
|
|
@@ -317076,7 +317619,7 @@ Health: ${healthSummary.totalIssues} issue(s) - ${healthSummary.consoleErrors.le
|
|
|
317076
317619
|
envRefs.add(match[1]);
|
|
317077
317620
|
if (envRefs.size > 0) {
|
|
317078
317621
|
const existingKeys = new Set(ctx.variableContext.availableVariableKeys);
|
|
317079
|
-
const missing = [...envRefs].filter((k) => !existingKeys.has(k));
|
|
317622
|
+
const missing = [...envRefs].filter((k) => !existingKeys.has(k) && !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(k));
|
|
317080
317623
|
if (missing.length > 0) {
|
|
317081
317624
|
autoVariableDefaults = {};
|
|
317082
317625
|
for (const key of missing) {
|
|
@@ -329958,9 +330501,9 @@ var require_dist2 = __commonJS({
|
|
|
329958
330501
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
329959
330502
|
};
|
|
329960
330503
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
329961
|
-
exports2.
|
|
329962
|
-
exports2.
|
|
329963
|
-
exports2.UNSUPPORTED_RENDERER_PREFIX = void 0;
|
|
330504
|
+
exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.isReservedRunnerEnvVar = exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.canonicalizeRoute = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
|
|
330505
|
+
exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = void 0;
|
|
330506
|
+
exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = void 0;
|
|
329964
330507
|
__exportStar(require_ai_provider(), exports2);
|
|
329965
330508
|
__exportStar(require_mobile_types(), exports2);
|
|
329966
330509
|
__exportStar(require_mobile_source_parser(), exports2);
|
|
@@ -330097,6 +330640,12 @@ var require_dist2 = __commonJS({
|
|
|
330097
330640
|
Object.defineProperty(exports2, "AUTH_MARKER_VERSION", { enumerable: true, get: function() {
|
|
330098
330641
|
return auth_recipe_js_1.AUTH_MARKER_VERSION;
|
|
330099
330642
|
} });
|
|
330643
|
+
Object.defineProperty(exports2, "RESERVED_RUNNER_ENV_VARS", { enumerable: true, get: function() {
|
|
330644
|
+
return auth_recipe_js_1.RESERVED_RUNNER_ENV_VARS;
|
|
330645
|
+
} });
|
|
330646
|
+
Object.defineProperty(exports2, "isReservedRunnerEnvVar", { enumerable: true, get: function() {
|
|
330647
|
+
return auth_recipe_js_1.isReservedRunnerEnvVar;
|
|
330648
|
+
} });
|
|
330100
330649
|
var auth_capture_run_js_1 = require_auth_capture_run();
|
|
330101
330650
|
Object.defineProperty(exports2, "runLoginCaptureRun", { enumerable: true, get: function() {
|
|
330102
330651
|
return auth_capture_run_js_1.runLoginCaptureRun;
|
|
@@ -330324,7 +330873,7 @@ var require_package4 = __commonJS({
|
|
|
330324
330873
|
"package.json"(exports2, module2) {
|
|
330325
330874
|
module2.exports = {
|
|
330326
330875
|
name: "@validate.qa/runner",
|
|
330327
|
-
version: "1.0.
|
|
330876
|
+
version: "1.0.34",
|
|
330328
330877
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330329
330878
|
bin: {
|
|
330330
330879
|
"validate-runner": "dist/cli.js"
|
|
@@ -330940,25 +331489,53 @@ async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
|
330940
331489
|
}
|
|
330941
331490
|
return elementId;
|
|
330942
331491
|
}
|
|
331492
|
+
async function sendKeysToFocusedInput(sessionPath, value) {
|
|
331493
|
+
await appiumRequest(`${sessionPath}/keys`, {
|
|
331494
|
+
method: "POST",
|
|
331495
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
331496
|
+
});
|
|
331497
|
+
}
|
|
330943
331498
|
async function typeText(sessionPath, value, bounds) {
|
|
330944
331499
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330945
|
-
if (
|
|
330946
|
-
|
|
331500
|
+
if (elementId) {
|
|
331501
|
+
try {
|
|
331502
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
331503
|
+
method: "POST",
|
|
331504
|
+
body: JSON.stringify({ text: value })
|
|
331505
|
+
});
|
|
331506
|
+
return;
|
|
331507
|
+
} catch (err) {
|
|
331508
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331509
|
+
}
|
|
330947
331510
|
}
|
|
330948
|
-
await
|
|
330949
|
-
method: "POST",
|
|
330950
|
-
body: JSON.stringify({ text: value })
|
|
330951
|
-
});
|
|
331511
|
+
await sendKeysToFocusedInput(sessionPath, value);
|
|
330952
331512
|
}
|
|
330953
331513
|
async function clearField(sessionPath, bounds) {
|
|
330954
331514
|
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
330955
|
-
if (
|
|
330956
|
-
|
|
331515
|
+
if (elementId) {
|
|
331516
|
+
try {
|
|
331517
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
331518
|
+
method: "POST",
|
|
331519
|
+
body: JSON.stringify({})
|
|
331520
|
+
});
|
|
331521
|
+
return;
|
|
331522
|
+
} catch (err) {
|
|
331523
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331524
|
+
}
|
|
331525
|
+
}
|
|
331526
|
+
try {
|
|
331527
|
+
await appiumRequest(`${sessionPath}/keys`, {
|
|
331528
|
+
method: "POST",
|
|
331529
|
+
// Common select-all + delete sequence is platform-dependent; empty keys
|
|
331530
|
+
// after focus is the portable baseline Appium accepts for many hybrids.
|
|
331531
|
+
body: JSON.stringify({ text: "", value: [] })
|
|
331532
|
+
});
|
|
331533
|
+
} catch (err) {
|
|
331534
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
331535
|
+
throw new Error(
|
|
331536
|
+
`No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
|
|
331537
|
+
);
|
|
330957
331538
|
}
|
|
330958
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
330959
|
-
method: "POST",
|
|
330960
|
-
body: JSON.stringify({})
|
|
330961
|
-
});
|
|
330962
331539
|
}
|
|
330963
331540
|
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
330964
331541
|
const dist = clampMobileSwipeDistance(distance);
|
|
@@ -332196,6 +332773,10 @@ async function startMobileNetworkCapture(options) {
|
|
|
332196
332773
|
|
|
332197
332774
|
// src/services/appium-executor.ts
|
|
332198
332775
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
332776
|
+
var ELEMENT_ID_PATTERN2 = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
332777
|
+
function validateElementId(elementId) {
|
|
332778
|
+
return typeof elementId === "string" && ELEMENT_ID_PATTERN2.test(elementId) ? elementId : null;
|
|
332779
|
+
}
|
|
332199
332780
|
var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
332200
332781
|
const raw = Number(process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
332201
332782
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 600;
|
|
@@ -332330,6 +332911,9 @@ async function createSession(target) {
|
|
|
332330
332911
|
});
|
|
332331
332912
|
const sessionId = result?.value?.sessionId ?? result?.sessionId;
|
|
332332
332913
|
if (!sessionId) throw new Error("Appium session creation returned no session ID");
|
|
332914
|
+
if (target.platform === "ANDROID") {
|
|
332915
|
+
await prepareAndroidInputEnvironment(target.deviceId);
|
|
332916
|
+
}
|
|
332333
332917
|
return sessionId;
|
|
332334
332918
|
}
|
|
332335
332919
|
var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
|
|
@@ -332717,7 +333301,9 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
332717
333301
|
method: "POST",
|
|
332718
333302
|
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
332719
333303
|
});
|
|
332720
|
-
const elementId =
|
|
333304
|
+
const elementId = validateElementId(
|
|
333305
|
+
result?.value?.ELEMENT || result?.value?.["element-6066-11e4-a52e-4f735466cecf"]
|
|
333306
|
+
);
|
|
332721
333307
|
if (elementId) return elementId;
|
|
332722
333308
|
} catch (err) {
|
|
332723
333309
|
if (isTransportError(err)) throw err;
|
|
@@ -332757,24 +333343,25 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
332757
333343
|
})
|
|
332758
333344
|
});
|
|
332759
333345
|
}
|
|
332760
|
-
async function getActiveElementId2(sessionId) {
|
|
332761
|
-
try {
|
|
332762
|
-
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
332763
|
-
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
332764
|
-
} catch {
|
|
332765
|
-
return null;
|
|
332766
|
-
}
|
|
332767
|
-
}
|
|
332768
|
-
async function sendKeysToFocusedInput(sessionId, value) {
|
|
332769
|
-
await wdRequest(`/session/${sessionId}/keys`, {
|
|
332770
|
-
method: "POST",
|
|
332771
|
-
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
332772
|
-
});
|
|
332773
|
-
}
|
|
332774
333346
|
function encodeAndroidInputText(value) {
|
|
332775
333347
|
const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
332776
333348
|
return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
|
|
332777
333349
|
}
|
|
333350
|
+
async function prepareAndroidInputEnvironment(deviceId) {
|
|
333351
|
+
if (!deviceId) return;
|
|
333352
|
+
const settingsToDisable = [
|
|
333353
|
+
["secure", "stylus_handwriting_enabled", "0"]
|
|
333354
|
+
];
|
|
333355
|
+
for (const [namespace, key, value] of settingsToDisable) {
|
|
333356
|
+
try {
|
|
333357
|
+
await execFileAsync2("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
333358
|
+
timeout: 8e3,
|
|
333359
|
+
windowsHide: true
|
|
333360
|
+
});
|
|
333361
|
+
} catch {
|
|
333362
|
+
}
|
|
333363
|
+
}
|
|
333364
|
+
}
|
|
332778
333365
|
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
332779
333366
|
if (!deviceId) {
|
|
332780
333367
|
throw new Error("Android keyboard fallback requires a device id");
|
|
@@ -332836,100 +333423,28 @@ async function executeType(sessionId, step, context) {
|
|
|
332836
333423
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
332837
333424
|
const value = String(step.value);
|
|
332838
333425
|
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
332839
|
-
|
|
332840
|
-
|
|
332841
|
-
|
|
332842
|
-
|
|
332843
|
-
|
|
332844
|
-
|
|
332845
|
-
|
|
332846
|
-
|
|
332847
|
-
|
|
332848
|
-
|
|
332849
|
-
|
|
332850
|
-
|
|
332851
|
-
|
|
332852
|
-
|
|
332853
|
-
|
|
332854
|
-
try {
|
|
332855
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
332856
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332857
|
-
if (!lastInputError) return;
|
|
332858
|
-
elementId = null;
|
|
332859
|
-
} catch (error2) {
|
|
332860
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
332861
|
-
lastInputError = error2;
|
|
332862
|
-
}
|
|
332863
|
-
}
|
|
332864
|
-
if (!elementId && bounds) {
|
|
332865
|
-
await tapCoordinates(sessionId, bounds);
|
|
332866
|
-
tappedBounds = true;
|
|
332867
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332868
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332869
|
-
}
|
|
332870
|
-
if (!elementId) {
|
|
332871
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332872
|
-
}
|
|
332873
|
-
if (elementId) {
|
|
332874
|
-
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
332875
|
-
if (!lastInputError) return;
|
|
332876
|
-
elementId = null;
|
|
332877
|
-
if (!bounds) {
|
|
332878
|
-
throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
|
|
332879
|
-
}
|
|
332880
|
-
}
|
|
332881
|
-
if (bounds) {
|
|
332882
|
-
if (!tappedBounds) {
|
|
332883
|
-
await tapCoordinates(sessionId, bounds);
|
|
332884
|
-
tappedBounds = true;
|
|
332885
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332886
|
-
}
|
|
332887
|
-
try {
|
|
332888
|
-
await sendKeysToFocusedInput(sessionId, value);
|
|
332889
|
-
return;
|
|
332890
|
-
} catch (error2) {
|
|
332891
|
-
if (isTransportError(error2)) throw error2;
|
|
332892
|
-
lastInputError = error2;
|
|
332893
|
-
}
|
|
332894
|
-
}
|
|
332895
|
-
if (context.platform === "ANDROID" && tappedBounds) {
|
|
332896
|
-
try {
|
|
332897
|
-
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
332898
|
-
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
332899
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332900
|
-
return;
|
|
332901
|
-
} catch (error2) {
|
|
332902
|
-
if (isTransportError(error2)) throw error2;
|
|
332903
|
-
lastInputError = error2;
|
|
333426
|
+
const sessionPath = `/session/${sessionId}`;
|
|
333427
|
+
try {
|
|
333428
|
+
await typeText(sessionPath, value, bounds);
|
|
333429
|
+
return;
|
|
333430
|
+
} catch (error2) {
|
|
333431
|
+
if (isTransportError(error2)) throw error2;
|
|
333432
|
+
if (context.platform === "ANDROID" && bounds) {
|
|
333433
|
+
try {
|
|
333434
|
+
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
333435
|
+
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
333436
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
333437
|
+
return;
|
|
333438
|
+
} catch (androidError) {
|
|
333439
|
+
if (isTransportError(androidError)) throw androidError;
|
|
333440
|
+
}
|
|
332904
333441
|
}
|
|
333442
|
+
throw error2 instanceof Error ? error2 : new Error(String(error2));
|
|
332905
333443
|
}
|
|
332906
|
-
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
332907
333444
|
}
|
|
332908
333445
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
332909
333446
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
332910
|
-
|
|
332911
|
-
if (step.target) {
|
|
332912
|
-
try {
|
|
332913
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
332914
|
-
} catch (error2) {
|
|
332915
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
332916
|
-
}
|
|
332917
|
-
}
|
|
332918
|
-
if (!elementId && bounds) {
|
|
332919
|
-
await tapCoordinates(sessionId, bounds);
|
|
332920
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
332921
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332922
|
-
}
|
|
332923
|
-
if (!elementId) {
|
|
332924
|
-
elementId = await getActiveElementId2(sessionId);
|
|
332925
|
-
}
|
|
332926
|
-
if (!elementId) {
|
|
332927
|
-
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
332928
|
-
}
|
|
332929
|
-
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, {
|
|
332930
|
-
method: "POST",
|
|
332931
|
-
body: "{}"
|
|
332932
|
-
});
|
|
333447
|
+
await clearField(`/session/${sessionId}`, bounds);
|
|
332933
333448
|
}
|
|
332934
333449
|
async function executeSwipe(sessionId, step) {
|
|
332935
333450
|
const direction = step.metadata?.direction || "up";
|