@validate.qa/runner 1.0.70 → 1.0.71
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 +1377 -351
- package/dist/cli.mjs +1374 -348
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -735,6 +735,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
735
735
|
exports2.computeScreenHash = computeScreenHash;
|
|
736
736
|
exports2.buildLocatorBundle = buildLocatorBundle;
|
|
737
737
|
exports2.preferredLocator = preferredLocator;
|
|
738
|
+
exports2.identityMatchInfo = identityMatchInfo;
|
|
738
739
|
exports2.parseMobileSource = parseMobileSource2;
|
|
739
740
|
exports2.resolveLocatorAgainstScreenDetailed = resolveLocatorAgainstScreenDetailed;
|
|
740
741
|
exports2.resolveLocatorAgainstScreen = resolveLocatorAgainstScreen;
|
|
@@ -1349,16 +1350,34 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1349
1350
|
bundle.contentDesc = el.accessibilityId;
|
|
1350
1351
|
return bundle;
|
|
1351
1352
|
}
|
|
1352
|
-
function
|
|
1353
|
-
|
|
1353
|
+
function identityFieldValue(el, field) {
|
|
1354
|
+
if (field === "text") {
|
|
1355
|
+
return el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
|
|
1356
|
+
}
|
|
1357
|
+
return el[field];
|
|
1358
|
+
}
|
|
1359
|
+
function choosePreferredIdentityField(el) {
|
|
1354
1360
|
const candidates = [
|
|
1355
|
-
{ value: el
|
|
1356
|
-
{ value: el
|
|
1357
|
-
{ value:
|
|
1361
|
+
{ field: "accessibilityId", value: identityFieldValue(el, "accessibilityId"), unique: el.identityUnique?.accessibilityId },
|
|
1362
|
+
{ field: "resourceId", value: identityFieldValue(el, "resourceId"), unique: el.identityUnique?.resourceId },
|
|
1363
|
+
{ field: "text", value: identityFieldValue(el, "text"), unique: el.identityUnique?.text }
|
|
1358
1364
|
];
|
|
1359
1365
|
const uniqueFirst = candidates.find((c) => c.value && c.unique !== false);
|
|
1360
|
-
const chosen =
|
|
1361
|
-
return chosen
|
|
1366
|
+
const chosen = uniqueFirst ?? candidates.find((c) => c.value);
|
|
1367
|
+
return chosen?.value ? { field: chosen.field, value: chosen.value } : void 0;
|
|
1368
|
+
}
|
|
1369
|
+
function preferredLocator(el) {
|
|
1370
|
+
return choosePreferredIdentityField(el)?.value ?? el.xpath;
|
|
1371
|
+
}
|
|
1372
|
+
function identityMatchInfo(el, elements) {
|
|
1373
|
+
const chosen = choosePreferredIdentityField(el);
|
|
1374
|
+
if (!chosen)
|
|
1375
|
+
return void 0;
|
|
1376
|
+
if (el.identityUnique?.[chosen.field] !== false)
|
|
1377
|
+
return void 0;
|
|
1378
|
+
const sameValue = elements.filter((candidate) => identityFieldValue(candidate, chosen.field) === chosen.value);
|
|
1379
|
+
const matchIndex = sameValue.indexOf(el);
|
|
1380
|
+
return matchIndex >= 0 && sameValue.length > 1 ? { matchIndex, expectedMatchCount: sameValue.length } : void 0;
|
|
1362
1381
|
}
|
|
1363
1382
|
function buildScreenSignal(elements, root, platform3) {
|
|
1364
1383
|
const signal = {
|
|
@@ -2112,6 +2131,9 @@ function readMobileMetadata(value) {
|
|
|
2112
2131
|
return { screenName: "UnknownScreen" };
|
|
2113
2132
|
}
|
|
2114
2133
|
const locatorBundle = isPlainObject(value.locatorBundle) && typeof value.locatorBundle.xpath === "string" && typeof value.locatorBundle.className === "string" ? value.locatorBundle : void 0;
|
|
2134
|
+
const rawMatchIndex = value.matchIndex;
|
|
2135
|
+
const rawExpectedMatchCount = value.expectedMatchCount;
|
|
2136
|
+
const hasValidMatchPair = typeof rawMatchIndex === "number" && Number.isInteger(rawMatchIndex) && rawMatchIndex >= 0 && typeof rawExpectedMatchCount === "number" && Number.isInteger(rawExpectedMatchCount) && rawExpectedMatchCount > rawMatchIndex;
|
|
2115
2137
|
return {
|
|
2116
2138
|
screenName: typeof value.screenName === "string" && value.screenName.trim() ? value.screenName.trim() : "UnknownScreen",
|
|
2117
2139
|
targetLabel: typeof value.targetLabel === "string" && value.targetLabel.trim() ? value.targetLabel.trim() : void 0,
|
|
@@ -2126,7 +2148,9 @@ function readMobileMetadata(value) {
|
|
|
2126
2148
|
distance: typeof value.distance === "number" && Number.isFinite(value.distance) ? value.distance : void 0,
|
|
2127
2149
|
appContext: value.appContext === "WEBVIEW" ? "WEBVIEW" : value.appContext === "NATIVE" ? "NATIVE" : void 0,
|
|
2128
2150
|
webCss: typeof value.webCss === "string" && value.webCss.trim() ? value.webCss.trim() : void 0,
|
|
2129
|
-
webviewContext: typeof value.webviewContext === "string" && /^WEBVIEW/i.test(value.webviewContext.trim()) ? value.webviewContext.trim() : void 0
|
|
2151
|
+
webviewContext: typeof value.webviewContext === "string" && /^WEBVIEW/i.test(value.webviewContext.trim()) ? value.webviewContext.trim() : void 0,
|
|
2152
|
+
matchIndex: hasValidMatchPair ? rawMatchIndex : void 0,
|
|
2153
|
+
expectedMatchCount: hasValidMatchPair ? rawExpectedMatchCount : void 0
|
|
2130
2154
|
};
|
|
2131
2155
|
}
|
|
2132
2156
|
function nonEmpty(value) {
|
|
@@ -2135,6 +2159,9 @@ function nonEmpty(value) {
|
|
|
2135
2159
|
const trimmed = value.trim();
|
|
2136
2160
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
2137
2161
|
}
|
|
2162
|
+
function matchInstanceFields(metadata) {
|
|
2163
|
+
return metadata.matchIndex !== void 0 && metadata.expectedMatchCount !== void 0 ? { matchIndex: metadata.matchIndex, expectedMatchCount: metadata.expectedMatchCount } : {};
|
|
2164
|
+
}
|
|
2138
2165
|
function buildStepTarget(metadata, selector) {
|
|
2139
2166
|
const locatorBundle = metadata.locatorBundle;
|
|
2140
2167
|
const trimmedSelector = nonEmpty(selector);
|
|
@@ -2145,7 +2172,8 @@ function buildStepTarget(metadata, selector) {
|
|
|
2145
2172
|
return {
|
|
2146
2173
|
primary: trimmedSelector,
|
|
2147
2174
|
fallbacks: [],
|
|
2148
|
-
screenName: metadata.screenName
|
|
2175
|
+
screenName: metadata.screenName,
|
|
2176
|
+
...matchInstanceFields(metadata)
|
|
2149
2177
|
};
|
|
2150
2178
|
}
|
|
2151
2179
|
const webCss = nonEmpty(metadata.webCss) ?? (trimmedSelector?.startsWith("css=") ? trimmedSelector.slice(4) : void 0) ?? (nonEmpty(locatorBundle.xpath)?.startsWith("css=") ? locatorBundle.xpath.slice(4) : void 0);
|
|
@@ -2168,8 +2196,9 @@ function buildStepTarget(metadata, selector) {
|
|
|
2168
2196
|
primary,
|
|
2169
2197
|
fallbacks,
|
|
2170
2198
|
screenName: metadata.screenName,
|
|
2171
|
-
locatorBundle
|
|
2199
|
+
locatorBundle,
|
|
2172
2200
|
// Include the locatorBundle for better selector generation later
|
|
2201
|
+
...matchInstanceFields(metadata)
|
|
2173
2202
|
};
|
|
2174
2203
|
}
|
|
2175
2204
|
function labelForAction(action, metadata) {
|
|
@@ -2616,6 +2645,165 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
2616
2645
|
`main().catch((err) => { console.error(err); process.exitCode = 1; });`
|
|
2617
2646
|
].join("\n");
|
|
2618
2647
|
}
|
|
2648
|
+
function webviewStepLogicalId(step) {
|
|
2649
|
+
if (step.metadata?.appContext !== "WEBVIEW")
|
|
2650
|
+
return null;
|
|
2651
|
+
const webCss = typeof step.metadata?.webCss === "string" && step.metadata.webCss.length > 0 ? step.metadata.webCss : step.target?.primary?.startsWith("css=") ? step.target.primary.slice(4) : "";
|
|
2652
|
+
const match = String(webCss ?? "").match(/^#([A-Za-z][\w-]*)$/);
|
|
2653
|
+
return match ? match[1].toLowerCase() : null;
|
|
2654
|
+
}
|
|
2655
|
+
function nativeStepResourceId(step) {
|
|
2656
|
+
const target = step.target;
|
|
2657
|
+
if (!target)
|
|
2658
|
+
return null;
|
|
2659
|
+
const xpaths = [target.locatorBundle?.xpath, ...target.fallbacks ?? []].filter((value) => typeof value === "string" && value.length > 0);
|
|
2660
|
+
for (const xpath of xpaths) {
|
|
2661
|
+
const match = xpath.match(/@resource-id=['"]([^'"]+)['"]/);
|
|
2662
|
+
if (match)
|
|
2663
|
+
return match[1].split("/").pop().toLowerCase();
|
|
2664
|
+
}
|
|
2665
|
+
const resourceId = nonEmpty(target.locatorBundle?.resourceId);
|
|
2666
|
+
if (resourceId)
|
|
2667
|
+
return resourceId.split("/").pop().toLowerCase();
|
|
2668
|
+
const primary = (target.primary ?? "").trim();
|
|
2669
|
+
if (/^[A-Za-z][\w-]*$/.test(primary) && !primary.startsWith("css="))
|
|
2670
|
+
return primary.toLowerCase();
|
|
2671
|
+
return null;
|
|
2672
|
+
}
|
|
2673
|
+
function renumberMobileSteps(steps) {
|
|
2674
|
+
steps.forEach((step, index) => {
|
|
2675
|
+
step.order = index;
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
function dropHybridNativeMirrorStepsInPlace(steps) {
|
|
2679
|
+
const webviewKeys = /* @__PURE__ */ new Set();
|
|
2680
|
+
for (const step of steps) {
|
|
2681
|
+
const id = webviewStepLogicalId(step);
|
|
2682
|
+
if (id)
|
|
2683
|
+
webviewKeys.add(`${step.target?.screenName ?? ""}::${id}`);
|
|
2684
|
+
}
|
|
2685
|
+
if (webviewKeys.size === 0)
|
|
2686
|
+
return 0;
|
|
2687
|
+
const kept = steps.filter((step) => {
|
|
2688
|
+
if (step.metadata?.appContext === "WEBVIEW")
|
|
2689
|
+
return true;
|
|
2690
|
+
if (step.action !== "tap" && step.action !== "type")
|
|
2691
|
+
return true;
|
|
2692
|
+
const id = nativeStepResourceId(step);
|
|
2693
|
+
if (id === null)
|
|
2694
|
+
return true;
|
|
2695
|
+
return !webviewKeys.has(`${step.target?.screenName ?? ""}::${id}`);
|
|
2696
|
+
});
|
|
2697
|
+
const removed = steps.length - kept.length;
|
|
2698
|
+
if (removed > 0) {
|
|
2699
|
+
steps.splice(0, steps.length, ...kept);
|
|
2700
|
+
renumberMobileSteps(steps);
|
|
2701
|
+
}
|
|
2702
|
+
return removed;
|
|
2703
|
+
}
|
|
2704
|
+
function strictOnMobileWanderFlag(value) {
|
|
2705
|
+
const v = (value || "").trim().toLowerCase();
|
|
2706
|
+
return v === "true" || v === "1";
|
|
2707
|
+
}
|
|
2708
|
+
function isMobileWanderSimplifyEnabled() {
|
|
2709
|
+
const value = (process.env.VALIDATEQA_MOBILE_WANDER_SIMPLIFY || "").trim().toLowerCase();
|
|
2710
|
+
return value === "on" || value === "true" || value === "1";
|
|
2711
|
+
}
|
|
2712
|
+
function mobileWanderTraceEnabled() {
|
|
2713
|
+
return strictOnMobileWanderFlag(process.env.VALIDATEQA_MOBILE_WANDER_TRACE);
|
|
2714
|
+
}
|
|
2715
|
+
function isMobileWanderCommitTapText(text) {
|
|
2716
|
+
const isChooser = /\b(continue\s+with|sign\s+in\s+with|log\s+in\s+with|use\s+(?:email|phone)|choose|pick|select)\b/i.test(text);
|
|
2717
|
+
if (isChooser)
|
|
2718
|
+
return false;
|
|
2719
|
+
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(text);
|
|
2720
|
+
}
|
|
2721
|
+
function isMobileWanderCommitStep(step) {
|
|
2722
|
+
if (step.action === "type" && typeof step.value === "string" && /\n$/.test(step.value))
|
|
2723
|
+
return true;
|
|
2724
|
+
if (step.action === "tap") {
|
|
2725
|
+
const text = [step.description, step.target?.primary].filter((v) => typeof v === "string" && v.length > 0).join(" ");
|
|
2726
|
+
return isMobileWanderCommitTapText(text);
|
|
2727
|
+
}
|
|
2728
|
+
return false;
|
|
2729
|
+
}
|
|
2730
|
+
function isMobileWanderStateToggleTap(step) {
|
|
2731
|
+
return step.action === "tap" && isStateValueMobileClassName(step.target?.locatorBundle?.className);
|
|
2732
|
+
}
|
|
2733
|
+
function simplifyMobileWanderingInPlace(steps, trace) {
|
|
2734
|
+
let removed = 0;
|
|
2735
|
+
const afterAdjacentDupes = [];
|
|
2736
|
+
for (const step of steps) {
|
|
2737
|
+
const previous = afterAdjacentDupes[afterAdjacentDupes.length - 1];
|
|
2738
|
+
const primary = step.target?.primary;
|
|
2739
|
+
const isAdjacentDuplicateTap = previous !== void 0 && step.action === "tap" && previous.action === "tap" && !!primary && primary === previous.target?.primary && step.target?.screenName === previous.target?.screenName && step.value === previous.value && !isMobileWanderStateToggleTap(step) && !isMobileWanderStateToggleTap(previous);
|
|
2740
|
+
if (isAdjacentDuplicateTap) {
|
|
2741
|
+
removed += 1;
|
|
2742
|
+
trace?.(`dropped adjacent duplicate tap "${primary}" (exact repeat, nothing between)`);
|
|
2743
|
+
afterAdjacentDupes[afterAdjacentDupes.length - 1] = step;
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
afterAdjacentDupes.push(step);
|
|
2747
|
+
}
|
|
2748
|
+
const lastTypeIndexByPrimary = /* @__PURE__ */ new Map();
|
|
2749
|
+
const refocusTapIndexesByPrimary = /* @__PURE__ */ new Map();
|
|
2750
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
2751
|
+
afterAdjacentDupes.forEach((step, index) => {
|
|
2752
|
+
if (step.action === "assertVisible" || step.action === "assertText" || step.action === "waitForElement") {
|
|
2753
|
+
lastTypeIndexByPrimary.clear();
|
|
2754
|
+
refocusTapIndexesByPrimary.clear();
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
if (step.action === "clear") {
|
|
2758
|
+
const primary2 = step.target?.primary;
|
|
2759
|
+
if (primary2) {
|
|
2760
|
+
lastTypeIndexByPrimary.delete(primary2);
|
|
2761
|
+
refocusTapIndexesByPrimary.delete(primary2);
|
|
2762
|
+
}
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
if (isMobileWanderCommitStep(step)) {
|
|
2766
|
+
lastTypeIndexByPrimary.clear();
|
|
2767
|
+
refocusTapIndexesByPrimary.clear();
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (step.action === "tap") {
|
|
2771
|
+
const primary2 = step.target?.primary;
|
|
2772
|
+
if (primary2) {
|
|
2773
|
+
const list = refocusTapIndexesByPrimary.get(primary2) ?? [];
|
|
2774
|
+
list.push(index);
|
|
2775
|
+
refocusTapIndexesByPrimary.set(primary2, list);
|
|
2776
|
+
}
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (step.action !== "type")
|
|
2780
|
+
return;
|
|
2781
|
+
const primary = step.target?.primary;
|
|
2782
|
+
if (!primary)
|
|
2783
|
+
return;
|
|
2784
|
+
const priorIndex = lastTypeIndexByPrimary.get(primary);
|
|
2785
|
+
if (priorIndex !== void 0) {
|
|
2786
|
+
toDrop.add(priorIndex);
|
|
2787
|
+
removed += 1;
|
|
2788
|
+
trace?.(`dropped superseded type into "${primary}" (a later type into the same field wins)`);
|
|
2789
|
+
for (const tapIndex of refocusTapIndexesByPrimary.get(primary) ?? []) {
|
|
2790
|
+
if (tapIndex > priorIndex && tapIndex < index && !toDrop.has(tapIndex)) {
|
|
2791
|
+
toDrop.add(tapIndex);
|
|
2792
|
+
removed += 1;
|
|
2793
|
+
trace?.(`dropped redundant re-focus tap on "${primary}" (it only re-focused a field whose write was superseded)`);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
lastTypeIndexByPrimary.set(primary, index);
|
|
2798
|
+
refocusTapIndexesByPrimary.set(primary, []);
|
|
2799
|
+
});
|
|
2800
|
+
if (removed === 0)
|
|
2801
|
+
return 0;
|
|
2802
|
+
const kept = afterAdjacentDupes.filter((_, index) => !toDrop.has(index));
|
|
2803
|
+
steps.splice(0, steps.length, ...kept);
|
|
2804
|
+
renumberMobileSteps(steps);
|
|
2805
|
+
return removed;
|
|
2806
|
+
}
|
|
2619
2807
|
function generateMobileTestFromSession(input) {
|
|
2620
2808
|
if (input.medium !== "IOS_NATIVE" && input.medium !== "ANDROID_NATIVE") {
|
|
2621
2809
|
throw new Error(`Mobile test generation requires IOS_NATIVE or ANDROID_NATIVE medium (received ${input.medium})`);
|
|
@@ -2659,12 +2847,20 @@ function generateMobileTestFromSession(input) {
|
|
|
2659
2847
|
if (steps.length === 0) {
|
|
2660
2848
|
throw new Error("No mobile actions were recorded for this session");
|
|
2661
2849
|
}
|
|
2850
|
+
dropHybridNativeMirrorStepsInPlace(steps);
|
|
2851
|
+
const wanderTraceMessages = [];
|
|
2852
|
+
const wanderTraceSink = (message) => {
|
|
2853
|
+
wanderTraceMessages.push(message);
|
|
2854
|
+
};
|
|
2855
|
+
if (isMobileWanderSimplifyEnabled()) {
|
|
2856
|
+
simplifyMobileWanderingInPlace(steps, mobileWanderTraceEnabled() ? wanderTraceSink : void 0);
|
|
2857
|
+
}
|
|
2662
2858
|
const interactionSteps = steps.filter((step) => step.action !== "launchApp");
|
|
2663
2859
|
const runnableInteractions = interactionSteps.filter(isRunnableStep);
|
|
2664
2860
|
if (interactionSteps.length === 0 || runnableInteractions.length === 0) {
|
|
2665
2861
|
throw new Error("Generated mobile test has no runnable steps \u2014 re-record the flow so each interaction targets an on-screen element.");
|
|
2666
2862
|
}
|
|
2667
|
-
const warnings = [];
|
|
2863
|
+
const warnings = wanderTraceMessages.map((message) => `[wander] ${message}`);
|
|
2668
2864
|
const unrunnable = interactionSteps.length - runnableInteractions.length;
|
|
2669
2865
|
if (unrunnable > 0) {
|
|
2670
2866
|
warnings.push(`${unrunnable} recorded step(s) could not be turned into a runnable action (missing a replayable identity locator or coordinate) and were kept best-effort.`);
|
|
@@ -6221,6 +6417,7 @@ __export(dist_exports, {
|
|
|
6221
6417
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
6222
6418
|
isIdentityMobileXpath: () => isIdentityMobileXpath,
|
|
6223
6419
|
isMobilePlatform: () => isMobilePlatform,
|
|
6420
|
+
isMobileWanderSimplifyEnabled: () => isMobileWanderSimplifyEnabled,
|
|
6224
6421
|
isPricingPlan: () => isPricingPlan,
|
|
6225
6422
|
isRunnableStep: () => isRunnableStep,
|
|
6226
6423
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
@@ -6242,6 +6439,7 @@ __export(dist_exports, {
|
|
|
6242
6439
|
mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
|
|
6243
6440
|
mobileLocatorBundleHasIdentity: () => mobileLocatorBundleHasIdentity,
|
|
6244
6441
|
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
6442
|
+
mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
|
|
6245
6443
|
normalizeControlName: () => normalizeControlName,
|
|
6246
6444
|
normalizeTtsVoiceId: () => normalizeTtsVoiceId,
|
|
6247
6445
|
otherMobilePlatform: () => otherMobilePlatform,
|
|
@@ -6270,6 +6468,7 @@ __export(dist_exports, {
|
|
|
6270
6468
|
scrubUrlPropertyBag: () => scrubUrlPropertyBag,
|
|
6271
6469
|
selectorCandidatesForStep: () => selectorCandidatesForStep,
|
|
6272
6470
|
selectorForValue: () => selectorForValue,
|
|
6471
|
+
simplifyMobileWanderingInPlace: () => simplifyMobileWanderingInPlace,
|
|
6273
6472
|
stepAppliesToPlatform: () => stepAppliesToPlatform,
|
|
6274
6473
|
stepIsDivergentForPlatform: () => stepIsDivergentForPlatform,
|
|
6275
6474
|
stepsMissingPlatformLocators: () => stepsMissingPlatformLocators,
|
|
@@ -10556,7 +10755,7 @@ var require_sonic_boom = __commonJS({
|
|
|
10556
10755
|
if (!(this instanceof SonicBoom)) {
|
|
10557
10756
|
return new SonicBoom(opts);
|
|
10558
10757
|
}
|
|
10559
|
-
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir:
|
|
10758
|
+
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir3, retryEAGAIN, fsync, contentMode, mode } = opts || {};
|
|
10560
10759
|
fd = fd || dest;
|
|
10561
10760
|
this._len = 0;
|
|
10562
10761
|
this.fd = -1;
|
|
@@ -10581,7 +10780,7 @@ var require_sonic_boom = __commonJS({
|
|
|
10581
10780
|
this.append = append || false;
|
|
10582
10781
|
this.mode = mode;
|
|
10583
10782
|
this.retryEAGAIN = retryEAGAIN || (() => true);
|
|
10584
|
-
this.mkdir =
|
|
10783
|
+
this.mkdir = mkdir3 || false;
|
|
10585
10784
|
let fsWriteSync;
|
|
10586
10785
|
let fsWrite;
|
|
10587
10786
|
if (contentMode === kContentModeBuffer) {
|
|
@@ -11293,7 +11492,7 @@ var require_thread_stream = __commonJS({
|
|
|
11293
11492
|
var { version } = require_package();
|
|
11294
11493
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
11295
11494
|
var { Worker } = require("worker_threads");
|
|
11296
|
-
var { join:
|
|
11495
|
+
var { join: join8 } = require("path");
|
|
11297
11496
|
var { pathToFileURL } = require("url");
|
|
11298
11497
|
var { wait } = require_wait();
|
|
11299
11498
|
var {
|
|
@@ -11344,7 +11543,7 @@ var require_thread_stream = __commonJS({
|
|
|
11344
11543
|
function createWorker(stream, opts) {
|
|
11345
11544
|
const { filename, workerData } = opts;
|
|
11346
11545
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
11347
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
11546
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11348
11547
|
const worker = new Worker(toExecute, {
|
|
11349
11548
|
...opts.workerOpts,
|
|
11350
11549
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -11812,7 +12011,7 @@ var require_transport = __commonJS({
|
|
|
11812
12011
|
var { createRequire } = require("module");
|
|
11813
12012
|
var { existsSync: existsSync3 } = require("fs");
|
|
11814
12013
|
var getCallers = require_caller();
|
|
11815
|
-
var { join:
|
|
12014
|
+
var { join: join8, isAbsolute: isAbsolute2, sep } = require("path");
|
|
11816
12015
|
var { fileURLToPath } = require("url");
|
|
11817
12016
|
var sleep = require_atomic_sleep();
|
|
11818
12017
|
var onExit = require_on_exit_leak_free();
|
|
@@ -11884,7 +12083,7 @@ var require_transport = __commonJS({
|
|
|
11884
12083
|
return false;
|
|
11885
12084
|
}
|
|
11886
12085
|
}
|
|
11887
|
-
return
|
|
12086
|
+
return isAbsolute2(path) && !existsSync3(path);
|
|
11888
12087
|
}
|
|
11889
12088
|
function stripQuotes(value) {
|
|
11890
12089
|
const first = value[0];
|
|
@@ -11965,7 +12164,7 @@ var require_transport = __commonJS({
|
|
|
11965
12164
|
throw new Error("only one of target or targets can be specified");
|
|
11966
12165
|
}
|
|
11967
12166
|
if (targets) {
|
|
11968
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
12167
|
+
target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
|
|
11969
12168
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
11970
12169
|
return {
|
|
11971
12170
|
...dest,
|
|
@@ -11983,7 +12182,7 @@ var require_transport = __commonJS({
|
|
|
11983
12182
|
});
|
|
11984
12183
|
});
|
|
11985
12184
|
} else if (pipeline) {
|
|
11986
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
12185
|
+
target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
|
|
11987
12186
|
options.pipelines = [pipeline.map((dest) => {
|
|
11988
12187
|
return {
|
|
11989
12188
|
...dest,
|
|
@@ -12002,11 +12201,11 @@ var require_transport = __commonJS({
|
|
|
12002
12201
|
return buildStream(fixTarget(target), options, worker, sync, name);
|
|
12003
12202
|
function fixTarget(origin) {
|
|
12004
12203
|
origin = bundlerOverrides[origin] || origin;
|
|
12005
|
-
if (
|
|
12204
|
+
if (isAbsolute2(origin) || origin.indexOf("file://") === 0) {
|
|
12006
12205
|
return origin;
|
|
12007
12206
|
}
|
|
12008
12207
|
if (origin === "pino/file") {
|
|
12009
|
-
return
|
|
12208
|
+
return join8(__dirname, "..", "file.js");
|
|
12010
12209
|
}
|
|
12011
12210
|
let fixTarget2;
|
|
12012
12211
|
for (const filePath of callers) {
|
|
@@ -12987,7 +13186,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
12987
13186
|
return circularValue;
|
|
12988
13187
|
}
|
|
12989
13188
|
let res = "";
|
|
12990
|
-
let
|
|
13189
|
+
let join8 = ",";
|
|
12991
13190
|
const originalIndentation = indentation;
|
|
12992
13191
|
if (Array.isArray(value)) {
|
|
12993
13192
|
if (value.length === 0) {
|
|
@@ -13001,7 +13200,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
13001
13200
|
indentation += spacer;
|
|
13002
13201
|
res += `
|
|
13003
13202
|
${indentation}`;
|
|
13004
|
-
|
|
13203
|
+
join8 = `,
|
|
13005
13204
|
${indentation}`;
|
|
13006
13205
|
}
|
|
13007
13206
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -13009,13 +13208,13 @@ ${indentation}`;
|
|
|
13009
13208
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
13010
13209
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
13011
13210
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
13012
|
-
res +=
|
|
13211
|
+
res += join8;
|
|
13013
13212
|
}
|
|
13014
13213
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
13015
13214
|
res += tmp !== void 0 ? tmp : "null";
|
|
13016
13215
|
if (value.length - 1 > maximumBreadth) {
|
|
13017
13216
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
13018
|
-
res += `${
|
|
13217
|
+
res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
13019
13218
|
}
|
|
13020
13219
|
if (spacer !== "") {
|
|
13021
13220
|
res += `
|
|
@@ -13036,7 +13235,7 @@ ${originalIndentation}`;
|
|
|
13036
13235
|
let separator = "";
|
|
13037
13236
|
if (spacer !== "") {
|
|
13038
13237
|
indentation += spacer;
|
|
13039
|
-
|
|
13238
|
+
join8 = `,
|
|
13040
13239
|
${indentation}`;
|
|
13041
13240
|
whitespace = " ";
|
|
13042
13241
|
}
|
|
@@ -13050,13 +13249,13 @@ ${indentation}`;
|
|
|
13050
13249
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
13051
13250
|
if (tmp !== void 0) {
|
|
13052
13251
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
13053
|
-
separator =
|
|
13252
|
+
separator = join8;
|
|
13054
13253
|
}
|
|
13055
13254
|
}
|
|
13056
13255
|
if (keyLength > maximumBreadth) {
|
|
13057
13256
|
const removedKeys = keyLength - maximumBreadth;
|
|
13058
13257
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
13059
|
-
separator =
|
|
13258
|
+
separator = join8;
|
|
13060
13259
|
}
|
|
13061
13260
|
if (spacer !== "" && separator.length > 1) {
|
|
13062
13261
|
res = `
|
|
@@ -13097,7 +13296,7 @@ ${originalIndentation}`;
|
|
|
13097
13296
|
}
|
|
13098
13297
|
const originalIndentation = indentation;
|
|
13099
13298
|
let res = "";
|
|
13100
|
-
let
|
|
13299
|
+
let join8 = ",";
|
|
13101
13300
|
if (Array.isArray(value)) {
|
|
13102
13301
|
if (value.length === 0) {
|
|
13103
13302
|
return "[]";
|
|
@@ -13110,7 +13309,7 @@ ${originalIndentation}`;
|
|
|
13110
13309
|
indentation += spacer;
|
|
13111
13310
|
res += `
|
|
13112
13311
|
${indentation}`;
|
|
13113
|
-
|
|
13312
|
+
join8 = `,
|
|
13114
13313
|
${indentation}`;
|
|
13115
13314
|
}
|
|
13116
13315
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -13118,13 +13317,13 @@ ${indentation}`;
|
|
|
13118
13317
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
13119
13318
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
13120
13319
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
13121
|
-
res +=
|
|
13320
|
+
res += join8;
|
|
13122
13321
|
}
|
|
13123
13322
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
13124
13323
|
res += tmp !== void 0 ? tmp : "null";
|
|
13125
13324
|
if (value.length - 1 > maximumBreadth) {
|
|
13126
13325
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
13127
|
-
res += `${
|
|
13326
|
+
res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
13128
13327
|
}
|
|
13129
13328
|
if (spacer !== "") {
|
|
13130
13329
|
res += `
|
|
@@ -13137,7 +13336,7 @@ ${originalIndentation}`;
|
|
|
13137
13336
|
let whitespace = "";
|
|
13138
13337
|
if (spacer !== "") {
|
|
13139
13338
|
indentation += spacer;
|
|
13140
|
-
|
|
13339
|
+
join8 = `,
|
|
13141
13340
|
${indentation}`;
|
|
13142
13341
|
whitespace = " ";
|
|
13143
13342
|
}
|
|
@@ -13146,7 +13345,7 @@ ${indentation}`;
|
|
|
13146
13345
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
13147
13346
|
if (tmp !== void 0) {
|
|
13148
13347
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
13149
|
-
separator =
|
|
13348
|
+
separator = join8;
|
|
13150
13349
|
}
|
|
13151
13350
|
}
|
|
13152
13351
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -13204,20 +13403,20 @@ ${originalIndentation}`;
|
|
|
13204
13403
|
indentation += spacer;
|
|
13205
13404
|
let res2 = `
|
|
13206
13405
|
${indentation}`;
|
|
13207
|
-
const
|
|
13406
|
+
const join9 = `,
|
|
13208
13407
|
${indentation}`;
|
|
13209
13408
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
13210
13409
|
let i = 0;
|
|
13211
13410
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
13212
13411
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
13213
13412
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
13214
|
-
res2 +=
|
|
13413
|
+
res2 += join9;
|
|
13215
13414
|
}
|
|
13216
13415
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
13217
13416
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
13218
13417
|
if (value.length - 1 > maximumBreadth) {
|
|
13219
13418
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
13220
|
-
res2 += `${
|
|
13419
|
+
res2 += `${join9}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
13221
13420
|
}
|
|
13222
13421
|
res2 += `
|
|
13223
13422
|
${originalIndentation}`;
|
|
@@ -13233,16 +13432,16 @@ ${originalIndentation}`;
|
|
|
13233
13432
|
return '"[Object]"';
|
|
13234
13433
|
}
|
|
13235
13434
|
indentation += spacer;
|
|
13236
|
-
const
|
|
13435
|
+
const join8 = `,
|
|
13237
13436
|
${indentation}`;
|
|
13238
13437
|
let res = "";
|
|
13239
13438
|
let separator = "";
|
|
13240
13439
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
13241
13440
|
if (isTypedArrayWithEntries(value)) {
|
|
13242
|
-
res += stringifyTypedArray(value,
|
|
13441
|
+
res += stringifyTypedArray(value, join8, maximumBreadth);
|
|
13243
13442
|
keys = keys.slice(value.length);
|
|
13244
13443
|
maximumPropertiesToStringify -= value.length;
|
|
13245
|
-
separator =
|
|
13444
|
+
separator = join8;
|
|
13246
13445
|
}
|
|
13247
13446
|
if (deterministic) {
|
|
13248
13447
|
keys = sort(keys, comparator);
|
|
@@ -13253,13 +13452,13 @@ ${indentation}`;
|
|
|
13253
13452
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
13254
13453
|
if (tmp !== void 0) {
|
|
13255
13454
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
13256
|
-
separator =
|
|
13455
|
+
separator = join8;
|
|
13257
13456
|
}
|
|
13258
13457
|
}
|
|
13259
13458
|
if (keyLength > maximumBreadth) {
|
|
13260
13459
|
const removedKeys = keyLength - maximumBreadth;
|
|
13261
13460
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
13262
|
-
separator =
|
|
13461
|
+
separator = join8;
|
|
13263
13462
|
}
|
|
13264
13463
|
if (separator !== "") {
|
|
13265
13464
|
res = `
|
|
@@ -61935,11 +62134,11 @@ var require_core = __commonJS({
|
|
|
61935
62134
|
function mapOuter(r, f) {
|
|
61936
62135
|
return r.matched ? f(r) : r;
|
|
61937
62136
|
}
|
|
61938
|
-
function ab(pa, pb,
|
|
61939
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) =>
|
|
62137
|
+
function ab(pa, pb, join8) {
|
|
62138
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join8(ma.value, vb, data, i, j)));
|
|
61940
62139
|
}
|
|
61941
|
-
function abc(pa, pb, pc,
|
|
61942
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) =>
|
|
62140
|
+
function abc(pa, pb, pc, join8) {
|
|
62141
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join8(ma.value, mb.value, vc, data, i, j))));
|
|
61943
62142
|
}
|
|
61944
62143
|
function action(f) {
|
|
61945
62144
|
return (data, i) => {
|
|
@@ -81167,10 +81366,14 @@ var require_mobile_generator = __commonJS({
|
|
|
81167
81366
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
81168
81367
|
"use strict";
|
|
81169
81368
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
81369
|
+
exports2.structuralScreenKey = structuralScreenKey;
|
|
81170
81370
|
exports2.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
81171
81371
|
exports2.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
81172
81372
|
exports2.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
81173
81373
|
exports2.collapseRetryCycles = collapseRetryCycles;
|
|
81374
|
+
exports2.coalesceSameFieldTypes = coalesceSameFieldTypes;
|
|
81375
|
+
exports2.coalesceRepeatedTaps = coalesceRepeatedTaps;
|
|
81376
|
+
exports2.elideNoOpDetours = elideNoOpDetours;
|
|
81174
81377
|
exports2.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
|
|
81175
81378
|
exports2.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
|
|
81176
81379
|
exports2.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
|
|
@@ -81379,6 +81582,22 @@ var require_mobile_generator = __commonJS({
|
|
|
81379
81582
|
MOBILE_ASSERT_TEXT_TOOL,
|
|
81380
81583
|
FINISH_GENERATE_TOOL
|
|
81381
81584
|
];
|
|
81585
|
+
var STRUCTURAL_KEY_MAX_ELEMENTS = 16;
|
|
81586
|
+
function structuralScreenKey(resolved) {
|
|
81587
|
+
if (!resolved)
|
|
81588
|
+
return null;
|
|
81589
|
+
const nativeElements = resolved.screen.elements;
|
|
81590
|
+
const hybridElements = resolved.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81591
|
+
if (nativeElements.length + hybridElements.length < 2)
|
|
81592
|
+
return null;
|
|
81593
|
+
const nativeSkeleton = [...nativeElements].sort((a, b) => a.role.localeCompare(b.role) || locatorLocalName(a.resourceId ?? "").localeCompare(locatorLocalName(b.resourceId ?? "")) || (a.accessibilityId ?? "").localeCompare(b.accessibilityId ?? "")).slice(0, STRUCTURAL_KEY_MAX_ELEMENTS).map((el) => `${el.role}|${locatorLocalName(el.resourceId ?? "")}|${el.accessibilityId ?? ""}`);
|
|
81594
|
+
const hybridSkeleton = [...hybridElements].sort((a, b) => (a.id || a.role).localeCompare(b.id || b.role)).slice(0, STRUCTURAL_KEY_MAX_ELEMENTS).map((el) => `${el.role}|${el.id ?? ""}`);
|
|
81595
|
+
const activityOrBundle = resolved.signal.activity ?? resolved.signal.bundleId ?? "";
|
|
81596
|
+
const payload = `${activityOrBundle}
|
|
81597
|
+
${nativeSkeleton.join("\n")}
|
|
81598
|
+
${hybridSkeleton.join("\n")}`;
|
|
81599
|
+
return (0, crypto_1.createHash)("sha1").update(payload).digest("hex");
|
|
81600
|
+
}
|
|
81382
81601
|
function platformForMedium(medium) {
|
|
81383
81602
|
if (medium === "IOS_NATIVE")
|
|
81384
81603
|
return "IOS";
|
|
@@ -81540,18 +81759,39 @@ var require_mobile_generator = __commonJS({
|
|
|
81540
81759
|
}
|
|
81541
81760
|
return `Typed value does not appear in the UI tree after typing into "${labelForElement(target)}". The field may not have been focused (common on WebView). Re-snapshot, tap the field, and type again.`;
|
|
81542
81761
|
}
|
|
81543
|
-
function
|
|
81762
|
+
function alreadyFocusedWithEqualValue(element, value) {
|
|
81763
|
+
return element.focused === true && typeof element.text === "string" && value.length > 0 && element.text === value;
|
|
81764
|
+
}
|
|
81765
|
+
function nativeElementsOf(resolved) {
|
|
81766
|
+
return resolved?.screen.elements ?? [];
|
|
81767
|
+
}
|
|
81768
|
+
function hybridElementsOf(resolved) {
|
|
81769
|
+
return resolved?.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81770
|
+
}
|
|
81771
|
+
function metadataForElement(element, screenName, platform3, elements = []) {
|
|
81772
|
+
const match = (0, mobile_source_parser_js_2.identityMatchInfo)(element, elements);
|
|
81544
81773
|
return {
|
|
81545
81774
|
screenName,
|
|
81546
81775
|
targetLabel: labelForElement(element),
|
|
81547
81776
|
locatorBundle: (0, mobile_source_parser_js_2.buildLocatorBundle)(element, platform3),
|
|
81548
|
-
bounds: element.bounds
|
|
81777
|
+
bounds: element.bounds,
|
|
81778
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81549
81779
|
};
|
|
81550
81780
|
}
|
|
81551
|
-
function
|
|
81781
|
+
function hybridIdentityMatchInfo(element, elements) {
|
|
81782
|
+
const hybridIdentityValue = (candidate) => nonEmpty3(candidate.id) ?? (candidate.editable === true ? void 0 : nonEmpty3(candidate.text));
|
|
81783
|
+
const chosenValue = hybridIdentityValue(element);
|
|
81784
|
+
if (!chosenValue)
|
|
81785
|
+
return void 0;
|
|
81786
|
+
const sameValue = elements.filter((candidate) => hybridIdentityValue(candidate) === chosenValue);
|
|
81787
|
+
const matchIndex = sameValue.indexOf(element);
|
|
81788
|
+
return matchIndex >= 0 && sameValue.length > 1 ? { matchIndex, expectedMatchCount: sameValue.length } : void 0;
|
|
81789
|
+
}
|
|
81790
|
+
function metadataForHybridElement(element, screenName, elements = []) {
|
|
81552
81791
|
const editable = isEditableHybridElement(element);
|
|
81553
81792
|
const label = labelForHybridElement(element);
|
|
81554
81793
|
const safeText = editable ? void 0 : nonEmpty3(element.text);
|
|
81794
|
+
const match = hybridIdentityMatchInfo(element, elements);
|
|
81555
81795
|
return {
|
|
81556
81796
|
screenName,
|
|
81557
81797
|
targetLabel: label,
|
|
@@ -81560,11 +81800,16 @@ var require_mobile_generator = __commonJS({
|
|
|
81560
81800
|
webviewContext: element.webviewContext,
|
|
81561
81801
|
bounds: element.bounds,
|
|
81562
81802
|
locatorBundle: {
|
|
81563
|
-
|
|
81803
|
+
// WebView controls resolve via `webCss` (above) in the WEBVIEW context, not
|
|
81804
|
+
// by xpath. `LocatorBundle.xpath` is contractually an XPath, so never store a
|
|
81805
|
+
// `css=` selector here — an empty xpath keeps the fallback chain honest and
|
|
81806
|
+
// prevents the css selector being misread as an xpath at replay.
|
|
81807
|
+
xpath: "",
|
|
81564
81808
|
className: element.tag || "webview",
|
|
81565
81809
|
...element.id ? { accessibilityId: element.id } : {},
|
|
81566
81810
|
...safeText ? { text: safeText } : {}
|
|
81567
|
-
}
|
|
81811
|
+
},
|
|
81812
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81568
81813
|
};
|
|
81569
81814
|
}
|
|
81570
81815
|
function isEditableHybridElement(element) {
|
|
@@ -82282,7 +82527,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82282
82527
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
82283
82528
|
recordAction: (action) => {
|
|
82284
82529
|
const screenHash = getLatest()?.signal.screenHash;
|
|
82285
|
-
const
|
|
82530
|
+
const structuralKey = structuralScreenKey(getLatest());
|
|
82531
|
+
const stamped = screenHash && !action.metadata.screenHash || structuralKey && !action.metadata.structuralKey ? {
|
|
82532
|
+
...action,
|
|
82533
|
+
metadata: {
|
|
82534
|
+
...action.metadata,
|
|
82535
|
+
...screenHash && !action.metadata.screenHash ? { screenHash } : {},
|
|
82536
|
+
...structuralKey && !action.metadata.structuralKey ? { structuralKey } : {}
|
|
82537
|
+
}
|
|
82538
|
+
} : action;
|
|
82286
82539
|
actions.push((0, shared_1.placeholderizeRecordedMobileAction)(finalizeAction(stamped), credentials));
|
|
82287
82540
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
82288
82541
|
assertCount++;
|
|
@@ -82388,7 +82641,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82388
82641
|
const result2 = await driver.webTap(hybridTap.webviewContext, hybridTap.css);
|
|
82389
82642
|
const absorbed2 = await absorbActionResult(result2, "mobile_tap(webview)", opensAuthenticationSurface(hybridTap));
|
|
82390
82643
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82391
|
-
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2);
|
|
82644
|
+
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2, hybridElementsOf(before2));
|
|
82392
82645
|
const label = metadata.targetLabel ?? hybridTap.css;
|
|
82393
82646
|
const tapAction = {
|
|
82394
82647
|
type: "TAP",
|
|
@@ -82440,7 +82693,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82440
82693
|
const tapAction = {
|
|
82441
82694
|
type: "TAP",
|
|
82442
82695
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82443
|
-
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
82696
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before)),
|
|
82444
82697
|
innerText: null,
|
|
82445
82698
|
url: "",
|
|
82446
82699
|
value: null
|
|
@@ -82480,10 +82733,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82480
82733
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82481
82734
|
}
|
|
82482
82735
|
const beforeScreenName2 = screenName();
|
|
82736
|
+
const before2 = latest();
|
|
82483
82737
|
const result2 = await driver.webType(hybridType.webviewContext, hybridType.css, value);
|
|
82484
82738
|
const absorbed2 = await absorbActionResult(result2, "mobile_type(webview)");
|
|
82485
82739
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82486
|
-
const metadata = metadataForHybridElement(hybridType, beforeScreenName2);
|
|
82740
|
+
const metadata = metadataForHybridElement(hybridType, beforeScreenName2, hybridElementsOf(before2));
|
|
82487
82741
|
const label = metadata.targetLabel ?? hybridType.css;
|
|
82488
82742
|
recordAction({
|
|
82489
82743
|
type: "TYPE",
|
|
@@ -82498,7 +82752,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82498
82752
|
const element = resolveRef(toolArgs.ref);
|
|
82499
82753
|
if (!element)
|
|
82500
82754
|
return unknownRef(toolArgs.ref);
|
|
82755
|
+
if (alreadyFocusedWithEqualValue(element, value)) {
|
|
82756
|
+
return {
|
|
82757
|
+
ok: false,
|
|
82758
|
+
error: `"${labelForElement(element)}" is already focused and already contains exactly this value \u2014 typing it again is a no-op. Move on to the next step.`
|
|
82759
|
+
};
|
|
82760
|
+
}
|
|
82501
82761
|
const beforeScreenName = screenName();
|
|
82762
|
+
const before = latest();
|
|
82502
82763
|
const result = await driver.type(value, element.bounds);
|
|
82503
82764
|
const absorbed = await absorbActionResult(result, "mobile_type");
|
|
82504
82765
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82506,7 +82767,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82506
82767
|
type: "TYPE",
|
|
82507
82768
|
value,
|
|
82508
82769
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82509
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82770
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82510
82771
|
});
|
|
82511
82772
|
}
|
|
82512
82773
|
const payload = actionPayload(result, absorbed);
|
|
@@ -82524,6 +82785,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82524
82785
|
return { ok: false, error: "This driver does not support non-clearing WEBVIEW keyboard submit." };
|
|
82525
82786
|
}
|
|
82526
82787
|
const beforeScreenName2 = screenName();
|
|
82788
|
+
const before2 = latest();
|
|
82527
82789
|
const result2 = await driver.webSubmitKeyboard(hybridField.webviewContext, hybridField.css);
|
|
82528
82790
|
const absorbed2 = await absorbActionResult(result2, "mobile_submit_keyboard(webview)");
|
|
82529
82791
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
@@ -82531,7 +82793,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82531
82793
|
type: "TYPE",
|
|
82532
82794
|
value: "\n",
|
|
82533
82795
|
selector: `css=${hybridField.css}`,
|
|
82534
|
-
metadata: metadataForHybridElement(hybridField, beforeScreenName2)
|
|
82796
|
+
metadata: metadataForHybridElement(hybridField, beforeScreenName2, hybridElementsOf(before2))
|
|
82535
82797
|
});
|
|
82536
82798
|
}
|
|
82537
82799
|
return actionPayload(result2, absorbed2);
|
|
@@ -82543,6 +82805,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82543
82805
|
return { ok: false, error: "This driver does not support non-clearing keyboard submit." };
|
|
82544
82806
|
}
|
|
82545
82807
|
const beforeScreenName = screenName();
|
|
82808
|
+
const before = latest();
|
|
82546
82809
|
const result = await driver.submitKeyboard(element.bounds);
|
|
82547
82810
|
const absorbed = await absorbActionResult(result, "mobile_submit_keyboard");
|
|
82548
82811
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82550,7 +82813,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82550
82813
|
type: "TYPE",
|
|
82551
82814
|
value: "\n",
|
|
82552
82815
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82553
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82816
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82554
82817
|
});
|
|
82555
82818
|
}
|
|
82556
82819
|
return actionPayload(result, absorbed);
|
|
@@ -82575,10 +82838,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82575
82838
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82576
82839
|
}
|
|
82577
82840
|
const beforeScreenName2 = screenName();
|
|
82841
|
+
const before2 = latest();
|
|
82578
82842
|
const result2 = await driver.webType(hybridFill.webviewContext, hybridFill.css, secret);
|
|
82579
82843
|
const absorbed2 = await absorbActionResult(result2, "mobile_fill_credential(webview)");
|
|
82580
82844
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82581
|
-
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2);
|
|
82845
|
+
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2, hybridElementsOf(before2));
|
|
82582
82846
|
const label = metadata.targetLabel ?? hybridFill.css;
|
|
82583
82847
|
recordAction({
|
|
82584
82848
|
type: "TYPE",
|
|
@@ -82593,7 +82857,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82593
82857
|
const element = resolveRef(toolArgs.ref);
|
|
82594
82858
|
if (!element)
|
|
82595
82859
|
return unknownRef(toolArgs.ref);
|
|
82860
|
+
if (alreadyFocusedWithEqualValue(element, secret)) {
|
|
82861
|
+
return {
|
|
82862
|
+
ok: false,
|
|
82863
|
+
error: `"${labelForElement(element)}" is already focused and already holds this exact credential value \u2014 typing it again is a no-op. Move on to the next step.`
|
|
82864
|
+
};
|
|
82865
|
+
}
|
|
82596
82866
|
const beforeScreenName = screenName();
|
|
82867
|
+
const before = latest();
|
|
82597
82868
|
const result = await driver.type(secret, element.bounds);
|
|
82598
82869
|
const absorbed = await absorbActionResult(result, "mobile_fill_credential");
|
|
82599
82870
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82601,7 +82872,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82601
82872
|
type: "TYPE",
|
|
82602
82873
|
value: (0, shared_1.mobileCredentialPlaceholder)(key),
|
|
82603
82874
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82604
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82875
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82605
82876
|
});
|
|
82606
82877
|
log2(` fill_credential: recorded {{${key}}} into ${labelForElement(element)}`);
|
|
82607
82878
|
}
|
|
@@ -82629,13 +82900,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82629
82900
|
if (!element)
|
|
82630
82901
|
return unknownRef(toolArgs.ref);
|
|
82631
82902
|
const beforeScreenName = screenName();
|
|
82903
|
+
const before = latest();
|
|
82632
82904
|
const result = await driver.clear(element.bounds);
|
|
82633
82905
|
const absorbed = await absorbActionResult(result, "mobile_clear");
|
|
82634
82906
|
if (shouldRecordAction(result, absorbed)) {
|
|
82635
82907
|
recordAction({
|
|
82636
82908
|
type: "CLEAR",
|
|
82637
82909
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82638
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82910
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82639
82911
|
});
|
|
82640
82912
|
}
|
|
82641
82913
|
return actionPayload(result, absorbed);
|
|
@@ -82698,7 +82970,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82698
82970
|
case "mobile_assert_visible": {
|
|
82699
82971
|
const hybrid = resolveHybridRef(toolArgs.ref);
|
|
82700
82972
|
if (hybrid) {
|
|
82701
|
-
const metadata = metadataForHybridElement(hybrid, screenName());
|
|
82973
|
+
const metadata = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82702
82974
|
recordAction({
|
|
82703
82975
|
type: "ASSERT_VISIBLE",
|
|
82704
82976
|
selector: `css=${hybrid.css}`,
|
|
@@ -82738,7 +83010,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82738
83010
|
error: `Live WEBVIEW text assertion failed: expected ${JSON.stringify(expected)} was not present on ref ${hybrid.ref}. Re-snapshot after the outcome appears and assert the text that is actually visible.`
|
|
82739
83011
|
};
|
|
82740
83012
|
}
|
|
82741
|
-
const metadata2 = metadataForHybridElement(hybrid, screenName());
|
|
83013
|
+
const metadata2 = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82742
83014
|
const stableWebIdentity = Boolean(nonEmpty3(hybrid.id) || nonEmpty3(hybrid.name));
|
|
82743
83015
|
if (stableWebIdentity) {
|
|
82744
83016
|
recordAction({
|
|
@@ -82791,7 +83063,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82791
83063
|
error: `Expected text ${JSON.stringify(expected)} is ${volatileExpected.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileExpected.reason}) \u2014 it changes between runs, so the test would fail forever on a healthy app. Assert durable post-state instead: an element unique to the destination screen, a stable label, or text your flow itself entered.`
|
|
82792
83064
|
};
|
|
82793
83065
|
}
|
|
82794
|
-
const metadata = metadataForElement(element, screenName(), platform3);
|
|
83066
|
+
const metadata = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82795
83067
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82796
83068
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
82797
83069
|
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
@@ -82837,7 +83109,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82837
83109
|
error: `Ref ${element.ref} is not a durable assertion target (${weakReason2}). Re-snapshot and assert a human-visible label, text, or stable control that proves the screen outcome.`
|
|
82838
83110
|
};
|
|
82839
83111
|
}
|
|
82840
|
-
const metadata2 = metadataForElement(element, screenName(), platform3);
|
|
83112
|
+
const metadata2 = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82841
83113
|
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata: metadata2 });
|
|
82842
83114
|
return { ok: true, label: metadata2.targetLabel ?? "element" };
|
|
82843
83115
|
}
|
|
@@ -82887,7 +83159,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82887
83159
|
error: `The live element matching ${JSON.stringify(accessibilityId ?? text)} is not a durable assertion target (${weakReason}).`
|
|
82888
83160
|
};
|
|
82889
83161
|
}
|
|
82890
|
-
const metadata = metadataForElement(liveElement, screenName(), platform3);
|
|
83162
|
+
const metadata = metadataForElement(liveElement, screenName(), platform3, live.screen.elements);
|
|
82891
83163
|
recordAction({
|
|
82892
83164
|
type: "ASSERT_VISIBLE",
|
|
82893
83165
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(liveElement),
|
|
@@ -83015,7 +83287,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83015
83287
|
actions.push(finalizeAction({
|
|
83016
83288
|
type: "ASSERT_VISIBLE",
|
|
83017
83289
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(anchor),
|
|
83018
|
-
metadata: metadataForElement(anchor, screen, platform3)
|
|
83290
|
+
metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
|
|
83019
83291
|
}));
|
|
83020
83292
|
return true;
|
|
83021
83293
|
}
|
|
@@ -83358,6 +83630,153 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83358
83630
|
}
|
|
83359
83631
|
return out;
|
|
83360
83632
|
}
|
|
83633
|
+
function isDurableMutation(action) {
|
|
83634
|
+
if (action.type === "TYPE" || action.type === "CLEAR")
|
|
83635
|
+
return true;
|
|
83636
|
+
if (action.type === "TAP")
|
|
83637
|
+
return (0, shared_1.isStateValueMobileClassName)(action.metadata.locatorBundle?.className);
|
|
83638
|
+
return false;
|
|
83639
|
+
}
|
|
83640
|
+
function mobileWanderTraceEnabled2() {
|
|
83641
|
+
const v = (process.env.VALIDATEQA_MOBILE_WANDER_TRACE || "").trim().toLowerCase();
|
|
83642
|
+
return v === "true" || v === "1";
|
|
83643
|
+
}
|
|
83644
|
+
function wanderTraceLabel(action) {
|
|
83645
|
+
const identity = action.metadata.targetLabel || action.selector || action.metadata.locatorBundle?.accessibilityId || action.metadata.locatorBundle?.resourceId || "(no identity)";
|
|
83646
|
+
return `${action.type} ${identity}`;
|
|
83647
|
+
}
|
|
83648
|
+
function logWanderDiff(pass, before, after, log2) {
|
|
83649
|
+
if (after.length >= before.length)
|
|
83650
|
+
return;
|
|
83651
|
+
log2(` ${pass}: collapsed ${before.length - after.length} step(s) (${before.length} \u2192 ${after.length}).`);
|
|
83652
|
+
if (!mobileWanderTraceEnabled2())
|
|
83653
|
+
return;
|
|
83654
|
+
const survivors = new Set(after);
|
|
83655
|
+
for (const action of before) {
|
|
83656
|
+
if (!survivors.has(action)) {
|
|
83657
|
+
log2(` [wander] ${pass}: dropped ${wanderTraceLabel(action)}`);
|
|
83658
|
+
}
|
|
83659
|
+
}
|
|
83660
|
+
}
|
|
83661
|
+
function coalesceSameFieldTypes(actions) {
|
|
83662
|
+
if (actions.length <= 1)
|
|
83663
|
+
return actions;
|
|
83664
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83665
|
+
const lastTypeIndexBySelector = /* @__PURE__ */ new Map();
|
|
83666
|
+
const refocusTapIndexesBySelector = /* @__PURE__ */ new Map();
|
|
83667
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83668
|
+
const current = actions[i];
|
|
83669
|
+
if (current.type === "ASSERT_VISIBLE" || current.type === "ASSERT_TEXT") {
|
|
83670
|
+
lastTypeIndexBySelector.clear();
|
|
83671
|
+
refocusTapIndexesBySelector.clear();
|
|
83672
|
+
continue;
|
|
83673
|
+
}
|
|
83674
|
+
if (current.type === "CLEAR") {
|
|
83675
|
+
lastTypeIndexBySelector.delete(current.selector);
|
|
83676
|
+
refocusTapIndexesBySelector.delete(current.selector);
|
|
83677
|
+
continue;
|
|
83678
|
+
}
|
|
83679
|
+
if (isFormCommitAction(current)) {
|
|
83680
|
+
lastTypeIndexBySelector.clear();
|
|
83681
|
+
refocusTapIndexesBySelector.clear();
|
|
83682
|
+
continue;
|
|
83683
|
+
}
|
|
83684
|
+
if (current.type === "TAP") {
|
|
83685
|
+
if (current.selector) {
|
|
83686
|
+
const list = refocusTapIndexesBySelector.get(current.selector) ?? [];
|
|
83687
|
+
list.push(i);
|
|
83688
|
+
refocusTapIndexesBySelector.set(current.selector, list);
|
|
83689
|
+
}
|
|
83690
|
+
continue;
|
|
83691
|
+
}
|
|
83692
|
+
if (current.type !== "TYPE" || !current.selector)
|
|
83693
|
+
continue;
|
|
83694
|
+
const priorIndex = lastTypeIndexBySelector.get(current.selector);
|
|
83695
|
+
if (priorIndex !== void 0) {
|
|
83696
|
+
toDrop.add(priorIndex);
|
|
83697
|
+
for (const tapIndex of refocusTapIndexesBySelector.get(current.selector) ?? []) {
|
|
83698
|
+
if (tapIndex > priorIndex && tapIndex < i)
|
|
83699
|
+
toDrop.add(tapIndex);
|
|
83700
|
+
}
|
|
83701
|
+
}
|
|
83702
|
+
lastTypeIndexBySelector.set(current.selector, i);
|
|
83703
|
+
refocusTapIndexesBySelector.set(current.selector, []);
|
|
83704
|
+
}
|
|
83705
|
+
if (toDrop.size === 0)
|
|
83706
|
+
return actions;
|
|
83707
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83708
|
+
}
|
|
83709
|
+
function coalesceRepeatedTaps(actions) {
|
|
83710
|
+
if (actions.length <= 1)
|
|
83711
|
+
return actions;
|
|
83712
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83713
|
+
const lastTapIndexBySelector = /* @__PURE__ */ new Map();
|
|
83714
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83715
|
+
const action = actions[i];
|
|
83716
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT") {
|
|
83717
|
+
lastTapIndexBySelector.clear();
|
|
83718
|
+
continue;
|
|
83719
|
+
}
|
|
83720
|
+
if (isDurableMutation(action)) {
|
|
83721
|
+
lastTapIndexBySelector.clear();
|
|
83722
|
+
continue;
|
|
83723
|
+
}
|
|
83724
|
+
if (action.type !== "TAP" || !action.selector)
|
|
83725
|
+
continue;
|
|
83726
|
+
const key = action.metadata.structuralKey;
|
|
83727
|
+
const priorIndex = lastTapIndexBySelector.get(action.selector);
|
|
83728
|
+
if (priorIndex !== void 0 && key && actions[priorIndex].metadata.structuralKey === key) {
|
|
83729
|
+
const priorHash = actions[priorIndex].metadata.screenHash;
|
|
83730
|
+
const currentHash = action.metadata.screenHash;
|
|
83731
|
+
const screenAdvanced = !!priorHash && !!currentHash && priorHash !== currentHash;
|
|
83732
|
+
if (!screenAdvanced) {
|
|
83733
|
+
toDrop.add(priorIndex);
|
|
83734
|
+
}
|
|
83735
|
+
}
|
|
83736
|
+
lastTapIndexBySelector.set(action.selector, i);
|
|
83737
|
+
}
|
|
83738
|
+
if (toDrop.size === 0)
|
|
83739
|
+
return actions;
|
|
83740
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83741
|
+
}
|
|
83742
|
+
var MAX_DETOUR_LENGTH = 6;
|
|
83743
|
+
function elideNoOpDetours(actions) {
|
|
83744
|
+
if (actions.length < 2)
|
|
83745
|
+
return actions;
|
|
83746
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83747
|
+
for (let j = 0; j < actions.length; j++) {
|
|
83748
|
+
if (toDrop.has(j))
|
|
83749
|
+
continue;
|
|
83750
|
+
if (actions[j].type !== "BACK" && actions[j].type !== "HOME")
|
|
83751
|
+
continue;
|
|
83752
|
+
const keyAfter = actions[j].metadata.structuralKey;
|
|
83753
|
+
if (!keyAfter)
|
|
83754
|
+
continue;
|
|
83755
|
+
let bestStart = null;
|
|
83756
|
+
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83757
|
+
for (let i = j; i >= earliestStart; i--) {
|
|
83758
|
+
if (i < j) {
|
|
83759
|
+
const inside = actions[i];
|
|
83760
|
+
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83761
|
+
break;
|
|
83762
|
+
}
|
|
83763
|
+
}
|
|
83764
|
+
if (i === 0)
|
|
83765
|
+
continue;
|
|
83766
|
+
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83767
|
+
if (i < j && keyBefore && keyBefore === keyAfter) {
|
|
83768
|
+
bestStart = i;
|
|
83769
|
+
}
|
|
83770
|
+
}
|
|
83771
|
+
if (bestStart !== null) {
|
|
83772
|
+
for (let k = bestStart; k <= j; k++)
|
|
83773
|
+
toDrop.add(k);
|
|
83774
|
+
}
|
|
83775
|
+
}
|
|
83776
|
+
if (toDrop.size === 0)
|
|
83777
|
+
return actions;
|
|
83778
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83779
|
+
}
|
|
83361
83780
|
function mobileActionsToPseudoPlaywright(actions) {
|
|
83362
83781
|
const escape = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
83363
83782
|
const lines = ["test('mobile', async ({ page }) => {"];
|
|
@@ -83445,6 +83864,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83445
83864
|
if (actions.length < beforeCycleCollapse) {
|
|
83446
83865
|
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
83447
83866
|
}
|
|
83867
|
+
const beforeTypeCoalesce = actions;
|
|
83868
|
+
actions = coalesceSameFieldTypes(actions);
|
|
83869
|
+
logWanderDiff("coalesceSameFieldTypes", beforeTypeCoalesce, actions, log2);
|
|
83870
|
+
const beforeTapCoalesce = actions;
|
|
83871
|
+
actions = coalesceRepeatedTaps(actions);
|
|
83872
|
+
logWanderDiff("coalesceRepeatedTaps", beforeTapCoalesce, actions, log2);
|
|
83873
|
+
const beforeDetourElision = actions;
|
|
83874
|
+
actions = elideNoOpDetours(actions);
|
|
83875
|
+
logWanderDiff("elideNoOpDetours", beforeDetourElision, actions, log2);
|
|
83448
83876
|
const beforeContainerDrop = actions.length;
|
|
83449
83877
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
83450
83878
|
if (actions.length < beforeContainerDrop) {
|
|
@@ -83629,48 +84057,29 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83629
84057
|
log2(" app data reset skipped by policy \u2014 scenario records after relaunch with preserved data.");
|
|
83630
84058
|
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: false, coldResetUnavailable: false, refuseGenerate: false, reason: "preserve_policy" }, log2);
|
|
83631
84059
|
}
|
|
83632
|
-
|
|
83633
|
-
|
|
83634
|
-
|
|
83635
|
-
|
|
83636
|
-
|
|
83637
|
-
|
|
83638
|
-
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
|
|
83639
|
-
}
|
|
83640
|
-
await driver.relaunch();
|
|
83641
|
-
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
83642
|
-
log2(" CLEAR_APP_DATA required but app-data reset was unavailable \u2014 refusing to generate from warm state.");
|
|
83643
|
-
return {
|
|
83644
|
-
appDataCleared: false,
|
|
83645
|
-
coldResetUnavailable: true,
|
|
83646
|
-
refuseGenerate: true,
|
|
83647
|
-
reason: "clear_app_data_unavailable"
|
|
83648
|
-
};
|
|
83649
|
-
}
|
|
83650
|
-
log2(" app-data reset unavailable \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay).");
|
|
83651
|
-
return applyScenarioEntryDeepLink(driver, target, {
|
|
83652
|
-
appDataCleared: false,
|
|
83653
|
-
coldResetUnavailable: true,
|
|
83654
|
-
refuseGenerate: false,
|
|
83655
|
-
reason: platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
|
|
83656
|
-
}, log2);
|
|
84060
|
+
const appDataCleared = await driver.resetAppState?.().catch((err) => {
|
|
84061
|
+
log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
84062
|
+
return false;
|
|
84063
|
+
}) ?? false;
|
|
84064
|
+
if (appDataCleared) {
|
|
84065
|
+
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
|
|
83657
84066
|
}
|
|
83658
84067
|
await driver.relaunch();
|
|
83659
84068
|
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
83660
|
-
log2(" CLEAR_APP_DATA required but iOS
|
|
84069
|
+
log2(platform3 === "IOS" ? " CLEAR_APP_DATA required but this iOS device cannot be cleared (simulator required) \u2014 refusing to generate from warm state." : " CLEAR_APP_DATA required but app-data reset was unavailable \u2014 refusing to generate from warm state.");
|
|
83661
84070
|
return {
|
|
83662
84071
|
appDataCleared: false,
|
|
83663
84072
|
coldResetUnavailable: true,
|
|
83664
84073
|
refuseGenerate: true,
|
|
83665
|
-
reason: "ios_clear_app_data_unavailable"
|
|
84074
|
+
reason: platform3 === "IOS" ? "ios_clear_app_data_unavailable" : "clear_app_data_unavailable"
|
|
83666
84075
|
};
|
|
83667
84076
|
}
|
|
83668
|
-
log2(" iOS
|
|
84077
|
+
log2(platform3 === "IOS" ? " iOS app-data reset unavailable (physical device or simctl failure) \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay)." : " app-data reset unavailable \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay).");
|
|
83669
84078
|
return applyScenarioEntryDeepLink(driver, target, {
|
|
83670
84079
|
appDataCleared: false,
|
|
83671
84080
|
coldResetUnavailable: true,
|
|
83672
84081
|
refuseGenerate: false,
|
|
83673
|
-
reason: "ios_no_clear_app"
|
|
84082
|
+
reason: platform3 === "IOS" ? "ios_no_clear_app" : platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
|
|
83674
84083
|
}, log2);
|
|
83675
84084
|
}
|
|
83676
84085
|
async function runMobileGeneratePhase(args) {
|
|
@@ -84273,6 +84682,8 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
84273
84682
|
"use strict";
|
|
84274
84683
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
84275
84684
|
exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = void 0;
|
|
84685
|
+
exports2.timed = timed;
|
|
84686
|
+
exports2.timedSync = timedSync;
|
|
84276
84687
|
exports2.executeMobileScriptDrivenHeal = executeMobileScriptDrivenHeal;
|
|
84277
84688
|
exports2.executeMobileTestWithHealing = executeMobileTestWithHealing2;
|
|
84278
84689
|
exports2.executeMobileLiveHeal = executeMobileLiveHeal2;
|
|
@@ -84482,6 +84893,32 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84482
84893
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
84483
84894
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
84484
84895
|
}
|
|
84896
|
+
function formatHealTimingLine(category, label, ms, failed, errMessage) {
|
|
84897
|
+
const base2 = `\u{1FA7A}\u23F1 [${category}] ${label} \u2014 ${ms}ms`;
|
|
84898
|
+
return failed ? `${base2} (FAILED: ${errMessage ?? "unknown error"})` : base2;
|
|
84899
|
+
}
|
|
84900
|
+
async function timed(category, label, onProgress, fn) {
|
|
84901
|
+
const start = Date.now();
|
|
84902
|
+
try {
|
|
84903
|
+
const result = await fn();
|
|
84904
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84905
|
+
return result;
|
|
84906
|
+
} catch (err) {
|
|
84907
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84908
|
+
throw err;
|
|
84909
|
+
}
|
|
84910
|
+
}
|
|
84911
|
+
function timedSync(category, label, onProgress, fn) {
|
|
84912
|
+
const start = Date.now();
|
|
84913
|
+
try {
|
|
84914
|
+
const result = fn();
|
|
84915
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84916
|
+
return result;
|
|
84917
|
+
} catch (err) {
|
|
84918
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84919
|
+
throw err;
|
|
84920
|
+
}
|
|
84921
|
+
}
|
|
84485
84922
|
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
84486
84923
|
function isIdentityEqualityXpath(value) {
|
|
84487
84924
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
@@ -84699,21 +85136,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84699
85136
|
return JSON.stringify(base2?.target) !== JSON.stringify(healed.target);
|
|
84700
85137
|
}
|
|
84701
85138
|
function findMissingChangedLocators(baseSteps, healedSteps, screen) {
|
|
84702
|
-
const
|
|
85139
|
+
const results = [];
|
|
84703
85140
|
healedSteps.forEach((healed, index) => {
|
|
84704
85141
|
if (!stepTargetChanged(baseSteps[index], healed))
|
|
84705
85142
|
return;
|
|
84706
85143
|
const candidate = stepToLocatorCandidate(healed);
|
|
84707
85144
|
if (!candidate)
|
|
84708
85145
|
return;
|
|
84709
|
-
|
|
84710
|
-
|
|
84711
|
-
|
|
84712
|
-
|
|
84713
|
-
});
|
|
85146
|
+
const matchCount = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen).length;
|
|
85147
|
+
const label = healed.target?.primary ?? JSON.stringify(candidate);
|
|
85148
|
+
const expressions = stepIdentityExpressions(healed);
|
|
85149
|
+
if (matchCount === 0) {
|
|
85150
|
+
results.push({ label, expressions, kind: "missing" });
|
|
85151
|
+
} else if (matchCount > 1) {
|
|
85152
|
+
results.push({ label, expressions, kind: "ambiguous", matchCount });
|
|
84714
85153
|
}
|
|
84715
85154
|
});
|
|
84716
|
-
return
|
|
85155
|
+
return results;
|
|
84717
85156
|
}
|
|
84718
85157
|
function changedStepExpressions(baseSteps, healedSteps) {
|
|
84719
85158
|
const out = [];
|
|
@@ -84942,10 +85381,10 @@ ${args.directive}`);
|
|
|
84942
85381
|
let content;
|
|
84943
85382
|
let finishReason;
|
|
84944
85383
|
try {
|
|
84945
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
85384
|
+
const response = await timed("model", `phase1-attempt-${attempt}-model-call`, options.onProgress, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
84946
85385
|
arm();
|
|
84947
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
84948
|
-
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
85386
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85387
|
+
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true }));
|
|
84949
85388
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84950
85389
|
finishReason = response?.choices?.[0]?.finish_reason ?? void 0;
|
|
84951
85390
|
} catch {
|
|
@@ -84988,14 +85427,18 @@ ${args.directive}`);
|
|
|
84988
85427
|
continue;
|
|
84989
85428
|
}
|
|
84990
85429
|
if (activeFailingSource && isLocatorNotFound(activeError) && !isDrift(activeError)) {
|
|
84991
|
-
const
|
|
84992
|
-
const missing =
|
|
85430
|
+
const failingSourceSnapshot = activeFailingSource;
|
|
85431
|
+
const missing = timedSync("device-op", `phase1-attempt-${attempt}-static-verify`, options.onProgress, () => {
|
|
85432
|
+
const screen = (0, mobile_source_parser_js_1.parseMobileSource)(failingSourceSnapshot, platform3);
|
|
85433
|
+
return findMissingChangedLocators(baseSteps, healedSteps, screen);
|
|
85434
|
+
});
|
|
84993
85435
|
if (missing.length > 0) {
|
|
84994
85436
|
for (const entry of missing)
|
|
84995
85437
|
pushUnique(triedLocators, entry.expressions);
|
|
84996
85438
|
markRecoverableTurn();
|
|
84997
85439
|
lastVerdict = "fix";
|
|
84998
|
-
|
|
85440
|
+
const first = missing[0];
|
|
85441
|
+
pendingDirective = first.kind === "ambiguous" ? `Your proposed locator ${first.label} matches ${first.matchCount ?? "more than one"} elements in the captured screen tree \u2014 it cannot identify the recorded instance. Pick an identity locator (resource-id, accessibility-id, or exact @text=) that resolves to EXACTLY ONE element in the tree.` : `Your proposed locator ${first.label} does NOT exist in the captured screen tree. Pick an identity locator (resource-id, accessibility-id, or exact @text=) that is PRESENT in the tree.`;
|
|
84999
85442
|
continue;
|
|
85000
85443
|
}
|
|
85001
85444
|
}
|
|
@@ -85009,9 +85452,10 @@ ${args.directive}`);
|
|
|
85009
85452
|
pendingDirective = "The proposed fix could not be device-verified here. If it is not clearly correct, revise it.";
|
|
85010
85453
|
continue;
|
|
85011
85454
|
}
|
|
85455
|
+
const runNativeTest = options.runNativeTest;
|
|
85012
85456
|
let verify;
|
|
85013
85457
|
try {
|
|
85014
|
-
verify = await options.runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables });
|
|
85458
|
+
verify = await timed("reexec", `phase1-attempt-${attempt}-device-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables }));
|
|
85015
85459
|
} catch {
|
|
85016
85460
|
return {
|
|
85017
85461
|
passed: false,
|
|
@@ -85118,6 +85562,9 @@ ${args.directive}`);
|
|
|
85118
85562
|
}
|
|
85119
85563
|
}
|
|
85120
85564
|
async function executeMobileTestWithHealing2(steps, target, ctx, options) {
|
|
85565
|
+
return timed("total", "heal-total", options.onProgress, () => executeMobileTestWithHealingImpl(steps, target, ctx, options));
|
|
85566
|
+
}
|
|
85567
|
+
async function executeMobileTestWithHealingImpl(steps, target, ctx, options) {
|
|
85121
85568
|
const invalidAuthSetup = detectInvalidMobileAuthSetup(steps, ctx.error);
|
|
85122
85569
|
if (invalidAuthSetup) {
|
|
85123
85570
|
return { passed: false, verdict: "test_invalid", healReason: invalidAuthSetup };
|
|
@@ -85244,9 +85691,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85244
85691
|
const candidate = partsToCandidate(parts);
|
|
85245
85692
|
if (!hasIdentitySignal(candidate))
|
|
85246
85693
|
return;
|
|
85247
|
-
const
|
|
85248
|
-
|
|
85249
|
-
|
|
85694
|
+
const matches = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen);
|
|
85695
|
+
const target = matches.find((el) => el.bounds.width > 0 && el.bounds.height > 0) ?? matches[0];
|
|
85696
|
+
if (target)
|
|
85697
|
+
await driver.tap(target.bounds);
|
|
85250
85698
|
}
|
|
85251
85699
|
function buildLiveUserContent(args) {
|
|
85252
85700
|
const parts = [];
|
|
@@ -85311,7 +85759,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85311
85759
|
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
85312
85760
|
let snap;
|
|
85313
85761
|
try {
|
|
85314
|
-
snap = await liveDriver.snapshot();
|
|
85762
|
+
snap = await timed("device-op", `phase2-pass-${pass}-snapshot`, options.onProgress, () => liveDriver.snapshot());
|
|
85315
85763
|
} catch {
|
|
85316
85764
|
return {
|
|
85317
85765
|
passed: false,
|
|
@@ -85337,10 +85785,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85337
85785
|
];
|
|
85338
85786
|
let content;
|
|
85339
85787
|
try {
|
|
85340
|
-
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(keepAlivePingable, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
85788
|
+
const response = await timed("model", `phase2-pass-${pass}-model-call`, options.onProgress, () => (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(keepAlivePingable, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
85341
85789
|
arm();
|
|
85342
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
85343
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
85790
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85791
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true })));
|
|
85344
85792
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
85345
85793
|
} catch {
|
|
85346
85794
|
continue;
|
|
@@ -85372,7 +85820,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85372
85820
|
const healedSteps = parsed.steps;
|
|
85373
85821
|
let verify;
|
|
85374
85822
|
try {
|
|
85375
|
-
verify = await runNativeTest({ steps: healedSteps, target, testVariables });
|
|
85823
|
+
verify = await timed("reexec", `phase2-pass-${pass}-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables }));
|
|
85376
85824
|
} catch {
|
|
85377
85825
|
return {
|
|
85378
85826
|
passed: false,
|
|
@@ -89226,7 +89674,7 @@ var require_kill_tree = __commonJS({
|
|
|
89226
89674
|
var node_child_process_1 = require("child_process");
|
|
89227
89675
|
var node_util_1 = require("util");
|
|
89228
89676
|
var node_fs_1 = require("fs");
|
|
89229
|
-
var
|
|
89677
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
89230
89678
|
var PS_TIMEOUT_MS = 2e3;
|
|
89231
89679
|
var PS_BUFFER = 1024 * 1024;
|
|
89232
89680
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -89280,7 +89728,7 @@ var require_kill_tree = __commonJS({
|
|
|
89280
89728
|
timer.unref?.();
|
|
89281
89729
|
let stdout;
|
|
89282
89730
|
try {
|
|
89283
|
-
const result = await
|
|
89731
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89284
89732
|
maxBuffer: PS_BUFFER,
|
|
89285
89733
|
signal: controller.signal
|
|
89286
89734
|
});
|
|
@@ -89318,7 +89766,7 @@ var require_kill_tree = __commonJS({
|
|
|
89318
89766
|
timer.unref?.();
|
|
89319
89767
|
let stdout;
|
|
89320
89768
|
try {
|
|
89321
|
-
const result = await
|
|
89769
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89322
89770
|
maxBuffer: PS_BUFFER,
|
|
89323
89771
|
signal: controller.signal
|
|
89324
89772
|
});
|
|
@@ -89395,7 +89843,7 @@ var require_kill_tree = __commonJS({
|
|
|
89395
89843
|
timer.unref?.();
|
|
89396
89844
|
let stdout;
|
|
89397
89845
|
try {
|
|
89398
|
-
const result = await
|
|
89846
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid="], {
|
|
89399
89847
|
maxBuffer: PS_BUFFER,
|
|
89400
89848
|
signal: controller.signal
|
|
89401
89849
|
});
|
|
@@ -99274,8 +99722,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99274
99722
|
onRetry: (attempt, reason, delayMs) => logs2.push(` \u26A0 scenario-gen AI call retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
99275
99723
|
});
|
|
99276
99724
|
} catch (aiErr) {
|
|
99277
|
-
const
|
|
99278
|
-
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${
|
|
99725
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
99726
|
+
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg3}`);
|
|
99279
99727
|
throw (0, ai_retry_js_1.markAIProviderError)(aiErr);
|
|
99280
99728
|
}
|
|
99281
99729
|
const iterDuration = Date.now() - iterStart;
|
|
@@ -99318,8 +99766,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99318
99766
|
try {
|
|
99319
99767
|
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
99320
99768
|
} catch (parseErr) {
|
|
99321
|
-
const
|
|
99322
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content:
|
|
99769
|
+
const errMsg3 = `Invalid tool arguments: ${parseErr.message}`;
|
|
99770
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg3 });
|
|
99323
99771
|
continue;
|
|
99324
99772
|
}
|
|
99325
99773
|
if (toolName === "finish_generate") {
|
|
@@ -100577,7 +101025,7 @@ var require_util4 = __commonJS({
|
|
|
100577
101025
|
}
|
|
100578
101026
|
path = url.path;
|
|
100579
101027
|
}
|
|
100580
|
-
var
|
|
101028
|
+
var isAbsolute2 = exports2.isAbsolute(path);
|
|
100581
101029
|
var parts = path.split(/\/+/);
|
|
100582
101030
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
100583
101031
|
part = parts[i];
|
|
@@ -100597,7 +101045,7 @@ var require_util4 = __commonJS({
|
|
|
100597
101045
|
}
|
|
100598
101046
|
path = parts.join("/");
|
|
100599
101047
|
if (path === "") {
|
|
100600
|
-
path =
|
|
101048
|
+
path = isAbsolute2 ? "/" : ".";
|
|
100601
101049
|
}
|
|
100602
101050
|
if (url) {
|
|
100603
101051
|
url.path = path;
|
|
@@ -100606,7 +101054,7 @@ var require_util4 = __commonJS({
|
|
|
100606
101054
|
return path;
|
|
100607
101055
|
}
|
|
100608
101056
|
exports2.normalize = normalize;
|
|
100609
|
-
function
|
|
101057
|
+
function join8(aRoot, aPath) {
|
|
100610
101058
|
if (aRoot === "") {
|
|
100611
101059
|
aRoot = ".";
|
|
100612
101060
|
}
|
|
@@ -100638,7 +101086,7 @@ var require_util4 = __commonJS({
|
|
|
100638
101086
|
}
|
|
100639
101087
|
return joined;
|
|
100640
101088
|
}
|
|
100641
|
-
exports2.join =
|
|
101089
|
+
exports2.join = join8;
|
|
100642
101090
|
exports2.isAbsolute = function(aPath) {
|
|
100643
101091
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
100644
101092
|
};
|
|
@@ -100811,7 +101259,7 @@ var require_util4 = __commonJS({
|
|
|
100811
101259
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
100812
101260
|
}
|
|
100813
101261
|
}
|
|
100814
|
-
sourceURL =
|
|
101262
|
+
sourceURL = join8(urlGenerate(parsed), sourceURL);
|
|
100815
101263
|
}
|
|
100816
101264
|
return normalize(sourceURL);
|
|
100817
101265
|
}
|
|
@@ -314524,8 +314972,8 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314524
314972
|
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel)
|
|
314525
314973
|
})), { label: `script-heal-${attempt}` });
|
|
314526
314974
|
} catch (aiErr) {
|
|
314527
|
-
const
|
|
314528
|
-
logs2.push(`\u26A0 AI call failed: ${
|
|
314975
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
314976
|
+
logs2.push(`\u26A0 AI call failed: ${errMsg3}`);
|
|
314529
314977
|
attemptHistory.push({
|
|
314530
314978
|
phase: "script",
|
|
314531
314979
|
attempt,
|
|
@@ -314533,13 +314981,13 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314533
314981
|
passed: false,
|
|
314534
314982
|
changedCode: false,
|
|
314535
314983
|
summary: "AI call failed before returning a heal result",
|
|
314536
|
-
errorSummary: truncateForPrompt(
|
|
314984
|
+
errorSummary: truncateForPrompt(errMsg3),
|
|
314537
314985
|
failedStep: summarizeFailedStep(currentStepResults)
|
|
314538
314986
|
});
|
|
314539
314987
|
if (feedbackEnabled) {
|
|
314540
|
-
lastTransportFailure = truncateForPrompt(
|
|
314988
|
+
lastTransportFailure = truncateForPrompt(errMsg3, 200) ?? "the AI request failed";
|
|
314541
314989
|
} else {
|
|
314542
|
-
currentError = `AI call failed on attempt ${attempt}: ${
|
|
314990
|
+
currentError = `AI call failed on attempt ${attempt}: ${errMsg3}`;
|
|
314543
314991
|
}
|
|
314544
314992
|
markRecoverableTurn();
|
|
314545
314993
|
continue;
|
|
@@ -314823,8 +315271,8 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314823
315271
|
// item 5b
|
|
314824
315272
|
});
|
|
314825
315273
|
} catch (runErr) {
|
|
314826
|
-
const
|
|
314827
|
-
logs2.push(`\u{1F4A5} Native execution threw: ${
|
|
315274
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
315275
|
+
logs2.push(`\u{1F4A5} Native execution threw: ${errMsg3}`);
|
|
314828
315276
|
attemptHistory.push({
|
|
314829
315277
|
phase: "script",
|
|
314830
315278
|
attempt,
|
|
@@ -314832,9 +315280,9 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314832
315280
|
passed: false,
|
|
314833
315281
|
changedCode: true,
|
|
314834
315282
|
summary: truncateForPrompt(changeDesc) ?? "Applied code fix, but native execution threw before verification completed",
|
|
314835
|
-
errorSummary: truncateForPrompt(
|
|
315283
|
+
errorSummary: truncateForPrompt(errMsg3)
|
|
314836
315284
|
});
|
|
314837
|
-
currentError = `Native execution threw: ${
|
|
315285
|
+
currentError = `Native execution threw: ${errMsg3}`;
|
|
314838
315286
|
currentStepResults = [];
|
|
314839
315287
|
continue;
|
|
314840
315288
|
}
|
|
@@ -315590,9 +316038,9 @@ Last error (after heal attempts): ${lastErrorSnippet}` : `Heal aborted: same err
|
|
|
315590
316038
|
previousErrorSignature = errorSig;
|
|
315591
316039
|
}
|
|
315592
316040
|
} catch (runErr) {
|
|
315593
|
-
const
|
|
315594
|
-
logs2.push(`\u{1F4A5} Native run threw: ${
|
|
315595
|
-
currentError =
|
|
316041
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
316042
|
+
logs2.push(`\u{1F4A5} Native run threw: ${errMsg3}`);
|
|
316043
|
+
currentError = errMsg3;
|
|
315596
316044
|
currentStepResults = [];
|
|
315597
316045
|
}
|
|
315598
316046
|
} else {
|
|
@@ -316154,8 +316602,8 @@ When you call finish_fix, preserve locators only when they are stable structural
|
|
|
316154
316602
|
}
|
|
316155
316603
|
}
|
|
316156
316604
|
} catch (toolErr) {
|
|
316157
|
-
const
|
|
316158
|
-
let errorContent = `Error: ${
|
|
316605
|
+
const errMsg3 = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
316606
|
+
let errorContent = `Error: ${errMsg3}`;
|
|
316159
316607
|
try {
|
|
316160
316608
|
const consoleResult = await mcpClient.callTool({ name: "browser_console_messages", arguments: {} });
|
|
316161
316609
|
const consoleText = Array.isArray(consoleResult.content) ? consoleResult.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n") : String(consoleResult.content ?? "");
|
|
@@ -316176,8 +316624,8 @@ ${errorContent}`;
|
|
|
316176
316624
|
}
|
|
316177
316625
|
}
|
|
316178
316626
|
} catch (mcpErr) {
|
|
316179
|
-
const
|
|
316180
|
-
logs2.push(`\u{1F4A5} MCP exploration failed: ${
|
|
316627
|
+
const errMsg3 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
|
|
316628
|
+
logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg3}`);
|
|
316181
316629
|
}
|
|
316182
316630
|
if (verdict === "app_bug") {
|
|
316183
316631
|
phase2AttemptHistory.push({
|
|
@@ -316771,9 +317219,9 @@ ${testBlocks}
|
|
|
316771
317219
|
}
|
|
316772
317220
|
emit(` \u2713 ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
316773
317221
|
} catch (err) {
|
|
316774
|
-
const
|
|
316775
|
-
toolResultContent = `ERROR: ${
|
|
316776
|
-
emit(` \u2717 ${
|
|
317222
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
317223
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
317224
|
+
emit(` \u2717 ${errMsg3}`);
|
|
316777
317225
|
}
|
|
316778
317226
|
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
|
|
316779
317227
|
}
|
|
@@ -323057,8 +323505,8 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
323057
323505
|
const snapItems = Array.isArray(snapResult.content) ? snapResult.content : [];
|
|
323058
323506
|
snapshotText = extractInlineSnapshotYaml(snapItems.find((c) => c.type === "text")?.text || null);
|
|
323059
323507
|
} catch (err) {
|
|
323060
|
-
const
|
|
323061
|
-
const isTimeout = /timeout|timed out|TimeoutError/i.test(
|
|
323508
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
323509
|
+
const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg3);
|
|
323062
323510
|
return {
|
|
323063
323511
|
snapshotText: null,
|
|
323064
323512
|
meta: {
|
|
@@ -327134,9 +327582,9 @@ ${currentSummary}`;
|
|
|
327134
327582
|
});
|
|
327135
327583
|
}
|
|
327136
327584
|
} catch (err) {
|
|
327137
|
-
const
|
|
327138
|
-
logs2.push(` Error persisting page: ${
|
|
327139
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${
|
|
327585
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327586
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327587
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg3}` });
|
|
327140
327588
|
}
|
|
327141
327589
|
continue;
|
|
327142
327590
|
}
|
|
@@ -327338,12 +327786,12 @@ ${currentSummary}`;
|
|
|
327338
327786
|
})
|
|
327339
327787
|
});
|
|
327340
327788
|
} catch (err) {
|
|
327341
|
-
const
|
|
327342
|
-
logs2.push(` Error persisting page: ${
|
|
327789
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327790
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327343
327791
|
toolResults.push({
|
|
327344
327792
|
role: "tool",
|
|
327345
327793
|
tool_call_id: toolCall.id,
|
|
327346
|
-
content: `ERROR persisting page: ${
|
|
327794
|
+
content: `ERROR persisting page: ${errMsg3}`
|
|
327347
327795
|
});
|
|
327348
327796
|
}
|
|
327349
327797
|
continue;
|
|
@@ -327378,12 +327826,12 @@ ${currentSummary}`;
|
|
|
327378
327826
|
content: JSON.stringify({ acknowledged: true })
|
|
327379
327827
|
});
|
|
327380
327828
|
} catch (err) {
|
|
327381
|
-
const
|
|
327382
|
-
logs2.push(` Error persisting transition: ${
|
|
327829
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327830
|
+
logs2.push(` Error persisting transition: ${errMsg3}`);
|
|
327383
327831
|
toolResults.push({
|
|
327384
327832
|
role: "tool",
|
|
327385
327833
|
tool_call_id: toolCall.id,
|
|
327386
|
-
content: `ERROR persisting transition: ${
|
|
327834
|
+
content: `ERROR persisting transition: ${errMsg3}`
|
|
327387
327835
|
});
|
|
327388
327836
|
}
|
|
327389
327837
|
continue;
|
|
@@ -328079,15 +328527,15 @@ Exploration complete: ${summary}`);
|
|
|
328079
328527
|
toolResultContent = (0, snapshot_context_compactor_js_1.formatMCPToolResult)(toolResultContent, MAX_TOOL_RESULT_CHARS);
|
|
328080
328528
|
logs2.push(` OK: ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
328081
328529
|
} catch (err) {
|
|
328082
|
-
const
|
|
328083
|
-
toolResultContent = `ERROR: ${
|
|
328084
|
-
logs2.push(` Error: ${
|
|
328530
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
328531
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
328532
|
+
logs2.push(` Error: ${errMsg3}`);
|
|
328085
328533
|
if (useV2) {
|
|
328086
328534
|
(0, exploration_state_js_1.reduceEvent)(state2, {
|
|
328087
328535
|
type: "ACTION_FAILED",
|
|
328088
328536
|
route: state2.currentRoute,
|
|
328089
328537
|
action: `${toolName}(${JSON.stringify(toolArgs).slice(0, 100)})`,
|
|
328090
|
-
error:
|
|
328538
|
+
error: errMsg3
|
|
328091
328539
|
});
|
|
328092
328540
|
}
|
|
328093
328541
|
}
|
|
@@ -330936,10 +331384,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
330936
331384
|
onLog: options.onLog,
|
|
330937
331385
|
onProvenCommands: recordProvenCommands
|
|
330938
331386
|
});
|
|
330939
|
-
const
|
|
331387
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
330940
331388
|
const fixPrompt = [
|
|
330941
331389
|
"Your previous attempt did not produce a valid result file.",
|
|
330942
|
-
`Error: ${
|
|
331390
|
+
`Error: ${errMsg3}`,
|
|
330943
331391
|
`Re-read BRIEF.md and write a valid SurveyorOutput JSON to ${resultFilePath}.`,
|
|
330944
331392
|
"Single JSON object. No prose. Then exit."
|
|
330945
331393
|
].join("\n");
|
|
@@ -333080,7 +333528,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333080
333528
|
log2(`First-pass discovery-result invalid or missing: ${firstError.message}`);
|
|
333081
333529
|
}
|
|
333082
333530
|
if (!parsedResult) {
|
|
333083
|
-
const
|
|
333531
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
333084
333532
|
if (planDebounceTimerRef.current) {
|
|
333085
333533
|
clearTimeout(planDebounceTimerRef.current);
|
|
333086
333534
|
planDebounceTimerRef.current = void 0;
|
|
@@ -333105,7 +333553,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333105
333553
|
""
|
|
333106
333554
|
].join("\n");
|
|
333107
333555
|
const fixPrompt = [
|
|
333108
|
-
`Your previous attempt did not produce a valid result file. Error: ${
|
|
333556
|
+
`Your previous attempt did not produce a valid result file. Error: ${errMsg3}`,
|
|
333109
333557
|
"Re-read BRIEF.md.",
|
|
333110
333558
|
phaseContext,
|
|
333111
333559
|
`Continue from where you left off \u2014 already verified tests are in ${testsDirPath}/`,
|
|
@@ -334734,10 +335182,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
334734
335182
|
onLog: parserLog,
|
|
334735
335183
|
onProvenCommands: recordProvenCommands
|
|
334736
335184
|
});
|
|
334737
|
-
const
|
|
335185
|
+
const errMsg3 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
|
|
334738
335186
|
const fixPrompt = [
|
|
334739
335187
|
"Your previous attempt did not leave a valid result file.",
|
|
334740
|
-
`Error: ${
|
|
335188
|
+
`Error: ${errMsg3}`,
|
|
334741
335189
|
`Writing a valid SurveyorOutput JSON object to ${resultFilePath} is your ONLY priority now.`,
|
|
334742
335190
|
"Procedure: navigate to at most 6-8 of the main pages, then IMMEDIATELY use",
|
|
334743
335191
|
`the Write tool to save the SurveyorOutput JSON to ${resultFilePath}. Do this`,
|
|
@@ -338618,8 +339066,8 @@ var require_resource_governor = __commonJS({
|
|
|
338618
339066
|
function isDiscoveryLike(mode) {
|
|
338619
339067
|
return mode === "discovery";
|
|
338620
339068
|
}
|
|
338621
|
-
function evaluateRunAdmission2(options, request) {
|
|
338622
|
-
const snapshot = getResourceSnapshot2();
|
|
339069
|
+
function evaluateRunAdmission2(options, request, snapshotOverride) {
|
|
339070
|
+
const snapshot = snapshotOverride ?? getResourceSnapshot2();
|
|
338623
339071
|
const candidateWeight = modeWeight(request.mode, options.weights);
|
|
338624
339072
|
const currentUnits = activeUnits(request.activeByType, options.weights);
|
|
338625
339073
|
const memory = pressureFactor(snapshot.memoryPct, options.memorySoftPct, options.memoryHardPct);
|
|
@@ -341117,8 +341565,8 @@ var require_adm_zip = __commonJS({
|
|
|
341117
341565
|
return null;
|
|
341118
341566
|
}
|
|
341119
341567
|
function fixPath(zipPath) {
|
|
341120
|
-
const { join:
|
|
341121
|
-
return
|
|
341568
|
+
const { join: join8, normalize, sep } = pth.posix;
|
|
341569
|
+
return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
|
|
341122
341570
|
}
|
|
341123
341571
|
function filenameFilter(filterfn) {
|
|
341124
341572
|
if (filterfn instanceof RegExp) {
|
|
@@ -343953,11 +344401,11 @@ var require_winchars = __commonJS({
|
|
|
343953
344401
|
var require_strip_absolute_path = __commonJS({
|
|
343954
344402
|
"../../node_modules/tar/lib/strip-absolute-path.js"(exports2, module2) {
|
|
343955
344403
|
"use strict";
|
|
343956
|
-
var { isAbsolute, parse } = require("path").win32;
|
|
344404
|
+
var { isAbsolute: isAbsolute2, parse } = require("path").win32;
|
|
343957
344405
|
module2.exports = (path) => {
|
|
343958
344406
|
let r = "";
|
|
343959
344407
|
let parsed = parse(path);
|
|
343960
|
-
while (
|
|
344408
|
+
while (isAbsolute2(path) || parsed.root) {
|
|
343961
344409
|
const root = path.charAt(0) === "/" && path.slice(0, 4) !== "//?/" ? "/" : parsed.root;
|
|
343962
344410
|
path = path.slice(root.length);
|
|
343963
344411
|
r += root;
|
|
@@ -346469,7 +346917,7 @@ var require_update = __commonJS({
|
|
|
346469
346917
|
var require_opts_arg = __commonJS({
|
|
346470
346918
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports2, module2) {
|
|
346471
346919
|
"use strict";
|
|
346472
|
-
var { promisify:
|
|
346920
|
+
var { promisify: promisify4 } = require("util");
|
|
346473
346921
|
var fs = require("fs");
|
|
346474
346922
|
var optsArg = (opts) => {
|
|
346475
346923
|
if (!opts)
|
|
@@ -346483,9 +346931,9 @@ var require_opts_arg = __commonJS({
|
|
|
346483
346931
|
else
|
|
346484
346932
|
throw new TypeError("invalid options argument");
|
|
346485
346933
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
346486
|
-
opts.mkdirAsync =
|
|
346934
|
+
opts.mkdirAsync = promisify4(opts.mkdir);
|
|
346487
346935
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
346488
|
-
opts.statAsync =
|
|
346936
|
+
opts.statAsync = promisify4(opts.stat);
|
|
346489
346937
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
346490
346938
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
346491
346939
|
return opts;
|
|
@@ -346738,10 +347186,10 @@ var require_chownr = __commonJS({
|
|
|
346738
347186
|
}
|
|
346739
347187
|
} : (path2, uid, gid) => lchownSync(path2, uid, gid);
|
|
346740
347188
|
var nodeVersion = process.version;
|
|
346741
|
-
var
|
|
347189
|
+
var readdir3 = (path2, options, cb) => fs.readdir(path2, options, cb);
|
|
346742
347190
|
var readdirSync = (path2, options) => fs.readdirSync(path2, options);
|
|
346743
347191
|
if (/^v4\./.test(nodeVersion))
|
|
346744
|
-
|
|
347192
|
+
readdir3 = (path2, options, cb) => fs.readdir(path2, cb);
|
|
346745
347193
|
var chown = (cpath, uid, gid, cb) => {
|
|
346746
347194
|
fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, (er) => {
|
|
346747
347195
|
cb(er && er.code !== "ENOENT" ? er : null);
|
|
@@ -346768,7 +347216,7 @@ var require_chownr = __commonJS({
|
|
|
346768
347216
|
}
|
|
346769
347217
|
};
|
|
346770
347218
|
var chownr = (p, uid, gid, cb) => {
|
|
346771
|
-
|
|
347219
|
+
readdir3(p, { withFileTypes: true }, (er, children) => {
|
|
346772
347220
|
if (er) {
|
|
346773
347221
|
if (er.code === "ENOENT")
|
|
346774
347222
|
return cb();
|
|
@@ -347042,7 +347490,7 @@ var require_path_reservations = __commonJS({
|
|
|
347042
347490
|
var assert = require("assert");
|
|
347043
347491
|
var normalize = require_normalize_unicode();
|
|
347044
347492
|
var stripSlashes = require_strip_trailing_slashes();
|
|
347045
|
-
var { join:
|
|
347493
|
+
var { join: join8 } = require("path");
|
|
347046
347494
|
var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
|
|
347047
347495
|
var isWindows = platform3 === "win32";
|
|
347048
347496
|
module2.exports = () => {
|
|
@@ -347051,7 +347499,7 @@ var require_path_reservations = __commonJS({
|
|
|
347051
347499
|
const getDirs = (path) => {
|
|
347052
347500
|
const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
|
|
347053
347501
|
if (set.length) {
|
|
347054
|
-
path2 =
|
|
347502
|
+
path2 = join8(set[set.length - 1], path2);
|
|
347055
347503
|
}
|
|
347056
347504
|
set.push(path2 || "/");
|
|
347057
347505
|
return set;
|
|
@@ -347119,7 +347567,7 @@ var require_path_reservations = __commonJS({
|
|
|
347119
347567
|
};
|
|
347120
347568
|
const reserve = (paths, fn) => {
|
|
347121
347569
|
paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
|
|
347122
|
-
return stripSlashes(
|
|
347570
|
+
return stripSlashes(join8(normalize(p))).toLowerCase();
|
|
347123
347571
|
});
|
|
347124
347572
|
const dirs = new Set(
|
|
347125
347573
|
paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
|
|
@@ -347174,7 +347622,7 @@ var require_unpack = __commonJS({
|
|
|
347174
347622
|
var fs = require("fs");
|
|
347175
347623
|
var fsm = require_fs_minipass();
|
|
347176
347624
|
var path = require("path");
|
|
347177
|
-
var
|
|
347625
|
+
var mkdir3 = require_mkdir();
|
|
347178
347626
|
var wc = require_winchars();
|
|
347179
347627
|
var pathReservations = require_path_reservations();
|
|
347180
347628
|
var stripAbsolutePath = require_strip_absolute_path();
|
|
@@ -347425,7 +347873,7 @@ var require_unpack = __commonJS({
|
|
|
347425
347873
|
}
|
|
347426
347874
|
}
|
|
347427
347875
|
[MKDIR](dir, mode, cb) {
|
|
347428
|
-
|
|
347876
|
+
mkdir3(normPath(dir), {
|
|
347429
347877
|
uid: this.uid,
|
|
347430
347878
|
gid: this.gid,
|
|
347431
347879
|
processUid: this.processUid,
|
|
@@ -347828,7 +348276,7 @@ var require_unpack = __commonJS({
|
|
|
347828
348276
|
}
|
|
347829
348277
|
[MKDIR](dir, mode) {
|
|
347830
348278
|
try {
|
|
347831
|
-
return
|
|
348279
|
+
return mkdir3.sync(normPath(dir), {
|
|
347832
348280
|
uid: this.uid,
|
|
347833
348281
|
gid: this.gid,
|
|
347834
348282
|
processUid: this.processUid,
|
|
@@ -351334,7 +351782,7 @@ var require_process_inventory = __commonJS({
|
|
|
351334
351782
|
var node_child_process_1 = require("child_process");
|
|
351335
351783
|
var node_os_1 = __importDefault(require("os"));
|
|
351336
351784
|
var node_util_1 = require("util");
|
|
351337
|
-
var
|
|
351785
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
351338
351786
|
var DEFAULT_LIMIT = 60;
|
|
351339
351787
|
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
351340
351788
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -351396,7 +351844,7 @@ var require_process_inventory = __commonJS({
|
|
|
351396
351844
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
351397
351845
|
timer.unref?.();
|
|
351398
351846
|
try {
|
|
351399
|
-
const { stdout } = await
|
|
351847
|
+
const { stdout } = await execFileAsync4(command, args, {
|
|
351400
351848
|
maxBuffer: 1024 * 1024,
|
|
351401
351849
|
signal: controller.signal
|
|
351402
351850
|
});
|
|
@@ -352542,7 +352990,7 @@ var require_package4 = __commonJS({
|
|
|
352542
352990
|
"package.json"(exports2, module2) {
|
|
352543
352991
|
module2.exports = {
|
|
352544
352992
|
name: "@validate.qa/runner",
|
|
352545
|
-
version: "1.0.
|
|
352993
|
+
version: "1.0.71",
|
|
352546
352994
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
352547
352995
|
bin: {
|
|
352548
352996
|
"validate-runner": "dist/cli.js"
|
|
@@ -352551,7 +352999,7 @@ var require_package4 = __commonJS({
|
|
|
352551
352999
|
build: "tsup",
|
|
352552
353000
|
dev: "tsx src/cli.ts",
|
|
352553
353001
|
test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
|
|
352554
|
-
"test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
353002
|
+
"test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/runner-watchdog.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
352555
353003
|
prepublishOnly: "npm run build"
|
|
352556
353004
|
},
|
|
352557
353005
|
files: [
|
|
@@ -352629,7 +353077,7 @@ function getConfigDir() {
|
|
|
352629
353077
|
}
|
|
352630
353078
|
|
|
352631
353079
|
// src/runner.ts
|
|
352632
|
-
var
|
|
353080
|
+
var import_path5 = require("path");
|
|
352633
353081
|
var import_os6 = require("os");
|
|
352634
353082
|
var import_node_crypto2 = require("crypto");
|
|
352635
353083
|
|
|
@@ -353224,6 +353672,69 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
353224
353672
|
};
|
|
353225
353673
|
}
|
|
353226
353674
|
|
|
353675
|
+
// src/services/mobile/ios-simulator-reset.ts
|
|
353676
|
+
var import_child_process2 = require("child_process");
|
|
353677
|
+
var import_util = require("util");
|
|
353678
|
+
var import_promises = require("fs/promises");
|
|
353679
|
+
var import_path3 = require("path");
|
|
353680
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
353681
|
+
var SIMCTL_TIMEOUT_MS = 15e3;
|
|
353682
|
+
var SIMULATOR_UDID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
353683
|
+
function isIosSimulatorUdid(udid) {
|
|
353684
|
+
return typeof udid === "string" && SIMULATOR_UDID_RE.test(udid.trim());
|
|
353685
|
+
}
|
|
353686
|
+
function errMsg(err) {
|
|
353687
|
+
return err instanceof Error ? err.message : String(err);
|
|
353688
|
+
}
|
|
353689
|
+
async function simctl(args) {
|
|
353690
|
+
const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
|
|
353691
|
+
timeout: SIMCTL_TIMEOUT_MS,
|
|
353692
|
+
encoding: "utf-8"
|
|
353693
|
+
});
|
|
353694
|
+
return stdout.trim();
|
|
353695
|
+
}
|
|
353696
|
+
async function clearIosSimulatorAppData(udid, bundleId, env = process.env) {
|
|
353697
|
+
if (!isIosSimulatorUdid(udid)) {
|
|
353698
|
+
return { ok: false, detail: `'${udid}' is not a simulator UDID \u2014 physical iOS devices cannot be cleared` };
|
|
353699
|
+
}
|
|
353700
|
+
let container;
|
|
353701
|
+
try {
|
|
353702
|
+
container = await simctl(["get_app_container", udid, bundleId, "data"]);
|
|
353703
|
+
} catch (err) {
|
|
353704
|
+
return { ok: false, detail: `simctl get_app_container failed (app not installed?): ${errMsg(err)}` };
|
|
353705
|
+
}
|
|
353706
|
+
if (!(0, import_path3.isAbsolute)(container) || !container.includes("/Containers/Data/Application/")) {
|
|
353707
|
+
return { ok: false, detail: `unexpected app container path: ${container}` };
|
|
353708
|
+
}
|
|
353709
|
+
try {
|
|
353710
|
+
await simctl(["spawn", udid, "defaults", "delete", bundleId]);
|
|
353711
|
+
} catch {
|
|
353712
|
+
}
|
|
353713
|
+
try {
|
|
353714
|
+
const entries = await (0, import_promises.readdir)(container);
|
|
353715
|
+
for (const entry of entries) {
|
|
353716
|
+
await (0, import_promises.rm)((0, import_path3.join)(container, entry), { recursive: true, force: true });
|
|
353717
|
+
}
|
|
353718
|
+
for (const dir of ["Documents", "Library/Caches", "Library/Preferences", "tmp"]) {
|
|
353719
|
+
await (0, import_promises.mkdir)((0, import_path3.join)(container, dir), { recursive: true });
|
|
353720
|
+
}
|
|
353721
|
+
} catch (err) {
|
|
353722
|
+
return { ok: false, detail: `app container wipe failed: ${errMsg(err)}` };
|
|
353723
|
+
}
|
|
353724
|
+
const keychainPolicy = (env.VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET ?? "").trim().toLowerCase();
|
|
353725
|
+
if (keychainPolicy !== "never") {
|
|
353726
|
+
try {
|
|
353727
|
+
await simctl(["keychain", udid, "reset"]);
|
|
353728
|
+
} catch (err) {
|
|
353729
|
+
return {
|
|
353730
|
+
ok: false,
|
|
353731
|
+
detail: `simulator keychain reset failed (${errMsg(err)}) \u2014 app data was wiped but keychain items (e.g. auth tokens) may survive. Set VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET=never to skip the keychain step.`
|
|
353732
|
+
};
|
|
353733
|
+
}
|
|
353734
|
+
}
|
|
353735
|
+
return { ok: true };
|
|
353736
|
+
}
|
|
353737
|
+
|
|
353227
353738
|
// src/services/mobile/appium-actions.ts
|
|
353228
353739
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
353229
353740
|
var APPIUM_WEBVIEW_CONTEXT_TIMEOUT_MS = 12e4;
|
|
@@ -353406,9 +353917,12 @@ async function tapAt(sessionPath, bounds) {
|
|
|
353406
353917
|
}
|
|
353407
353918
|
}
|
|
353408
353919
|
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
353409
|
-
if (platform3 !== "ANDROID") return false;
|
|
353410
353920
|
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
353411
353921
|
if (policy === "never") return false;
|
|
353922
|
+
if (platform3 === "IOS") {
|
|
353923
|
+
return isIosSimulatorUdid(deviceId);
|
|
353924
|
+
}
|
|
353925
|
+
if (platform3 !== "ANDROID") return false;
|
|
353412
353926
|
if (policy === "always") return true;
|
|
353413
353927
|
return !!deviceId && deviceId.startsWith("emulator-");
|
|
353414
353928
|
}
|
|
@@ -354287,6 +354801,31 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354287
354801
|
});
|
|
354288
354802
|
}
|
|
354289
354803
|
|
|
354804
|
+
// src/services/device-quarantine.ts
|
|
354805
|
+
var quarantinedDevices = /* @__PURE__ */ new Set();
|
|
354806
|
+
var DeviceQuarantinedError = class extends Error {
|
|
354807
|
+
constructor(deviceId) {
|
|
354808
|
+
super(
|
|
354809
|
+
`Device ${deviceId} is quarantined: a previous run on it was force-released by the watchdog and has not settled yet. Refusing to open a new Appium session until it does.`
|
|
354810
|
+
);
|
|
354811
|
+
this.name = "DeviceQuarantinedError";
|
|
354812
|
+
}
|
|
354813
|
+
};
|
|
354814
|
+
function quarantineDevice(deviceId) {
|
|
354815
|
+
quarantinedDevices.add(deviceId);
|
|
354816
|
+
}
|
|
354817
|
+
function unquarantineDevice(deviceId) {
|
|
354818
|
+
quarantinedDevices.delete(deviceId);
|
|
354819
|
+
}
|
|
354820
|
+
function isDeviceQuarantined(deviceId) {
|
|
354821
|
+
return typeof deviceId === "string" && quarantinedDevices.has(deviceId);
|
|
354822
|
+
}
|
|
354823
|
+
function assertDeviceNotQuarantined(deviceId) {
|
|
354824
|
+
if (isDeviceQuarantined(deviceId)) {
|
|
354825
|
+
throw new DeviceQuarantinedError(deviceId);
|
|
354826
|
+
}
|
|
354827
|
+
}
|
|
354828
|
+
|
|
354290
354829
|
// src/services/appium-executor.ts
|
|
354291
354830
|
var import_node_child_process2 = require("child_process");
|
|
354292
354831
|
var import_node_util2 = require("util");
|
|
@@ -354295,11 +354834,11 @@ var import_node_path2 = require("path");
|
|
|
354295
354834
|
var import_runner_core8 = __toESM(require_dist2());
|
|
354296
354835
|
|
|
354297
354836
|
// src/services/device-discovery.ts
|
|
354298
|
-
var
|
|
354837
|
+
var import_child_process3 = require("child_process");
|
|
354299
354838
|
var import_os3 = require("os");
|
|
354300
354839
|
function runCommand(command, args, timeout = 1e4) {
|
|
354301
354840
|
try {
|
|
354302
|
-
return (0,
|
|
354841
|
+
return (0, import_child_process3.execFileSync)(command, args, {
|
|
354303
354842
|
encoding: "utf-8",
|
|
354304
354843
|
timeout,
|
|
354305
354844
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -354599,9 +355138,6 @@ function countReadyDevicesByKind(platform3, snapshot = getCachedDeviceSnapshot()
|
|
|
354599
355138
|
function selectFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(), requirements = {}) {
|
|
354600
355139
|
const ready = snapshot.filter((d) => d.platform === platform3 && d.isAvailable && d.state === "BOOTED");
|
|
354601
355140
|
if (ready.length === 0) return { kind: "busy", reason: `no booted ${platform3} device` };
|
|
354602
|
-
if (requirements.resetPolicy === "CLEAR_APP_DATA" && platform3 !== "ANDROID") {
|
|
354603
|
-
return { kind: "unsatisfiable", reason: "CLEAR_APP_DATA is unsupported on iOS" };
|
|
354604
|
-
}
|
|
354605
355141
|
let candidates = requirements.resetPolicy === "CLEAR_APP_DATA" ? ready.filter((d) => !d.isPhysical) : [...ready];
|
|
354606
355142
|
const appId = requirements.appId?.trim();
|
|
354607
355143
|
if (appId) {
|
|
@@ -354651,13 +355187,13 @@ function pickFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(),
|
|
|
354651
355187
|
// src/services/mobile/mobile-network-capture.ts
|
|
354652
355188
|
var import_node_child_process = require("child_process");
|
|
354653
355189
|
var import_node_crypto = require("crypto");
|
|
354654
|
-
var
|
|
355190
|
+
var import_promises2 = require("fs/promises");
|
|
354655
355191
|
var import_node_os = require("os");
|
|
354656
355192
|
var import_node_path = require("path");
|
|
354657
355193
|
var import_node_util = require("util");
|
|
354658
355194
|
var import_mockttp = require("mockttp");
|
|
354659
355195
|
var import_runner_core7 = __toESM(require_dist2());
|
|
354660
|
-
var
|
|
355196
|
+
var execFileAsync2 = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
354661
355197
|
var MAX_CALLS = 1e3;
|
|
354662
355198
|
var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
|
|
354663
355199
|
var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
@@ -354845,7 +355381,7 @@ var MitmProxy = class {
|
|
|
354845
355381
|
try {
|
|
354846
355382
|
await server3.stop();
|
|
354847
355383
|
} catch (err) {
|
|
354848
|
-
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${
|
|
355384
|
+
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg2(err)}`);
|
|
354849
355385
|
}
|
|
354850
355386
|
this.onLog?.(
|
|
354851
355387
|
`[mobile-net] proxy closed: ${this.calls.length} calls captured` + (this.dropped > 0 ? ` (${this.dropped} dropped over cap)` : "")
|
|
@@ -354870,7 +355406,7 @@ var MitmProxy = class {
|
|
|
354870
355406
|
isHttps
|
|
354871
355407
|
});
|
|
354872
355408
|
} catch (err) {
|
|
354873
|
-
this.onLog?.(`[mobile-net] request capture error (ignored): ${
|
|
355409
|
+
this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg2(err)}`);
|
|
354874
355410
|
}
|
|
354875
355411
|
}
|
|
354876
355412
|
// ── response → finalize the matched request ───────────────────────────────
|
|
@@ -354913,7 +355449,7 @@ var MitmProxy = class {
|
|
|
354913
355449
|
intercepted: true
|
|
354914
355450
|
});
|
|
354915
355451
|
} catch (err) {
|
|
354916
|
-
this.onLog?.(`[mobile-net] response capture error (ignored): ${
|
|
355452
|
+
this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg2(err)}`);
|
|
354917
355453
|
}
|
|
354918
355454
|
}
|
|
354919
355455
|
// ── tls-client-error → record WHY interception failed ─────────────────────
|
|
@@ -354991,16 +355527,16 @@ async function generateCaCert(dir, onLog) {
|
|
|
354991
355527
|
// rather than on a short expiry.
|
|
354992
355528
|
});
|
|
354993
355529
|
const caCertPath = (0, import_node_path.join)(dir, "validateqa-mobile-ca.pem");
|
|
354994
|
-
await (0,
|
|
355530
|
+
await (0, import_promises2.writeFile)(caCertPath, cert, "utf-8");
|
|
354995
355531
|
return { caCertPath, caKeyPem: key, caCertPem: cert };
|
|
354996
355532
|
} catch (err) {
|
|
354997
|
-
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${
|
|
355533
|
+
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg2(err)}`);
|
|
354998
355534
|
return null;
|
|
354999
355535
|
}
|
|
355000
355536
|
}
|
|
355001
355537
|
async function androidReadProxy(deviceId) {
|
|
355002
355538
|
try {
|
|
355003
|
-
const { stdout } = await
|
|
355539
|
+
const { stdout } = await execFileAsync2(
|
|
355004
355540
|
"adb",
|
|
355005
355541
|
["-s", deviceId, "shell", "settings", "get", "global", "http_proxy"],
|
|
355006
355542
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
@@ -355013,33 +355549,33 @@ async function androidReadProxy(deviceId) {
|
|
|
355013
355549
|
}
|
|
355014
355550
|
async function androidSetProxy(deviceId, hostPort, onLog) {
|
|
355015
355551
|
try {
|
|
355016
|
-
await
|
|
355552
|
+
await execFileAsync2(
|
|
355017
355553
|
"adb",
|
|
355018
355554
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", hostPort],
|
|
355019
355555
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355020
355556
|
);
|
|
355021
355557
|
return true;
|
|
355022
355558
|
} catch (err) {
|
|
355023
|
-
onLog?.(`[mobile-net] adb set http_proxy failed: ${
|
|
355559
|
+
onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg2(err)}`);
|
|
355024
355560
|
return false;
|
|
355025
355561
|
}
|
|
355026
355562
|
}
|
|
355027
355563
|
async function androidClearProxy(deviceId, originalProxy, onLog) {
|
|
355028
355564
|
const restoreValue = originalProxy && originalProxy.length > 0 ? originalProxy : ":0";
|
|
355029
355565
|
try {
|
|
355030
|
-
await
|
|
355566
|
+
await execFileAsync2(
|
|
355031
355567
|
"adb",
|
|
355032
355568
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", restoreValue],
|
|
355033
355569
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355034
355570
|
);
|
|
355035
355571
|
} catch (err) {
|
|
355036
|
-
onLog?.(`[mobile-net] adb restore http_proxy failed: ${
|
|
355572
|
+
onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg2(err)}`);
|
|
355037
355573
|
}
|
|
355038
355574
|
}
|
|
355039
355575
|
async function androidInstallCa(deviceId, caCertPath) {
|
|
355040
355576
|
const devicePath = "/sdcard/validateqa-mobile-ca.pem";
|
|
355041
355577
|
try {
|
|
355042
|
-
await
|
|
355578
|
+
await execFileAsync2("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
|
|
355043
355579
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355044
355580
|
});
|
|
355045
355581
|
return {
|
|
@@ -355047,23 +355583,23 @@ async function androidInstallCa(deviceId, caCertPath) {
|
|
|
355047
355583
|
note: `CA copied from ${caCertPath} to ${devicePath} but NOT auto-trusted: Android 7+ apps must opt into user CAs (network_security_config), and the system store requires a rooted/writable image. HTTPS bodies decrypt only for apps that trust user CAs; pinned apps never decrypt.`
|
|
355048
355584
|
};
|
|
355049
355585
|
} catch (err) {
|
|
355050
|
-
return { installed: false, note: `CA push failed: ${
|
|
355586
|
+
return { installed: false, note: `CA push failed: ${errMsg2(err)}` };
|
|
355051
355587
|
}
|
|
355052
355588
|
}
|
|
355053
355589
|
async function androidRemoveCa(deviceId, onLog) {
|
|
355054
355590
|
try {
|
|
355055
|
-
await
|
|
355591
|
+
await execFileAsync2(
|
|
355056
355592
|
"adb",
|
|
355057
355593
|
["-s", deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"],
|
|
355058
355594
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355059
355595
|
);
|
|
355060
355596
|
} catch (err) {
|
|
355061
|
-
onLog?.(`[mobile-net] adb remove CA failed: ${
|
|
355597
|
+
onLog?.(`[mobile-net] adb remove CA failed: ${errMsg2(err)}`);
|
|
355062
355598
|
}
|
|
355063
355599
|
}
|
|
355064
355600
|
async function iosSimTrustCa(deviceId, caCertPath) {
|
|
355065
355601
|
try {
|
|
355066
|
-
await
|
|
355602
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
|
|
355067
355603
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355068
355604
|
});
|
|
355069
355605
|
return {
|
|
@@ -355071,18 +355607,18 @@ async function iosSimTrustCa(deviceId, caCertPath) {
|
|
|
355071
355607
|
note: "CA added to simulator keychain (add-root-cert). NOTE: the simulator has no per-app HTTP proxy setting controllable via simctl \u2014 it shares the host network, so only traffic that actually routes through the proxy is intercepted; pinned apps still never decrypt."
|
|
355072
355608
|
};
|
|
355073
355609
|
} catch (err) {
|
|
355074
|
-
return { trusted: false, note: `simctl add-root-cert failed: ${
|
|
355610
|
+
return { trusted: false, note: `simctl add-root-cert failed: ${errMsg2(err)}` };
|
|
355075
355611
|
}
|
|
355076
355612
|
}
|
|
355077
355613
|
async function iosSimRemoveCa(deviceId, onLog) {
|
|
355078
355614
|
if (process.env.VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET === "1") {
|
|
355079
355615
|
try {
|
|
355080
|
-
await
|
|
355616
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "reset"], {
|
|
355081
355617
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355082
355618
|
});
|
|
355083
355619
|
onLog?.(`[mobile-net] simctl keychain reset run for ${deviceId} (opt-in; cleared ALL sim certs).`);
|
|
355084
355620
|
} catch (err) {
|
|
355085
|
-
onLog?.(`[mobile-net] simctl keychain reset failed: ${
|
|
355621
|
+
onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg2(err)}`);
|
|
355086
355622
|
}
|
|
355087
355623
|
return;
|
|
355088
355624
|
}
|
|
@@ -355090,7 +355626,7 @@ async function iosSimRemoveCa(deviceId, onLog) {
|
|
|
355090
355626
|
`[mobile-net] NOTE: the validate.qa capture CA remains TRUSTED in simulator ${deviceId}. simctl has no scoped remove (only a full keychain reset that wipes ALL certs), so we leave the single CA in place. Use a throwaway simulator, or set VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET=1 to reset the whole sim keychain on teardown.`
|
|
355091
355627
|
);
|
|
355092
355628
|
}
|
|
355093
|
-
function
|
|
355629
|
+
function errMsg2(err) {
|
|
355094
355630
|
return err instanceof Error ? err.message : String(err);
|
|
355095
355631
|
}
|
|
355096
355632
|
function isProcessAlive(pid) {
|
|
@@ -355181,7 +355717,7 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355181
355717
|
let service;
|
|
355182
355718
|
let prior;
|
|
355183
355719
|
try {
|
|
355184
|
-
const { stdout: order } = await
|
|
355720
|
+
const { stdout: order } = await execFileAsync2("networksetup", ["-listnetworkserviceorder"], {
|
|
355185
355721
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355186
355722
|
});
|
|
355187
355723
|
const svc = parsePrimaryNetworkService(order);
|
|
@@ -355190,26 +355726,26 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355190
355726
|
}
|
|
355191
355727
|
service = svc;
|
|
355192
355728
|
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
355193
|
-
|
|
355194
|
-
|
|
355729
|
+
execFileAsync2("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
355730
|
+
execFileAsync2("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
355195
355731
|
]);
|
|
355196
355732
|
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
355197
355733
|
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
355198
|
-
await
|
|
355734
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355199
355735
|
}
|
|
355200
355736
|
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
355201
355737
|
return { configured: true, service, prior };
|
|
355202
355738
|
} catch (err) {
|
|
355203
355739
|
if (service && prior) {
|
|
355204
|
-
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${
|
|
355740
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg2(err)}`);
|
|
355205
355741
|
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
355206
355742
|
try {
|
|
355207
|
-
await
|
|
355743
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355208
355744
|
} catch {
|
|
355209
355745
|
}
|
|
355210
355746
|
}
|
|
355211
355747
|
}
|
|
355212
|
-
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${
|
|
355748
|
+
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg2(err)}` };
|
|
355213
355749
|
}
|
|
355214
355750
|
}
|
|
355215
355751
|
async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
@@ -355219,9 +355755,9 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
|
355219
355755
|
};
|
|
355220
355756
|
for (const args of buildHostProxyRestoreCommands(service, restore)) {
|
|
355221
355757
|
try {
|
|
355222
|
-
await
|
|
355758
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355223
355759
|
} catch (err) {
|
|
355224
|
-
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${
|
|
355760
|
+
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg2(err)}`);
|
|
355225
355761
|
}
|
|
355226
355762
|
}
|
|
355227
355763
|
onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
|
|
@@ -355233,14 +355769,14 @@ function markerPath(sessionKey) {
|
|
|
355233
355769
|
}
|
|
355234
355770
|
async function writePendingCleanup(sessionKey, marker) {
|
|
355235
355771
|
try {
|
|
355236
|
-
await (0,
|
|
355237
|
-
await (0,
|
|
355772
|
+
await (0, import_promises2.mkdir)(PENDING_CLEANUP_DIR, { recursive: true });
|
|
355773
|
+
await (0, import_promises2.writeFile)(markerPath(sessionKey), JSON.stringify(marker), "utf-8");
|
|
355238
355774
|
} catch {
|
|
355239
355775
|
}
|
|
355240
355776
|
}
|
|
355241
355777
|
async function removePendingCleanup(sessionKey) {
|
|
355242
355778
|
try {
|
|
355243
|
-
await (0,
|
|
355779
|
+
await (0, import_promises2.unlink)(markerPath(sessionKey));
|
|
355244
355780
|
} catch {
|
|
355245
355781
|
}
|
|
355246
355782
|
}
|
|
@@ -355273,7 +355809,7 @@ function restoreDeviceSync(marker, onLog) {
|
|
|
355273
355809
|
async function reconcilePendingCleanups(onLog) {
|
|
355274
355810
|
let files;
|
|
355275
355811
|
try {
|
|
355276
|
-
files = await (0,
|
|
355812
|
+
files = await (0, import_promises2.readdir)(PENDING_CLEANUP_DIR);
|
|
355277
355813
|
} catch {
|
|
355278
355814
|
return;
|
|
355279
355815
|
}
|
|
@@ -355283,7 +355819,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
355283
355819
|
if (activeCleanups.has(sessionKey)) continue;
|
|
355284
355820
|
const full = (0, import_node_path.join)(PENDING_CLEANUP_DIR, file);
|
|
355285
355821
|
try {
|
|
355286
|
-
const raw = await (0,
|
|
355822
|
+
const raw = await (0, import_promises2.readFile)(full, "utf-8");
|
|
355287
355823
|
const marker = JSON.parse(raw);
|
|
355288
355824
|
if (marker && typeof marker.deviceId === "string" && marker.platform) {
|
|
355289
355825
|
if (!shouldReconcilePendingCleanup(marker)) {
|
|
@@ -355296,7 +355832,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
355296
355832
|
} catch {
|
|
355297
355833
|
}
|
|
355298
355834
|
try {
|
|
355299
|
-
await (0,
|
|
355835
|
+
await (0, import_promises2.unlink)(full);
|
|
355300
355836
|
} catch {
|
|
355301
355837
|
}
|
|
355302
355838
|
}
|
|
@@ -355334,7 +355870,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355334
355870
|
let caKeyPem = null;
|
|
355335
355871
|
let caCertPem = null;
|
|
355336
355872
|
try {
|
|
355337
|
-
tmpDir = await (0,
|
|
355873
|
+
tmpDir = await (0, import_promises2.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "validateqa-mobile-ca-"));
|
|
355338
355874
|
const ca = await generateCaCert(tmpDir, onLog);
|
|
355339
355875
|
if (ca) {
|
|
355340
355876
|
caCertPath = ca.caCertPath;
|
|
@@ -355342,7 +355878,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355342
355878
|
caCertPem = ca.caCertPem;
|
|
355343
355879
|
}
|
|
355344
355880
|
} catch (err) {
|
|
355345
|
-
onLog?.(`[mobile-net] CA setup degraded: ${
|
|
355881
|
+
onLog?.(`[mobile-net] CA setup degraded: ${errMsg2(err)}`);
|
|
355346
355882
|
}
|
|
355347
355883
|
if (!caKeyPem || !caCertPem) {
|
|
355348
355884
|
try {
|
|
@@ -355350,9 +355886,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355350
355886
|
caKeyPem = fallback.key;
|
|
355351
355887
|
caCertPem = fallback.cert;
|
|
355352
355888
|
} catch (err) {
|
|
355353
|
-
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${
|
|
355889
|
+
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg2(err)}`);
|
|
355354
355890
|
if (tmpDir) {
|
|
355355
|
-
await (0,
|
|
355891
|
+
await (0, import_promises2.rm)(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
355356
355892
|
}
|
|
355357
355893
|
throw err instanceof Error ? err : new Error(String(err));
|
|
355358
355894
|
}
|
|
@@ -355407,7 +355943,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355407
355943
|
}
|
|
355408
355944
|
}
|
|
355409
355945
|
} catch (err) {
|
|
355410
|
-
wiringReason = `device wiring degraded: ${
|
|
355946
|
+
wiringReason = `device wiring degraded: ${errMsg2(err)}`;
|
|
355411
355947
|
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
355412
355948
|
}
|
|
355413
355949
|
if (proxySet || caInstalledOnDevice || hostProxySet) {
|
|
@@ -355496,9 +356032,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355496
356032
|
await proxy.close();
|
|
355497
356033
|
if (capturedTmpDir) {
|
|
355498
356034
|
try {
|
|
355499
|
-
await (0,
|
|
356035
|
+
await (0, import_promises2.rm)(capturedTmpDir, { recursive: true, force: true });
|
|
355500
356036
|
} catch (err) {
|
|
355501
|
-
onLog?.(`[mobile-net] temp CA cleanup failed: ${
|
|
356037
|
+
onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg2(err)}`);
|
|
355502
356038
|
}
|
|
355503
356039
|
}
|
|
355504
356040
|
const apiCount = finalCalls.filter((c) => c.isApi).length;
|
|
@@ -355531,11 +356067,11 @@ var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
|
355531
356067
|
})();
|
|
355532
356068
|
var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
355533
356069
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
355534
|
-
var WD_SESSION_CREATE_TIMEOUT_MS =
|
|
356070
|
+
var WD_SESSION_CREATE_TIMEOUT_MS = Number(process.env.VALIDATEQA_WD_SESSION_CREATE_TIMEOUT_MS) || 3e5;
|
|
355535
356071
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
355536
356072
|
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
355537
356073
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
355538
|
-
var
|
|
356074
|
+
var execFileAsync3 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
355539
356075
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
355540
356076
|
var WebDriverTransportError = class extends Error {
|
|
355541
356077
|
isTransport = true;
|
|
@@ -355547,6 +356083,13 @@ var WebDriverTransportError = class extends Error {
|
|
|
355547
356083
|
function isTransportError(err) {
|
|
355548
356084
|
return err instanceof WebDriverTransportError || typeof err === "object" && err !== null && err.isTransport === true;
|
|
355549
356085
|
}
|
|
356086
|
+
var MobileAmbiguousMatchError = class extends Error {
|
|
356087
|
+
isAmbiguousMatch = true;
|
|
356088
|
+
constructor(message) {
|
|
356089
|
+
super(message);
|
|
356090
|
+
this.name = "MobileAmbiguousMatchError";
|
|
356091
|
+
}
|
|
356092
|
+
};
|
|
355550
356093
|
function crossPlatformRunReadinessError(steps, authoringPlatform, runPlatform) {
|
|
355551
356094
|
if (!authoringPlatform || authoringPlatform === runPlatform) return null;
|
|
355552
356095
|
const missing = stepsMissingPlatformLocators(steps, runPlatform);
|
|
@@ -355692,6 +356235,7 @@ function androidSystemPortForwardRemovalArgs(target, error2, env = process.env)
|
|
|
355692
356235
|
return ["-s", deviceId, "forward", "--remove", `tcp:${systemPort}`];
|
|
355693
356236
|
}
|
|
355694
356237
|
async function createSession(target) {
|
|
356238
|
+
assertDeviceNotQuarantined(target.deviceId);
|
|
355695
356239
|
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform) : {};
|
|
355696
356240
|
const parallelCaps = mobileParallelSessionCapabilities(
|
|
355697
356241
|
target.platform,
|
|
@@ -355713,6 +356257,13 @@ async function createSession(target) {
|
|
|
355713
356257
|
// whole survey. The runner owns session teardown explicitly, so a long
|
|
355714
356258
|
// idle window is safe. Env-overridable.
|
|
355715
356259
|
"appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
|
|
356260
|
+
// Xcode 27+ relocates Simulator.app out of <DEVELOPER_DIR>/Applications, so
|
|
356261
|
+
// XCUITest's default "open Simulator UI" step fails ("Simulator.app does not
|
|
356262
|
+
// exist") on this toolchain. The simulator is already booted and driven
|
|
356263
|
+
// headlessly via CoreSimulator, so tell XCUITest to skip opening the GUI.
|
|
356264
|
+
// Scoped to simulator UDIDs (a physical iOS device has no such step);
|
|
356265
|
+
// override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
|
|
356266
|
+
...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
355716
356267
|
...parallelCaps,
|
|
355717
356268
|
...realCaps
|
|
355718
356269
|
} : {
|
|
@@ -355752,7 +356303,7 @@ async function removeStaleAndroidSystemPortForward(target, error2) {
|
|
|
355752
356303
|
const args = androidSystemPortForwardRemovalArgs(target, error2);
|
|
355753
356304
|
if (!args) return false;
|
|
355754
356305
|
try {
|
|
355755
|
-
await
|
|
356306
|
+
await execFileAsync3("adb", args, { timeout: 1e4, windowsHide: true });
|
|
355756
356307
|
return true;
|
|
355757
356308
|
} catch {
|
|
355758
356309
|
return false;
|
|
@@ -355919,6 +356470,9 @@ async function deleteSession(sessionId) {
|
|
|
355919
356470
|
} catch {
|
|
355920
356471
|
}
|
|
355921
356472
|
}
|
|
356473
|
+
async function deleteAppiumSession(sessionId) {
|
|
356474
|
+
await deleteSession(sessionId);
|
|
356475
|
+
}
|
|
355922
356476
|
async function takeScreenshot(sessionId) {
|
|
355923
356477
|
try {
|
|
355924
356478
|
const result = await wdRequest(`/session/${sessionId}/screenshot`);
|
|
@@ -355990,10 +356544,9 @@ async function stopScreenRecording(sessionId, log2) {
|
|
|
355990
356544
|
return void 0;
|
|
355991
356545
|
}
|
|
355992
356546
|
}
|
|
355993
|
-
function
|
|
355994
|
-
|
|
355995
|
-
|
|
355996
|
-
const parsed = bounds;
|
|
356547
|
+
function parseMobileBoundsLike(value) {
|
|
356548
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.x === "number" && typeof value.y === "number" && typeof value.width === "number" && typeof value.height === "number") {
|
|
356549
|
+
const parsed = value;
|
|
355997
356550
|
if ([parsed.x, parsed.y, parsed.width, parsed.height].every(Number.isFinite)) {
|
|
355998
356551
|
return {
|
|
355999
356552
|
x: parsed.x,
|
|
@@ -356005,6 +356558,9 @@ function boundsFromStep(step) {
|
|
|
356005
356558
|
}
|
|
356006
356559
|
return null;
|
|
356007
356560
|
}
|
|
356561
|
+
function boundsFromStep(step) {
|
|
356562
|
+
return parseMobileBoundsLike(step.metadata?.bounds);
|
|
356563
|
+
}
|
|
356008
356564
|
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
356009
356565
|
if (isCrossPlatform) return null;
|
|
356010
356566
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
@@ -356156,6 +356712,131 @@ async function revealLikelyOffscreenElement(sessionId) {
|
|
|
356156
356712
|
});
|
|
356157
356713
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
356158
356714
|
}
|
|
356715
|
+
function strictMatchMode() {
|
|
356716
|
+
const raw = (process.env.VALIDATEQA_MOBILE_STRICT_MATCH ?? "").trim().toLowerCase();
|
|
356717
|
+
if (raw === "strict") return "strict";
|
|
356718
|
+
if (raw === "off") return "off";
|
|
356719
|
+
return "lenient";
|
|
356720
|
+
}
|
|
356721
|
+
var MOBILE_MAX_DISAMBIG_CANDIDATES = 20;
|
|
356722
|
+
var COINCIDENT_CENTER_EPSILON_PX = 2;
|
|
356723
|
+
function isRealElementRect(rect) {
|
|
356724
|
+
return rect.width > 1 && rect.height > 1;
|
|
356725
|
+
}
|
|
356726
|
+
function rectContainsPoint(rect, x, y) {
|
|
356727
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
356728
|
+
}
|
|
356729
|
+
function rectCenter(rect) {
|
|
356730
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
356731
|
+
}
|
|
356732
|
+
function centerDistance(rect, point) {
|
|
356733
|
+
const center = rectCenter(rect);
|
|
356734
|
+
return Math.hypot(center.x - point.x, center.y - point.y);
|
|
356735
|
+
}
|
|
356736
|
+
function chooseRecordedMatch(candidates, opts) {
|
|
356737
|
+
if (candidates.length === 0) return { kind: "ambiguous", count: 0 };
|
|
356738
|
+
if (candidates.length === 1) {
|
|
356739
|
+
return { kind: "matched", elementId: candidates[0].elementId, reason: "single" };
|
|
356740
|
+
}
|
|
356741
|
+
if (typeof opts.matchIndex === "number" && Number.isInteger(opts.matchIndex) && opts.matchIndex >= 0 && opts.matchIndex < candidates.length && typeof opts.expectedMatchCount === "number" && opts.expectedMatchCount === candidates.length) {
|
|
356742
|
+
return { kind: "matched", elementId: candidates[opts.matchIndex].elementId, reason: "matchIndex" };
|
|
356743
|
+
}
|
|
356744
|
+
{
|
|
356745
|
+
const withRect = candidates.filter(
|
|
356746
|
+
(candidate) => candidate.rect != null
|
|
356747
|
+
);
|
|
356748
|
+
if (withRect.length === candidates.length) {
|
|
356749
|
+
const real = withRect.filter((candidate) => isRealElementRect(candidate.rect));
|
|
356750
|
+
if (real.length === 1) {
|
|
356751
|
+
return { kind: "matched", elementId: real[0].elementId, reason: "visible" };
|
|
356752
|
+
}
|
|
356753
|
+
const centers = withRect.map((candidate) => rectCenter(candidate.rect));
|
|
356754
|
+
const coincident = centers.every((center) => Math.abs(center.x - centers[0].x) <= COINCIDENT_CENTER_EPSILON_PX && Math.abs(center.y - centers[0].y) <= COINCIDENT_CENTER_EPSILON_PX);
|
|
356755
|
+
if (coincident) {
|
|
356756
|
+
return { kind: "matched", elementId: withRect[0].elementId, reason: "coincident" };
|
|
356757
|
+
}
|
|
356758
|
+
}
|
|
356759
|
+
}
|
|
356760
|
+
if (opts.recordedBounds) {
|
|
356761
|
+
const bounds = opts.recordedBounds;
|
|
356762
|
+
const center = rectCenter(bounds);
|
|
356763
|
+
const withRect = candidates.filter(
|
|
356764
|
+
(candidate) => candidate.rect != null
|
|
356765
|
+
);
|
|
356766
|
+
const containing = withRect.filter((candidate) => rectContainsPoint(candidate.rect, center.x, center.y));
|
|
356767
|
+
if (containing.length > 0) {
|
|
356768
|
+
const best = [...containing].sort(
|
|
356769
|
+
(a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height || centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356770
|
+
)[0];
|
|
356771
|
+
return { kind: "matched", elementId: best.elementId, reason: "bounds" };
|
|
356772
|
+
}
|
|
356773
|
+
if (withRect.length > 0) {
|
|
356774
|
+
const tolerance = Math.max(bounds.height, 1);
|
|
356775
|
+
const nearest = [...withRect].sort(
|
|
356776
|
+
(a, b) => centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356777
|
+
)[0];
|
|
356778
|
+
if (centerDistance(nearest.rect, center) <= tolerance) {
|
|
356779
|
+
return { kind: "matched", elementId: nearest.elementId, reason: "bounds" };
|
|
356780
|
+
}
|
|
356781
|
+
}
|
|
356782
|
+
}
|
|
356783
|
+
return opts.strict === "strict" ? { kind: "ambiguous", count: candidates.length } : { kind: "fallback", elementId: candidates[0].elementId, count: candidates.length };
|
|
356784
|
+
}
|
|
356785
|
+
function extractElementIdsFromPluralResponse(result) {
|
|
356786
|
+
const rawList = result?.value;
|
|
356787
|
+
if (!Array.isArray(rawList)) return [];
|
|
356788
|
+
const ids = [];
|
|
356789
|
+
for (const entry of rawList) {
|
|
356790
|
+
const record = entry;
|
|
356791
|
+
const id = validateElementId(record?.ELEMENT ?? record?.["element-6066-11e4-a52e-4f735466cecf"]);
|
|
356792
|
+
if (id) ids.push(id);
|
|
356793
|
+
}
|
|
356794
|
+
return ids;
|
|
356795
|
+
}
|
|
356796
|
+
async function findElements(sessionId, strategy) {
|
|
356797
|
+
const result = await wdRequest(`/session/${sessionId}/elements`, {
|
|
356798
|
+
method: "POST",
|
|
356799
|
+
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
356800
|
+
});
|
|
356801
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356802
|
+
}
|
|
356803
|
+
async function findWebViewElements(sessionPath, css) {
|
|
356804
|
+
const result = await wdRequest(`${sessionPath}/elements`, {
|
|
356805
|
+
method: "POST",
|
|
356806
|
+
body: JSON.stringify({ using: "css selector", value: css })
|
|
356807
|
+
});
|
|
356808
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356809
|
+
}
|
|
356810
|
+
async function readElementRect(sessionId, elementId) {
|
|
356811
|
+
try {
|
|
356812
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/rect`);
|
|
356813
|
+
return parseMobileBoundsLike(result?.value);
|
|
356814
|
+
} catch (err) {
|
|
356815
|
+
if (isTransportError(err)) throw err;
|
|
356816
|
+
return null;
|
|
356817
|
+
}
|
|
356818
|
+
}
|
|
356819
|
+
async function disambiguateElementIds(sessionId, candidateIds, opts) {
|
|
356820
|
+
const indexOnly = candidateIds.map((elementId) => ({ elementId }));
|
|
356821
|
+
const indexResult = chooseRecordedMatch(indexOnly, {
|
|
356822
|
+
recordedBounds: null,
|
|
356823
|
+
matchIndex: opts.matchIndex,
|
|
356824
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356825
|
+
strict: opts.strict
|
|
356826
|
+
});
|
|
356827
|
+
if (indexResult.kind === "matched" || candidateIds.length > MOBILE_MAX_DISAMBIG_CANDIDATES) {
|
|
356828
|
+
return indexResult;
|
|
356829
|
+
}
|
|
356830
|
+
const withRects = await Promise.all(
|
|
356831
|
+
candidateIds.map(async (elementId) => ({ elementId, rect: await readElementRect(sessionId, elementId) }))
|
|
356832
|
+
);
|
|
356833
|
+
return chooseRecordedMatch(withRects, {
|
|
356834
|
+
recordedBounds: opts.recordedBounds ?? null,
|
|
356835
|
+
matchIndex: opts.matchIndex,
|
|
356836
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356837
|
+
strict: opts.strict
|
|
356838
|
+
});
|
|
356839
|
+
}
|
|
356159
356840
|
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
356160
356841
|
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
356161
356842
|
if (strategies.length === 0) {
|
|
@@ -356163,22 +356844,45 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356163
356844
|
opts?.identityOnly ? "No identity locator (accessibility id / resource id / text) available for this step \u2014 cannot verify the screen" : "No locator strategies available for this step"
|
|
356164
356845
|
);
|
|
356165
356846
|
}
|
|
356847
|
+
const mode = strictMatchMode();
|
|
356848
|
+
const matchIndex = typeof target?.matchIndex === "number" ? target.matchIndex : void 0;
|
|
356849
|
+
const expectedMatchCount = typeof target?.expectedMatchCount === "number" ? target.expectedMatchCount : void 0;
|
|
356166
356850
|
const startTime = Date.now();
|
|
356167
356851
|
let lastError = "";
|
|
356852
|
+
let lastAmbiguity = null;
|
|
356168
356853
|
let revealAttempted = false;
|
|
356169
356854
|
while (Date.now() - startTime < timeoutMs) {
|
|
356170
356855
|
for (const strategy of strategies) {
|
|
356171
356856
|
try {
|
|
356172
|
-
const
|
|
356173
|
-
|
|
356174
|
-
|
|
356857
|
+
const ids = await findElements(sessionId, strategy);
|
|
356858
|
+
if (ids.length === 0) continue;
|
|
356859
|
+
if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
356860
|
+
return ids[0];
|
|
356861
|
+
}
|
|
356862
|
+
const result = await disambiguateElementIds(sessionId, ids, {
|
|
356863
|
+
recordedBounds: opts?.recordedBounds,
|
|
356864
|
+
matchIndex,
|
|
356865
|
+
expectedMatchCount,
|
|
356866
|
+
strict: mode
|
|
356175
356867
|
});
|
|
356176
|
-
|
|
356177
|
-
|
|
356868
|
+
if (result.kind === "matched") return result.elementId;
|
|
356869
|
+
const shouldFailClosed = opts?.failClosedOnAmbiguity || result.kind === "ambiguous" && !opts?.tolerateAmbiguity;
|
|
356870
|
+
if (shouldFailClosed) {
|
|
356871
|
+
throw new MobileAmbiguousMatchError(
|
|
356872
|
+
`ambiguous locator: ${result.count} matches, cannot identify the recorded instance (strategy=${strategy.using} value=${strategy.value})`
|
|
356873
|
+
);
|
|
356874
|
+
}
|
|
356875
|
+
opts?.log?.(
|
|
356876
|
+
`\u26A0 ambiguous locator (${result.count} matches for strategy=${strategy.using} value=${strategy.value}) \u2014 using the first match. Set VALIDATEQA_MOBILE_STRICT_MATCH=strict to fail instead.`
|
|
356178
356877
|
);
|
|
356179
|
-
|
|
356878
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
356180
356879
|
} catch (err) {
|
|
356181
356880
|
if (isTransportError(err)) throw err;
|
|
356881
|
+
if (err instanceof MobileAmbiguousMatchError) {
|
|
356882
|
+
lastAmbiguity = err;
|
|
356883
|
+
lastError = err.message;
|
|
356884
|
+
continue;
|
|
356885
|
+
}
|
|
356182
356886
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356183
356887
|
}
|
|
356184
356888
|
}
|
|
@@ -356193,6 +356897,7 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356193
356897
|
}
|
|
356194
356898
|
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
356195
356899
|
}
|
|
356900
|
+
if (lastAmbiguity) throw lastAmbiguity;
|
|
356196
356901
|
throw new Error(`Element not found after ${timeoutMs}ms. Tried ${strategies.length} strategies${revealAttempted ? " plus one reveal scroll" : ""}. Last error: ${lastError}`);
|
|
356197
356902
|
}
|
|
356198
356903
|
async function tapCoordinates(sessionId, bounds) {
|
|
@@ -356226,7 +356931,7 @@ async function prepareAndroidInputEnvironment(deviceId) {
|
|
|
356226
356931
|
];
|
|
356227
356932
|
for (const [namespace, key, value] of settingsToDisable) {
|
|
356228
356933
|
try {
|
|
356229
|
-
await
|
|
356934
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
356230
356935
|
timeout: 8e3,
|
|
356231
356936
|
windowsHide: true
|
|
356232
356937
|
});
|
|
@@ -356238,14 +356943,14 @@ async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
|
356238
356943
|
if (!deviceId) {
|
|
356239
356944
|
throw new Error("Android keyboard fallback requires a device id");
|
|
356240
356945
|
}
|
|
356241
|
-
await
|
|
356946
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
|
|
356242
356947
|
timeout: 1e4,
|
|
356243
356948
|
windowsHide: true
|
|
356244
356949
|
});
|
|
356245
356950
|
}
|
|
356246
356951
|
async function pressAndroidKeyboardEnter(deviceId) {
|
|
356247
356952
|
if (!deviceId) throw new Error("Android keyboard Enter fallback requires a device id");
|
|
356248
|
-
await
|
|
356953
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
|
|
356249
356954
|
timeout: 1e4,
|
|
356250
356955
|
windowsHide: true
|
|
356251
356956
|
});
|
|
@@ -356333,21 +357038,40 @@ async function executeInWebViewContext(sessionId, step, css, body) {
|
|
|
356333
357038
|
const cleanCss = css.startsWith("css=") ? css.slice(4) : css;
|
|
356334
357039
|
await withWebViewContext(sessionPath, webview, () => body(sessionPath, cleanCss));
|
|
356335
357040
|
}
|
|
356336
|
-
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS) {
|
|
357041
|
+
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
357042
|
+
const mode = strictMatchMode();
|
|
356337
357043
|
const startedAt = Date.now();
|
|
356338
357044
|
let lastError = "";
|
|
356339
357045
|
while (Date.now() - startedAt < timeoutMs) {
|
|
356340
357046
|
try {
|
|
356341
|
-
const
|
|
356342
|
-
|
|
356343
|
-
|
|
356344
|
-
})
|
|
356345
|
-
|
|
356346
|
-
|
|
356347
|
-
|
|
356348
|
-
|
|
357047
|
+
const ids = await findWebViewElements(sessionPath, css);
|
|
357048
|
+
if (ids.length === 0) {
|
|
357049
|
+
lastError = `no match for css=${css}`;
|
|
357050
|
+
} else if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
357051
|
+
return ids[0];
|
|
357052
|
+
} else {
|
|
357053
|
+
const candidates = ids.map((elementId) => ({ elementId }));
|
|
357054
|
+
const result = chooseRecordedMatch(candidates, {
|
|
357055
|
+
recordedBounds: null,
|
|
357056
|
+
matchIndex: opts?.matchIndex,
|
|
357057
|
+
expectedMatchCount: opts?.expectedMatchCount,
|
|
357058
|
+
strict: mode
|
|
357059
|
+
});
|
|
357060
|
+
if (result.kind === "matched") {
|
|
357061
|
+
return result.elementId;
|
|
357062
|
+
}
|
|
357063
|
+
if (result.kind === "ambiguous" && !opts?.tolerateAmbiguity) {
|
|
357064
|
+
throw new MobileAmbiguousMatchError(
|
|
357065
|
+
`ambiguous WEBVIEW locator: ${result.count} matches for css=${css}, cannot identify the recorded instance`
|
|
357066
|
+
);
|
|
357067
|
+
}
|
|
357068
|
+
opts?.log?.(
|
|
357069
|
+
`\u26A0 ambiguous WEBVIEW locator (${result.count} matches for css=${css}) \u2014 using the first match. Set VALIDATEQA_MOBILE_STRICT_MATCH=strict to fail instead.`
|
|
357070
|
+
);
|
|
357071
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
357072
|
+
}
|
|
356349
357073
|
} catch (err) {
|
|
356350
|
-
if (isTransportError(err)) throw err;
|
|
357074
|
+
if (isTransportError(err) || err instanceof MobileAmbiguousMatchError) throw err;
|
|
356351
357075
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356352
357076
|
}
|
|
356353
357077
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
@@ -356356,21 +357080,42 @@ async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIM
|
|
|
356356
357080
|
`WEBVIEW element not found for css=${css} after ${timeoutMs}ms` + (lastError ? `. Last error: ${lastError}` : "")
|
|
356357
357081
|
);
|
|
356358
357082
|
}
|
|
357083
|
+
function isWebViewHydrationTransient(err) {
|
|
357084
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357085
|
+
return /no such element|NoSuchElement|element not interactable|stale element|WEBVIEW step requires|Empty page dictionary|not connectable|no WEBVIEW context|chrome not reachable/i.test(message);
|
|
357086
|
+
}
|
|
357087
|
+
var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
|
|
357088
|
+
async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
|
|
357089
|
+
const deadline = Date.now() + Math.max(1e3, timeoutMs);
|
|
357090
|
+
for (; ; ) {
|
|
357091
|
+
try {
|
|
357092
|
+
await executeInWebViewContext(sessionId, step, css, body);
|
|
357093
|
+
return;
|
|
357094
|
+
} catch (err) {
|
|
357095
|
+
if (!isWebViewHydrationTransient(err) || Date.now() >= deadline) throw err;
|
|
357096
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
357097
|
+
}
|
|
357098
|
+
}
|
|
357099
|
+
}
|
|
356359
357100
|
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356360
357101
|
const webCss = stepWebViewCss(step);
|
|
356361
357102
|
if (webCss) {
|
|
356362
|
-
await
|
|
357103
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356363
357104
|
sessionId,
|
|
356364
357105
|
step,
|
|
356365
357106
|
webCss,
|
|
356366
|
-
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css)
|
|
357107
|
+
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css),
|
|
357108
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356367
357109
|
);
|
|
356368
357110
|
return;
|
|
356369
357111
|
}
|
|
356370
357112
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356371
357113
|
if (step.target) {
|
|
356372
357114
|
try {
|
|
356373
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357115
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357116
|
+
platform: platform3,
|
|
357117
|
+
recordedBounds: bounds
|
|
357118
|
+
});
|
|
356374
357119
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356375
357120
|
method: "POST",
|
|
356376
357121
|
body: "{}"
|
|
@@ -356392,11 +357137,12 @@ async function executeType(sessionId, step, context) {
|
|
|
356392
357137
|
const keysValue = keyboardSubmitOnly ? WEBDRIVER_ENTER_KEY : value;
|
|
356393
357138
|
const webCss = stepWebViewCss(step);
|
|
356394
357139
|
if (webCss) {
|
|
356395
|
-
await
|
|
357140
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356396
357141
|
sessionId,
|
|
356397
357142
|
step,
|
|
356398
357143
|
webCss,
|
|
356399
|
-
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value)
|
|
357144
|
+
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value),
|
|
357145
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356400
357146
|
);
|
|
356401
357147
|
return;
|
|
356402
357148
|
}
|
|
@@ -356407,7 +357153,9 @@ async function executeType(sessionId, step, context) {
|
|
|
356407
357153
|
const locatorStrategies = buildLocatorStrategies(step.target, context.platform);
|
|
356408
357154
|
if (step.target && locatorStrategies.length > 0) {
|
|
356409
357155
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
356410
|
-
platform: context.platform
|
|
357156
|
+
platform: context.platform,
|
|
357157
|
+
recordedBounds: bounds,
|
|
357158
|
+
log: context.log
|
|
356411
357159
|
});
|
|
356412
357160
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356413
357161
|
method: "POST",
|
|
@@ -356448,22 +357196,28 @@ async function executeType(sessionId, step, context) {
|
|
|
356448
357196
|
}
|
|
356449
357197
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356450
357198
|
const webCss = stepWebViewCss(step);
|
|
357199
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356451
357200
|
if (webCss) {
|
|
356452
357201
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356453
|
-
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357202
|
+
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357203
|
+
matchIndex: step.target?.matchIndex,
|
|
357204
|
+
expectedMatchCount: step.target?.expectedMatchCount
|
|
357205
|
+
});
|
|
356454
357206
|
await wdRequest(`${sessionPath}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356455
357207
|
});
|
|
356456
357208
|
return;
|
|
356457
357209
|
}
|
|
356458
357210
|
const locatorStrategies = buildLocatorStrategies(step.target, platform3);
|
|
356459
357211
|
if (step.target && locatorStrategies.length > 0) {
|
|
356460
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357212
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357213
|
+
platform: platform3,
|
|
357214
|
+
recordedBounds: bounds
|
|
357215
|
+
});
|
|
356461
357216
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
356462
357217
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
356463
357218
|
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356464
357219
|
return;
|
|
356465
357220
|
}
|
|
356466
|
-
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356467
357221
|
await clearField(`/session/${sessionId}`, bounds ?? void 0);
|
|
356468
357222
|
}
|
|
356469
357223
|
async function executeSwipe(sessionId, step) {
|
|
@@ -356524,34 +357278,47 @@ async function executeWaitForElement(sessionId, step, platform3) {
|
|
|
356524
357278
|
sessionId,
|
|
356525
357279
|
step,
|
|
356526
357280
|
webCss,
|
|
356527
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout).then(() => void 0)
|
|
357281
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout, { disambiguate: false }).then(() => void 0)
|
|
356528
357282
|
);
|
|
356529
357283
|
return;
|
|
356530
357284
|
}
|
|
356531
|
-
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
357285
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3, disambiguate: false });
|
|
356532
357286
|
}
|
|
356533
|
-
async function executeAssertVisible(sessionId, step, platform3) {
|
|
357287
|
+
async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356534
357288
|
const webCss = stepWebViewCss(step);
|
|
356535
357289
|
if (webCss) {
|
|
356536
357290
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356537
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357291
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357292
|
+
matchIndex: step.target?.matchIndex,
|
|
357293
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357294
|
+
tolerateAmbiguity: true
|
|
357295
|
+
});
|
|
356538
357296
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/displayed`);
|
|
356539
357297
|
if (!result2?.value) throw new Error(`WEBVIEW element found but not visible: css=${css}`);
|
|
356540
357298
|
});
|
|
356541
357299
|
return;
|
|
356542
357300
|
}
|
|
356543
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357301
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357302
|
+
identityOnly: true,
|
|
357303
|
+
platform: platform3,
|
|
357304
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357305
|
+
tolerateAmbiguity: true
|
|
357306
|
+
});
|
|
356544
357307
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
356545
357308
|
if (!result?.value) {
|
|
356546
357309
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
356547
357310
|
}
|
|
356548
357311
|
}
|
|
356549
|
-
async function executeAssertText(sessionId, step, platform3) {
|
|
357312
|
+
async function executeAssertText(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356550
357313
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
356551
357314
|
const webCss = stepWebViewCss(step);
|
|
356552
357315
|
if (webCss) {
|
|
356553
357316
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356554
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357317
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357318
|
+
matchIndex: step.target?.matchIndex,
|
|
357319
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357320
|
+
tolerateAmbiguity: true
|
|
357321
|
+
});
|
|
356555
357322
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/text`);
|
|
356556
357323
|
const actualText2 = typeof result2?.value === "string" ? result2.value : "";
|
|
356557
357324
|
const textCandidates2 = await readElementTextCandidates(sessionId, elementId2, actualText2);
|
|
@@ -356563,7 +357330,12 @@ async function executeAssertText(sessionId, step, platform3) {
|
|
|
356563
357330
|
});
|
|
356564
357331
|
return;
|
|
356565
357332
|
}
|
|
356566
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357333
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357334
|
+
identityOnly: true,
|
|
357335
|
+
platform: platform3,
|
|
357336
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357337
|
+
tolerateAmbiguity: true
|
|
357338
|
+
});
|
|
356567
357339
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
356568
357340
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
356569
357341
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -356603,6 +357375,8 @@ function acquireControlPortSlot(deviceId) {
|
|
|
356603
357375
|
function releaseControlPortSlot(deviceId) {
|
|
356604
357376
|
controlPortSlotsByDevice.delete(deviceId);
|
|
356605
357377
|
}
|
|
357378
|
+
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357379
|
+
var pinnedDeviceSnapshot = null;
|
|
356606
357380
|
async function executeMobileTest(options) {
|
|
356607
357381
|
const logLines = [];
|
|
356608
357382
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -356610,6 +357384,7 @@ async function executeMobileTest(options) {
|
|
|
356610
357384
|
const { steps: runSteps, missingKeys: missingVariableKeys } = resolveMobileStepValuePlaceholders(options.steps, options.testVariables);
|
|
356611
357385
|
const runPlatform = options.target.platform;
|
|
356612
357386
|
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
357387
|
+
const captureStepScreenshots = options.captureStepScreenshots !== false;
|
|
356613
357388
|
if (missingVariableKeys.length > 0) {
|
|
356614
357389
|
log2(`\u26A0 Unresolved step-value placeholder(s): ${missingVariableKeys.map((k) => `{{${k}}}`).join(", ")} \u2014 no matching test variable configured for this project/run.`);
|
|
356615
357390
|
}
|
|
@@ -356674,32 +357449,42 @@ async function executeMobileTest(options) {
|
|
|
356674
357449
|
await startAppiumServer();
|
|
356675
357450
|
log2("Appium server started");
|
|
356676
357451
|
}
|
|
356677
|
-
log2("Discovering devices...");
|
|
356678
|
-
const devices = discoverAllDevices({ includeApps: true });
|
|
356679
357452
|
const platformFilter = options.target.platform;
|
|
356680
|
-
const
|
|
356681
|
-
|
|
356682
|
-
);
|
|
356683
|
-
if (
|
|
356684
|
-
|
|
356685
|
-
|
|
357453
|
+
const pinnedDeviceId = options.target.assignedDeviceId;
|
|
357454
|
+
const candidateSnapshot = pinnedDeviceSnapshot;
|
|
357455
|
+
const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && candidateSnapshot.deviceId === pinnedDeviceId && Date.now() - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
|
|
357456
|
+
if (reusablePinnedSnapshot) {
|
|
357457
|
+
assertDeviceNotQuarantined(reusablePinnedSnapshot.device.id);
|
|
357458
|
+
selectedDevice = reusablePinnedSnapshot.device;
|
|
357459
|
+
log2(`Reusing known device (skipped device re-scan): ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357460
|
+
} else {
|
|
357461
|
+
log2("Discovering devices...");
|
|
357462
|
+
const devices = discoverAllDevices({ includeApps: true });
|
|
357463
|
+
const matchingDevices = devices.filter(
|
|
357464
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
356686
357465
|
);
|
|
356687
|
-
|
|
356688
|
-
if (options.target.assignedDeviceId) {
|
|
356689
|
-
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
356690
|
-
if (!selectedDevice) {
|
|
357466
|
+
if (matchingDevices.length === 0) {
|
|
356691
357467
|
throw new Error(
|
|
356692
|
-
`
|
|
357468
|
+
`No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
|
|
356693
357469
|
);
|
|
356694
357470
|
}
|
|
357471
|
+
if (options.target.assignedDeviceId) {
|
|
357472
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
357473
|
+
if (!selectedDevice) {
|
|
357474
|
+
throw new Error(
|
|
357475
|
+
`Assigned ${platformFilter} device ${options.target.assignedDeviceId} is no longer booted/available (booted: ${matchingDevices.map((d) => d.id).join(", ") || "none"}). Refusing to fall back to another device.`
|
|
357476
|
+
);
|
|
357477
|
+
}
|
|
357478
|
+
}
|
|
357479
|
+
if (!selectedDevice && options.target.recordedDeviceId) {
|
|
357480
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
|
|
357481
|
+
}
|
|
357482
|
+
if (!selectedDevice) {
|
|
357483
|
+
selectedDevice = matchingDevices[0];
|
|
357484
|
+
}
|
|
357485
|
+
log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357486
|
+
pinnedDeviceSnapshot = options.target.assignedDeviceId ? { deviceId: options.target.assignedDeviceId, device: selectedDevice, at: Date.now() } : null;
|
|
356695
357487
|
}
|
|
356696
|
-
if (!selectedDevice && options.target.recordedDeviceId) {
|
|
356697
|
-
selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
|
|
356698
|
-
}
|
|
356699
|
-
if (!selectedDevice) {
|
|
356700
|
-
selectedDevice = matchingDevices[0];
|
|
356701
|
-
}
|
|
356702
|
-
log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
|
|
356703
357488
|
try {
|
|
356704
357489
|
capture = await startMobileNetworkCapture({
|
|
356705
357490
|
platform: options.target.platform,
|
|
@@ -356766,7 +357551,14 @@ async function executeMobileTest(options) {
|
|
|
356766
357551
|
const resetAllowed = shouldResetMobileAppData(options.target.platform, selectedDevice.id);
|
|
356767
357552
|
if (resetAllowed) {
|
|
356768
357553
|
try {
|
|
356769
|
-
|
|
357554
|
+
if (options.target.platform === "IOS") {
|
|
357555
|
+
await terminateTargetApp(sessionId, "IOS", options.target.appId).catch(() => {
|
|
357556
|
+
});
|
|
357557
|
+
const wiped = await clearIosSimulatorAppData(selectedDevice.id, options.target.appId);
|
|
357558
|
+
if (!wiped.ok) throw new Error(wiped.detail);
|
|
357559
|
+
} else {
|
|
357560
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
357561
|
+
}
|
|
356770
357562
|
clearedAppDataBeforeLaunch = true;
|
|
356771
357563
|
log2(resetPolicy === "CLEAR_APP_DATA" ? "App data cleared before launch (target resetPolicy=CLEAR_APP_DATA)." : "App data cleared before launch (test was recorded from a cold logged-out state).");
|
|
356772
357564
|
} catch (err) {
|
|
@@ -356819,17 +357611,21 @@ async function executeMobileTest(options) {
|
|
|
356819
357611
|
const entryStep = selectEntryDriftGateStep(runSteps, runPlatform);
|
|
356820
357612
|
if (entryStep) {
|
|
356821
357613
|
try {
|
|
356822
|
-
const entryTimeout =
|
|
357614
|
+
const entryTimeout = Math.max(DEFAULT_STEP_TIMEOUT_MS, COLD_BOOT_ENTRY_TIMEOUT_MS);
|
|
356823
357615
|
const entryWebCss = stepWebViewCss(entryStep);
|
|
356824
357616
|
if (entryWebCss) {
|
|
356825
357617
|
await executeInWebViewContext(
|
|
356826
357618
|
sessionId,
|
|
356827
357619
|
entryStep,
|
|
356828
357620
|
entryWebCss,
|
|
356829
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout).then(() => void 0)
|
|
357621
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout, { disambiguate: false }).then(() => void 0)
|
|
356830
357622
|
);
|
|
356831
357623
|
} else {
|
|
356832
|
-
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357624
|
+
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357625
|
+
identityOnly: true,
|
|
357626
|
+
platform: runPlatform,
|
|
357627
|
+
disambiguate: false
|
|
357628
|
+
});
|
|
356833
357629
|
}
|
|
356834
357630
|
} catch (err) {
|
|
356835
357631
|
if (isTransportError(err)) throw err;
|
|
@@ -356889,10 +357685,10 @@ async function executeMobileTest(options) {
|
|
|
356889
357685
|
await executeWaitForElement(sessionId, step, runPlatform);
|
|
356890
357686
|
break;
|
|
356891
357687
|
case "assertVisible":
|
|
356892
|
-
await executeAssertVisible(sessionId, step, runPlatform);
|
|
357688
|
+
await executeAssertVisible(sessionId, step, runPlatform, isCrossPlatform);
|
|
356893
357689
|
break;
|
|
356894
357690
|
case "assertText":
|
|
356895
|
-
await executeAssertText(sessionId, step, runPlatform);
|
|
357691
|
+
await executeAssertText(sessionId, step, runPlatform, isCrossPlatform);
|
|
356896
357692
|
break;
|
|
356897
357693
|
default: {
|
|
356898
357694
|
const _exhaustive = step.action;
|
|
@@ -356911,7 +357707,7 @@ async function executeMobileTest(options) {
|
|
|
356911
357707
|
environmental = stepError;
|
|
356912
357708
|
}
|
|
356913
357709
|
}
|
|
356914
|
-
const screenshot = await takeScreenshot(sessionId).catch(() => null);
|
|
357710
|
+
const screenshot = captureStepScreenshots ? await takeScreenshot(sessionId).catch(() => null) : null;
|
|
356915
357711
|
screenshots.push(screenshot ?? "");
|
|
356916
357712
|
if (status === "failed") {
|
|
356917
357713
|
await recordFirstFailingSource();
|
|
@@ -357173,10 +357969,10 @@ async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
|
357173
357969
|
await executeWaitForElement(sessionId, step, platform3);
|
|
357174
357970
|
return true;
|
|
357175
357971
|
case "assertVisible":
|
|
357176
|
-
await executeAssertVisible(sessionId, step, platform3);
|
|
357972
|
+
await executeAssertVisible(sessionId, step, platform3, true);
|
|
357177
357973
|
return true;
|
|
357178
357974
|
case "assertText":
|
|
357179
|
-
await executeAssertText(sessionId, step, platform3);
|
|
357975
|
+
await executeAssertText(sessionId, step, platform3, true);
|
|
357180
357976
|
return true;
|
|
357181
357977
|
default:
|
|
357182
357978
|
break;
|
|
@@ -357258,7 +358054,15 @@ async function harvestPlatformLocators(options) {
|
|
|
357258
358054
|
const identityStrategies = webViewCss ? [] : buildIdentityLocatorStrategies(step.target);
|
|
357259
358055
|
if (!webViewCss && identityStrategies.length > 0 && step.target) {
|
|
357260
358056
|
try {
|
|
357261
|
-
elementId = await findElement(
|
|
358057
|
+
elementId = await findElement(
|
|
358058
|
+
session.sessionId,
|
|
358059
|
+
step.target,
|
|
358060
|
+
Math.max(DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS),
|
|
358061
|
+
{
|
|
358062
|
+
identityOnly: true,
|
|
358063
|
+
failClosedOnAmbiguity: true
|
|
358064
|
+
}
|
|
358065
|
+
);
|
|
357262
358066
|
} catch (err) {
|
|
357263
358067
|
if (isTransportError(err)) throw err;
|
|
357264
358068
|
elementId = null;
|
|
@@ -357424,10 +358228,10 @@ function decideMobileDeviceGate(input) {
|
|
|
357424
358228
|
|
|
357425
358229
|
// src/services/mobile-bridge.ts
|
|
357426
358230
|
var import_http = require("http");
|
|
357427
|
-
var
|
|
358231
|
+
var import_child_process4 = require("child_process");
|
|
357428
358232
|
var import_crypto = require("crypto");
|
|
357429
358233
|
var import_os4 = require("os");
|
|
357430
|
-
var
|
|
358234
|
+
var import_path4 = require("path");
|
|
357431
358235
|
var import_fs3 = require("fs");
|
|
357432
358236
|
init_dist();
|
|
357433
358237
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
@@ -357534,7 +358338,7 @@ async function getWindowSize3() {
|
|
|
357534
358338
|
}
|
|
357535
358339
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
357536
358340
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
357537
|
-
var BRIDGE_PID_FILE = (0,
|
|
358341
|
+
var BRIDGE_PID_FILE = (0, import_path4.join)((0, import_os4.homedir)(), ".validate.qa", "mobile-bridge.pid");
|
|
357538
358342
|
function readBridgePidFile() {
|
|
357539
358343
|
try {
|
|
357540
358344
|
const raw = (0, import_fs3.readFileSync)(BRIDGE_PID_FILE, "utf-8").trim();
|
|
@@ -357546,7 +358350,7 @@ function readBridgePidFile() {
|
|
|
357546
358350
|
}
|
|
357547
358351
|
function writeBridgePidFile(pid) {
|
|
357548
358352
|
try {
|
|
357549
|
-
(0, import_fs3.mkdirSync)((0,
|
|
358353
|
+
(0, import_fs3.mkdirSync)((0, import_path4.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
|
|
357550
358354
|
(0, import_fs3.writeFileSync)(BRIDGE_PID_FILE, String(pid), { mode: 384 });
|
|
357551
358355
|
} catch {
|
|
357552
358356
|
}
|
|
@@ -357561,19 +358365,19 @@ function killStaleBridge() {
|
|
|
357561
358365
|
const pid = readBridgePidFile();
|
|
357562
358366
|
if (!pid) return;
|
|
357563
358367
|
try {
|
|
357564
|
-
(0,
|
|
358368
|
+
(0, import_child_process4.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
|
|
357565
358369
|
} catch {
|
|
357566
358370
|
clearBridgePidFile();
|
|
357567
358371
|
return;
|
|
357568
358372
|
}
|
|
357569
358373
|
try {
|
|
357570
|
-
const pidsOnPort = (0,
|
|
358374
|
+
const pidsOnPort = (0, import_child_process4.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
|
|
357571
358375
|
const pidList = pidsOnPort.split("\n").filter(Boolean);
|
|
357572
358376
|
if (!pidList.includes(String(pid))) {
|
|
357573
358377
|
return;
|
|
357574
358378
|
}
|
|
357575
|
-
(0,
|
|
357576
|
-
(0,
|
|
358379
|
+
(0, import_child_process4.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
|
|
358380
|
+
(0, import_child_process4.execFileSync)("sleep", ["1"], { stdio: "ignore" });
|
|
357577
358381
|
} catch {
|
|
357578
358382
|
} finally {
|
|
357579
358383
|
clearBridgePidFile();
|
|
@@ -357846,6 +358650,13 @@ async function startSession(validated, res) {
|
|
|
357846
358650
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
357847
358651
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
357848
358652
|
"appium:noReset": true,
|
|
358653
|
+
// Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
|
|
358654
|
+
// Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
|
|
358655
|
+
// "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
|
|
358656
|
+
// skip the GUI open. Scoped to simulator UDIDs; APPIUM_IOS_HEADLESS=false
|
|
358657
|
+
// overrides. Verified live: without this, recorder/mirror sessions on iOS
|
|
358658
|
+
// die with "Simulator.app does not exist" while executor sessions work.
|
|
358659
|
+
...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
357849
358660
|
...realCaps
|
|
357850
358661
|
} : {
|
|
357851
358662
|
platformName: "Android",
|
|
@@ -357864,7 +358675,12 @@ async function startSession(validated, res) {
|
|
|
357864
358675
|
capabilities: {
|
|
357865
358676
|
alwaysMatch: capabilities
|
|
357866
358677
|
}
|
|
357867
|
-
})
|
|
358678
|
+
}),
|
|
358679
|
+
// Session creation is the one legitimately slow WebDriver call (iOS WDA
|
|
358680
|
+
// build/launch can take minutes on a cold toolchain) — same ceiling as
|
|
358681
|
+
// the executor's wdRequest session-create path. Without this, the shared
|
|
358682
|
+
// appiumRequest 10s default aborts every iOS recorder session.
|
|
358683
|
+
signal: AbortSignal.timeout(WD_SESSION_CREATE_TIMEOUT_MS)
|
|
357868
358684
|
});
|
|
357869
358685
|
state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
|
|
357870
358686
|
if (!state.appiumSessionId) {
|
|
@@ -358642,6 +359458,9 @@ function detachMirrorSession(deviceId) {
|
|
|
358642
359458
|
stopAllMirrorClients();
|
|
358643
359459
|
stopSessionReaper();
|
|
358644
359460
|
}
|
|
359461
|
+
function getMirrorSessionId(deviceId) {
|
|
359462
|
+
return runnerChannels.get(deviceId)?.sessionId;
|
|
359463
|
+
}
|
|
358645
359464
|
|
|
358646
359465
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
358647
359466
|
var import_runner_core9 = __toESM(require_dist2());
|
|
@@ -358672,6 +359491,7 @@ var MobileBridgeDriver = class {
|
|
|
358672
359491
|
sessionId;
|
|
358673
359492
|
platform;
|
|
358674
359493
|
appId;
|
|
359494
|
+
deviceId;
|
|
358675
359495
|
allowAppReset;
|
|
358676
359496
|
recoverSession;
|
|
358677
359497
|
maxSessionRecoveries;
|
|
@@ -358692,6 +359512,7 @@ var MobileBridgeDriver = class {
|
|
|
358692
359512
|
this.sessionId = config.sessionId;
|
|
358693
359513
|
this.platform = config.platform;
|
|
358694
359514
|
this.appId = config.appId;
|
|
359515
|
+
this.deviceId = config.deviceId;
|
|
358695
359516
|
this.allowAppReset = config.allowAppReset === true;
|
|
358696
359517
|
this.recoverSession = config.recoverSession;
|
|
358697
359518
|
this.maxSessionRecoveries = Math.max(0, Math.floor(config.maxSessionRecoveries ?? 1));
|
|
@@ -358989,17 +359810,34 @@ var MobileBridgeDriver = class {
|
|
|
358989
359810
|
});
|
|
358990
359811
|
}
|
|
358991
359812
|
/**
|
|
358992
|
-
* Deterministic state reset: clear app data + relaunch
|
|
358993
|
-
*
|
|
358994
|
-
*
|
|
358995
|
-
*
|
|
358996
|
-
*
|
|
359813
|
+
* Deterministic state reset: clear app data + relaunch, per the
|
|
359814
|
+
* runner-computed allowAppReset policy (Android emulators / iOS simulators
|
|
359815
|
+
* only — a personal physical device is never wiped). Android clears via
|
|
359816
|
+
* `mobile: clearApp`; iOS terminates the app and wipes its simulator data
|
|
359817
|
+
* container + keychain through `simctl` (see ios-simulator-reset.ts).
|
|
359818
|
+
* Returns false when unsupported/disallowed or when the clear fails —
|
|
359819
|
+
* callers fall back to relaunch(), preserving today's behavior. On success,
|
|
359820
|
+
* waits for cold-boot content so recording and replay agree on the true
|
|
359821
|
+
* entry screen.
|
|
358997
359822
|
*/
|
|
358998
359823
|
async resetAppState() {
|
|
358999
|
-
if (!this.allowAppReset
|
|
359824
|
+
if (!this.allowAppReset) return false;
|
|
359825
|
+
if (this.platform === "IOS" && !isIosSimulatorUdid(this.deviceId)) return false;
|
|
359000
359826
|
return this.withSessionRecovery("app data reset", async () => {
|
|
359001
359827
|
try {
|
|
359002
|
-
|
|
359828
|
+
if (this.platform === "IOS") {
|
|
359829
|
+
try {
|
|
359830
|
+
await this.executeScript("mobile: terminateApp", [{ ...this.appLifecycleArgs(), timeout: 5e3 }]);
|
|
359831
|
+
} catch (err) {
|
|
359832
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
359833
|
+
}
|
|
359834
|
+
const wiped = await clearIosSimulatorAppData(this.deviceId ?? "", this.appId);
|
|
359835
|
+
if (!wiped.ok) {
|
|
359836
|
+
throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
|
|
359837
|
+
}
|
|
359838
|
+
} else {
|
|
359839
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
359840
|
+
}
|
|
359003
359841
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
359004
359842
|
try {
|
|
359005
359843
|
await waitForColdBootSettle(this.sessionPath);
|
|
@@ -359138,7 +359976,7 @@ function readActiveBundleId2(result) {
|
|
|
359138
359976
|
}
|
|
359139
359977
|
|
|
359140
359978
|
// src/services/doctor.ts
|
|
359141
|
-
var
|
|
359979
|
+
var import_child_process5 = require("child_process");
|
|
359142
359980
|
var import_fs4 = require("fs");
|
|
359143
359981
|
var import_os5 = require("os");
|
|
359144
359982
|
var C = {
|
|
@@ -359158,7 +359996,7 @@ function computeDoctorReadiness(web, ios, android) {
|
|
|
359158
359996
|
}
|
|
359159
359997
|
function run(cmd) {
|
|
359160
359998
|
try {
|
|
359161
|
-
return (0,
|
|
359999
|
+
return (0, import_child_process5.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
359162
360000
|
} catch {
|
|
359163
360001
|
return null;
|
|
359164
360002
|
}
|
|
@@ -359590,6 +360428,113 @@ function envInt(name, fallback) {
|
|
|
359590
360428
|
}
|
|
359591
360429
|
var MOBILE_DEVICE_CEILING = Math.max(0, envInt("MOBILE_DEVICE_CEILING", 0));
|
|
359592
360430
|
var MOBILE_HEAL_HOLD_MS = Math.max(0, envInt("MOBILE_HEAL_HOLD_MS", 2e4));
|
|
360431
|
+
var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 30 * 6e4));
|
|
360432
|
+
var MOBILE_RUN_ABORT_GRACE_MS = Math.max(0, envInt("MOBILE_RUN_ABORT_GRACE_MS", 2 * 6e4));
|
|
360433
|
+
var MOBILE_CANCEL_POLL_MS = Math.max(0, envInt("MOBILE_CANCEL_POLL_MS", 15e3));
|
|
360434
|
+
var MobileRunWatchdog = class {
|
|
360435
|
+
constructor(options) {
|
|
360436
|
+
this.options = options;
|
|
360437
|
+
}
|
|
360438
|
+
phase = "idle";
|
|
360439
|
+
released = false;
|
|
360440
|
+
quarantined = false;
|
|
360441
|
+
cancelAbortTimer = null;
|
|
360442
|
+
cancelGraceTimer = null;
|
|
360443
|
+
/** Current phase (exposed for tests / observability). */
|
|
360444
|
+
getPhase() {
|
|
360445
|
+
return this.phase;
|
|
360446
|
+
}
|
|
360447
|
+
/** Arm the wall-clock abort timer. Call once, immediately after dispatch. */
|
|
360448
|
+
start() {
|
|
360449
|
+
if (this.phase !== "idle") return;
|
|
360450
|
+
this.phase = "active";
|
|
360451
|
+
if (this.options.abortMs > 0) {
|
|
360452
|
+
this.cancelAbortTimer = this.options.schedule(
|
|
360453
|
+
() => this.triggerAbort(`exceeded the ${Math.round(this.options.abortMs / 6e4)}min wall-clock ceiling`),
|
|
360454
|
+
this.options.abortMs
|
|
360455
|
+
);
|
|
360456
|
+
}
|
|
360457
|
+
}
|
|
360458
|
+
/** Stage 1. Idempotent (a no-op unless still ACTIVE). Also called by the
|
|
360459
|
+
* cancellation observer. Quarantines the device (blocking the run's own
|
|
360460
|
+
* session-recovery from resurrecting the killed session — observed live),
|
|
360461
|
+
* kills the run's session (best-effort), and starts the grace clock;
|
|
360462
|
+
* deliberately does NOT release the lease. */
|
|
360463
|
+
triggerAbort(reason) {
|
|
360464
|
+
if (this.phase !== "active") return;
|
|
360465
|
+
this.phase = "aborting";
|
|
360466
|
+
this.clearAbortTimer();
|
|
360467
|
+
if (this.options.deviceId) {
|
|
360468
|
+
this.quarantined = true;
|
|
360469
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360470
|
+
this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
|
|
360471
|
+
} else {
|
|
360472
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
|
|
360473
|
+
}
|
|
360474
|
+
if (this.options.graceMs > 0) {
|
|
360475
|
+
this.cancelGraceTimer = this.options.schedule(() => this.forceRelease(), this.options.graceMs);
|
|
360476
|
+
} else {
|
|
360477
|
+
this.forceRelease();
|
|
360478
|
+
}
|
|
360479
|
+
}
|
|
360480
|
+
/** The run's promise settled (normal completion, or after an abort). The single
|
|
360481
|
+
* release owner: releases at most once and lifts any quarantine. Idempotent. */
|
|
360482
|
+
settle() {
|
|
360483
|
+
if (this.phase === "settled") return;
|
|
360484
|
+
const wasForceReleased = this.released;
|
|
360485
|
+
this.phase = "settled";
|
|
360486
|
+
this.clearAbortTimer();
|
|
360487
|
+
this.clearGraceTimer();
|
|
360488
|
+
if (!this.released) {
|
|
360489
|
+
this.released = true;
|
|
360490
|
+
this.options.callbacks.release();
|
|
360491
|
+
}
|
|
360492
|
+
if (this.quarantined && this.options.deviceId) {
|
|
360493
|
+
this.quarantined = false;
|
|
360494
|
+
this.options.callbacks.unquarantine(this.options.deviceId);
|
|
360495
|
+
if (wasForceReleased) {
|
|
360496
|
+
this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
|
|
360497
|
+
}
|
|
360498
|
+
}
|
|
360499
|
+
}
|
|
360500
|
+
/** Stage 2. The run did not settle within the grace window after the abort. Force-
|
|
360501
|
+
* release the slot to unwedge capacity; the device stays quarantined (since the
|
|
360502
|
+
* stage-1 abort) until the promise eventually settles. Idempotent and guarded
|
|
360503
|
+
* against a late settle. */
|
|
360504
|
+
forceRelease() {
|
|
360505
|
+
if (this.released || this.phase === "settled") return;
|
|
360506
|
+
this.clearGraceTimer();
|
|
360507
|
+
this.released = true;
|
|
360508
|
+
this.options.callbacks.release();
|
|
360509
|
+
if (this.options.deviceId) {
|
|
360510
|
+
if (!this.quarantined) {
|
|
360511
|
+
this.quarantined = true;
|
|
360512
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360513
|
+
}
|
|
360514
|
+
this.options.callbacks.log(
|
|
360515
|
+
"warn",
|
|
360516
|
+
`Run ${this.options.runId} did not settle ${Math.round(this.options.graceMs / 1e3)}s after abort \u2014 force-released its slot; device ${this.options.deviceId} stays quarantined from re-lease until the run's promise settles.`
|
|
360517
|
+
);
|
|
360518
|
+
} else {
|
|
360519
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} did not settle after abort \u2014 force-released its slot to unwedge mobile capacity.`);
|
|
360520
|
+
}
|
|
360521
|
+
}
|
|
360522
|
+
clearAbortTimer() {
|
|
360523
|
+
if (this.cancelAbortTimer) {
|
|
360524
|
+
this.cancelAbortTimer();
|
|
360525
|
+
this.cancelAbortTimer = null;
|
|
360526
|
+
}
|
|
360527
|
+
}
|
|
360528
|
+
clearGraceTimer() {
|
|
360529
|
+
if (this.cancelGraceTimer) {
|
|
360530
|
+
this.cancelGraceTimer();
|
|
360531
|
+
this.cancelGraceTimer = null;
|
|
360532
|
+
}
|
|
360533
|
+
}
|
|
360534
|
+
};
|
|
360535
|
+
function isServerCancelledRunStatus(status) {
|
|
360536
|
+
return status === "ERROR" || status === "CANCELLED" || status === "CANCELLING";
|
|
360537
|
+
}
|
|
359593
360538
|
function scrubAuditMessages(messages, testVariables) {
|
|
359594
360539
|
if (messages === void 0 || messages === null) return messages;
|
|
359595
360540
|
const compacted = compactForAudit(messages);
|
|
@@ -359665,7 +360610,7 @@ var log = {
|
|
|
359665
360610
|
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
359666
360611
|
};
|
|
359667
360612
|
function getRunsDir() {
|
|
359668
|
-
const dir = process.env.RUNS_DIR || (0,
|
|
360613
|
+
const dir = process.env.RUNS_DIR || (0, import_path5.join)((0, import_os6.homedir)(), ".validate.qa", "runs");
|
|
359669
360614
|
(0, import_runner_core12.ensurePrivateDirectory)(dir);
|
|
359670
360615
|
return dir;
|
|
359671
360616
|
}
|
|
@@ -361151,9 +362096,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361151
362096
|
// flip the test to needs_manual_authoring), so an environmentally
|
|
361152
362097
|
// aborted pass — wrong screen, system dialog, transport flake —
|
|
361153
362098
|
// must not persist step divergences it could not fairly judge.
|
|
361154
|
-
//
|
|
361155
|
-
//
|
|
361156
|
-
|
|
362099
|
+
// The same applies to a harvest that ADVANCED THROUGH NOTHING
|
|
362100
|
+
// (reachedOrder null): it never got past the entry screen, so its
|
|
362101
|
+
// step judgments are cold-boot/entry noise, not app knowledge —
|
|
362102
|
+
// observed live: a slow WKWebView hydration marked step 1
|
|
362103
|
+
// divergent and permanently dead-ended the test in
|
|
362104
|
+
// needs_manual_authoring with no retry path. Harvested bundles
|
|
362105
|
+
// are still posted: they were positively resolved and the merge
|
|
362106
|
+
// is idempotent.
|
|
362107
|
+
divergentOrders: harvest.environmentalError || harvest.reachedOrder == null ? [] : harvest.divergentOrders,
|
|
361157
362108
|
reachedOrder: harvest.reachedOrder,
|
|
361158
362109
|
logs: harvest.logs,
|
|
361159
362110
|
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
@@ -361597,9 +362548,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361597
362548
|
sessionId: session.sessionId,
|
|
361598
362549
|
platform: platform3,
|
|
361599
362550
|
appId: session.appId,
|
|
361600
|
-
|
|
361601
|
-
//
|
|
361602
|
-
//
|
|
362551
|
+
deviceId: session.deviceId,
|
|
362552
|
+
// Deterministic-state authorization: Android emulators / iOS
|
|
362553
|
+
// simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
|
|
362554
|
+
// Android default) — a personal physical device must never have
|
|
362555
|
+
// its app data wiped.
|
|
361603
362556
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
361604
362557
|
maxSessionRecoveries: 1,
|
|
361605
362558
|
recoverSession: async (cause) => {
|
|
@@ -362115,6 +363068,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362115
363068
|
sessionId: opened.sessionId,
|
|
362116
363069
|
platform: target.platform,
|
|
362117
363070
|
appId: opened.appId,
|
|
363071
|
+
deviceId: opened.deviceId,
|
|
362118
363072
|
allowAppReset: target.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(target.platform, opened.deviceId)
|
|
362119
363073
|
});
|
|
362120
363074
|
};
|
|
@@ -362727,6 +363681,7 @@ async function startRunner(opts) {
|
|
|
362727
363681
|
else activeMobileOpsByPlatform.set(p, next);
|
|
362728
363682
|
};
|
|
362729
363683
|
const leasedDeviceIds = /* @__PURE__ */ new Map();
|
|
363684
|
+
const activeMobileRuns = /* @__PURE__ */ new Map();
|
|
362730
363685
|
let activeMobileDiscoveries = 0;
|
|
362731
363686
|
const mobileHealHold = /* @__PURE__ */ new Map();
|
|
362732
363687
|
const pruneMobileHealHolds = () => {
|
|
@@ -362977,6 +363932,32 @@ async function startRunner(opts) {
|
|
|
362977
363932
|
}
|
|
362978
363933
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
362979
363934
|
pidsWatchdog.unref();
|
|
363935
|
+
const pollMobileRunCancellations = async () => {
|
|
363936
|
+
if (shuttingDown || activeMobileRuns.size === 0) return;
|
|
363937
|
+
const runIds = [...activeMobileRuns.keys()];
|
|
363938
|
+
try {
|
|
363939
|
+
const res = await fetch(`${serverUrl}/api/runner/runs/status`, {
|
|
363940
|
+
method: "POST",
|
|
363941
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
363942
|
+
body: JSON.stringify({ runIds }),
|
|
363943
|
+
signal: AbortSignal.timeout(1e4)
|
|
363944
|
+
});
|
|
363945
|
+
if (!res.ok) return;
|
|
363946
|
+
const body = await res.json().catch(() => null);
|
|
363947
|
+
if (!body || !Array.isArray(body.runs)) return;
|
|
363948
|
+
for (const row of body.runs) {
|
|
363949
|
+
if (!row || typeof row.id !== "string" || typeof row.status !== "string") continue;
|
|
363950
|
+
if (!isServerCancelledRunStatus(row.status)) continue;
|
|
363951
|
+
const watchdog = activeMobileRuns.get(row.id);
|
|
363952
|
+
if (watchdog) watchdog.triggerAbort(`was cancelled server-side (status ${row.status})`);
|
|
363953
|
+
}
|
|
363954
|
+
} catch {
|
|
363955
|
+
}
|
|
363956
|
+
};
|
|
363957
|
+
const mobileCancelPoll = enableMobile && MOBILE_CANCEL_POLL_MS > 0 ? setInterval(() => {
|
|
363958
|
+
void pollMobileRunCancellations();
|
|
363959
|
+
}, MOBILE_CANCEL_POLL_MS) : null;
|
|
363960
|
+
mobileCancelPoll?.unref?.();
|
|
362980
363961
|
const deferralLogged = /* @__PURE__ */ new Map();
|
|
362981
363962
|
const logDeferredOnce = (runId, line) => {
|
|
362982
363963
|
if (deferralLogged.get(runId) === line) return;
|
|
@@ -363059,14 +364040,17 @@ async function startRunner(opts) {
|
|
|
363059
364040
|
});
|
|
363060
364041
|
const capacity = MOBILE_DEVICE_CEILING > 0 ? Math.min(derived, MOBILE_DEVICE_CEILING) : derived;
|
|
363061
364042
|
const leaseTarget = run2.target ?? run2.deviceRequirements;
|
|
363062
|
-
const
|
|
364043
|
+
const leaseExclusions = {
|
|
364044
|
+
has: (id) => leasedDeviceIds.has(id) || isDeviceQuarantined(id)
|
|
364045
|
+
};
|
|
364046
|
+
const leaseSelection = selectFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot, {
|
|
363063
364047
|
appId: typeof leaseTarget?.appId === "string" ? leaseTarget.appId : void 0,
|
|
363064
364048
|
recordedDeviceId: typeof leaseTarget?.recordedDeviceId === "string" ? leaseTarget.recordedDeviceId : void 0,
|
|
363065
364049
|
platformVersion: typeof leaseTarget?.platformVersion === "string" ? leaseTarget.platformVersion : void 0,
|
|
363066
364050
|
deviceName: typeof run2.deviceRequirements?.deviceName === "string" ? run2.deviceRequirements.deviceName : void 0,
|
|
363067
364051
|
resetPolicy: leaseTarget?.resetPolicy
|
|
363068
364052
|
});
|
|
363069
|
-
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform,
|
|
364053
|
+
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot) : null;
|
|
363070
364054
|
if (leaseSelection.kind === "unsatisfiable") {
|
|
363071
364055
|
mobileLeaseError = leaseSelection.reason;
|
|
363072
364056
|
log.warn(` Mobile requirements unsatisfiable for ${run2.runId}: ${leaseSelection.reason}; claiming once to report the configuration error`);
|
|
@@ -363124,6 +364108,54 @@ async function startRunner(opts) {
|
|
|
363124
364108
|
await new Promise((r) => setTimeout(r, staggerMs));
|
|
363125
364109
|
}
|
|
363126
364110
|
claimedThisPoll++;
|
|
364111
|
+
const applyRelease = () => {
|
|
364112
|
+
activeRuns--;
|
|
364113
|
+
decrementType(runMode);
|
|
364114
|
+
decrementProject(runProject);
|
|
364115
|
+
if (isMobileOp) {
|
|
364116
|
+
if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
|
|
364117
|
+
if (isDiscovery) activeMobileDiscoveries--;
|
|
364118
|
+
if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
|
|
364119
|
+
activeMobileRuns.delete(run2.runId);
|
|
364120
|
+
}
|
|
364121
|
+
};
|
|
364122
|
+
const runWatchdog = isMobileOp ? new MobileRunWatchdog({
|
|
364123
|
+
runId: run2.runId,
|
|
364124
|
+
deviceId: leasedDeviceId ?? void 0,
|
|
364125
|
+
abortMs: MOBILE_RUN_WATCHDOG_MS,
|
|
364126
|
+
graceMs: MOBILE_RUN_ABORT_GRACE_MS,
|
|
364127
|
+
// Real timers, unref'd so a pending watchdog never keeps the process
|
|
364128
|
+
// alive on shutdown.
|
|
364129
|
+
schedule: (fn, ms) => {
|
|
364130
|
+
const t = setTimeout(fn, ms);
|
|
364131
|
+
if (typeof t.unref === "function") t.unref();
|
|
364132
|
+
return () => clearTimeout(t);
|
|
364133
|
+
},
|
|
364134
|
+
callbacks: {
|
|
364135
|
+
abort: (deviceId, rid, reason) => {
|
|
364136
|
+
const sessionId = getMirrorSessionId(deviceId);
|
|
364137
|
+
log.warn(
|
|
364138
|
+
`Run ${rid} ${reason} \u2014 aborting its in-flight Appium session${sessionId ? ` ${sessionId}` : " (no live session found)"} on device ${deviceId} so it fails fast and frees the lease.`
|
|
364139
|
+
);
|
|
364140
|
+
if (sessionId) void deleteAppiumSession(sessionId);
|
|
364141
|
+
},
|
|
364142
|
+
release: applyRelease,
|
|
364143
|
+
quarantine: (deviceId) => {
|
|
364144
|
+
quarantineDevice(deviceId);
|
|
364145
|
+
},
|
|
364146
|
+
unquarantine: (deviceId) => {
|
|
364147
|
+
unquarantineDevice(deviceId);
|
|
364148
|
+
},
|
|
364149
|
+
log: (level, message) => {
|
|
364150
|
+
if (level === "warn") log.warn(message);
|
|
364151
|
+
else log.info(message);
|
|
364152
|
+
}
|
|
364153
|
+
}
|
|
364154
|
+
}) : null;
|
|
364155
|
+
if (runWatchdog) {
|
|
364156
|
+
activeMobileRuns.set(run2.runId, runWatchdog);
|
|
364157
|
+
runWatchdog.start();
|
|
364158
|
+
}
|
|
363127
364159
|
executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
|
|
363128
364160
|
if (mobilePlatform) {
|
|
363129
364161
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
@@ -363134,14 +364166,8 @@ async function startRunner(opts) {
|
|
|
363134
364166
|
const msg = err instanceof Error ? err.message : String(err);
|
|
363135
364167
|
log.warn(`Run ${run2.runId} crashed: ${msg}`);
|
|
363136
364168
|
}).finally(() => {
|
|
363137
|
-
|
|
363138
|
-
|
|
363139
|
-
decrementProject(runProject);
|
|
363140
|
-
if (isMobileOp) {
|
|
363141
|
-
if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
|
|
363142
|
-
if (isDiscovery) activeMobileDiscoveries--;
|
|
363143
|
-
if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
|
|
363144
|
-
}
|
|
364169
|
+
if (runWatchdog) runWatchdog.settle();
|
|
364170
|
+
else applyRelease();
|
|
363145
364171
|
});
|
|
363146
364172
|
}
|
|
363147
364173
|
}
|