@validate.qa/runner 1.0.70 → 1.0.72
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 +1526 -384
- package/dist/cli.mjs +1523 -381
- 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) {
|
|
@@ -11292,8 +11491,8 @@ var require_thread_stream = __commonJS({
|
|
|
11292
11491
|
"use strict";
|
|
11293
11492
|
var { version } = require_package();
|
|
11294
11493
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
11295
|
-
var { Worker } = require("worker_threads");
|
|
11296
|
-
var { join:
|
|
11494
|
+
var { Worker: Worker2 } = require("worker_threads");
|
|
11495
|
+
var { join: join8 } = require("path");
|
|
11297
11496
|
var { pathToFileURL } = require("url");
|
|
11298
11497
|
var { wait } = require_wait();
|
|
11299
11498
|
var {
|
|
@@ -11344,8 +11543,8 @@ 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"] ||
|
|
11348
|
-
const worker = new
|
|
11546
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11547
|
+
const worker = new Worker2(toExecute, {
|
|
11349
11548
|
...opts.workerOpts,
|
|
11350
11549
|
name: opts.workerOpts?.name || "thread-stream",
|
|
11351
11550
|
trackUnmanagedFds: false,
|
|
@@ -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) => {
|
|
@@ -74445,6 +74644,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74445
74644
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
74446
74645
|
exports2.computeMobileSurveyBreadthContinuation = computeMobileSurveyBreadthContinuation;
|
|
74447
74646
|
exports2.computeMobileBreadthFinishRedirectDecision = computeMobileBreadthFinishRedirectDecision;
|
|
74647
|
+
exports2.hybridScreenFingerprint = hybridScreenFingerprint;
|
|
74448
74648
|
exports2.runMobileExplorePhase = runMobileExplorePhase;
|
|
74449
74649
|
var crypto_1 = require("crypto");
|
|
74450
74650
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
@@ -74898,11 +75098,12 @@ var require_mobile_explorer = __commonJS({
|
|
|
74898
75098
|
const validSurfaceDigest = /^[0-9a-f]{16,64}$/.test(normalizedSurfaceDigest) ? normalizedSurfaceDigest : void 0;
|
|
74899
75099
|
const legacyElementDigests = [...new Set(elements.map((element) => element.pageIdentityDigest?.trim().toLowerCase() ?? "").filter((digest) => /^[0-9a-f]{16,64}$/.test(digest)))];
|
|
74900
75100
|
const pageIdentityDigest = validSurfaceDigest ?? (legacyElementDigests.length === 1 ? legacyElementDigests[0] : void 0);
|
|
75101
|
+
const hasStableControlIdentity = elements.some((element) => Boolean(safeHybridIdentity(element.id, credentials) || safeHybridIdentity(element.name, credentials)));
|
|
74901
75102
|
const descriptors = elements.map((element) => {
|
|
74902
75103
|
const editable = element.editable === true || /^(?:input|textarea|select)$/i.test(element.tag) || /(?:textbox|searchbox|combobox)/i.test(element.role);
|
|
74903
75104
|
const id = safeHybridIdentity(element.id, credentials)?.toLowerCase() ?? "";
|
|
74904
75105
|
const name = safeHybridIdentity(element.name, credentials)?.toLowerCase() ?? "";
|
|
74905
|
-
const semanticText = editable || id || name ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75106
|
+
const semanticText = editable || id || name || hasStableControlIdentity ? "" : stableHybridSemanticText(element.text, credentials);
|
|
74906
75107
|
return [
|
|
74907
75108
|
element.tag.trim().toLowerCase(),
|
|
74908
75109
|
element.role.trim().toLowerCase(),
|
|
@@ -81167,10 +81368,14 @@ var require_mobile_generator = __commonJS({
|
|
|
81167
81368
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
81168
81369
|
"use strict";
|
|
81169
81370
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
81371
|
+
exports2.structuralScreenKey = structuralScreenKey;
|
|
81170
81372
|
exports2.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
81171
81373
|
exports2.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
81172
81374
|
exports2.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
81173
81375
|
exports2.collapseRetryCycles = collapseRetryCycles;
|
|
81376
|
+
exports2.coalesceSameFieldTypes = coalesceSameFieldTypes;
|
|
81377
|
+
exports2.coalesceRepeatedTaps = coalesceRepeatedTaps;
|
|
81378
|
+
exports2.elideNoOpDetours = elideNoOpDetours;
|
|
81174
81379
|
exports2.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
|
|
81175
81380
|
exports2.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
|
|
81176
81381
|
exports2.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
|
|
@@ -81379,6 +81584,22 @@ var require_mobile_generator = __commonJS({
|
|
|
81379
81584
|
MOBILE_ASSERT_TEXT_TOOL,
|
|
81380
81585
|
FINISH_GENERATE_TOOL
|
|
81381
81586
|
];
|
|
81587
|
+
var STRUCTURAL_KEY_MAX_ELEMENTS = 16;
|
|
81588
|
+
function structuralScreenKey(resolved) {
|
|
81589
|
+
if (!resolved)
|
|
81590
|
+
return null;
|
|
81591
|
+
const nativeElements = resolved.screen.elements;
|
|
81592
|
+
const hybridElements = resolved.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81593
|
+
if (nativeElements.length + hybridElements.length < 2)
|
|
81594
|
+
return null;
|
|
81595
|
+
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 ?? ""}`);
|
|
81596
|
+
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 ?? ""}`);
|
|
81597
|
+
const activityOrBundle = resolved.signal.activity ?? resolved.signal.bundleId ?? "";
|
|
81598
|
+
const payload = `${activityOrBundle}
|
|
81599
|
+
${nativeSkeleton.join("\n")}
|
|
81600
|
+
${hybridSkeleton.join("\n")}`;
|
|
81601
|
+
return (0, crypto_1.createHash)("sha1").update(payload).digest("hex");
|
|
81602
|
+
}
|
|
81382
81603
|
function platformForMedium(medium) {
|
|
81383
81604
|
if (medium === "IOS_NATIVE")
|
|
81384
81605
|
return "IOS";
|
|
@@ -81540,18 +81761,39 @@ var require_mobile_generator = __commonJS({
|
|
|
81540
81761
|
}
|
|
81541
81762
|
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
81763
|
}
|
|
81543
|
-
function
|
|
81764
|
+
function alreadyFocusedWithEqualValue(element, value) {
|
|
81765
|
+
return element.focused === true && typeof element.text === "string" && value.length > 0 && element.text === value;
|
|
81766
|
+
}
|
|
81767
|
+
function nativeElementsOf(resolved) {
|
|
81768
|
+
return resolved?.screen.elements ?? [];
|
|
81769
|
+
}
|
|
81770
|
+
function hybridElementsOf(resolved) {
|
|
81771
|
+
return resolved?.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81772
|
+
}
|
|
81773
|
+
function metadataForElement(element, screenName, platform3, elements = []) {
|
|
81774
|
+
const match = (0, mobile_source_parser_js_2.identityMatchInfo)(element, elements);
|
|
81544
81775
|
return {
|
|
81545
81776
|
screenName,
|
|
81546
81777
|
targetLabel: labelForElement(element),
|
|
81547
81778
|
locatorBundle: (0, mobile_source_parser_js_2.buildLocatorBundle)(element, platform3),
|
|
81548
|
-
bounds: element.bounds
|
|
81779
|
+
bounds: element.bounds,
|
|
81780
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81549
81781
|
};
|
|
81550
81782
|
}
|
|
81551
|
-
function
|
|
81783
|
+
function hybridIdentityMatchInfo(element, elements) {
|
|
81784
|
+
const hybridIdentityValue = (candidate) => nonEmpty3(candidate.id) ?? (candidate.editable === true ? void 0 : nonEmpty3(candidate.text));
|
|
81785
|
+
const chosenValue = hybridIdentityValue(element);
|
|
81786
|
+
if (!chosenValue)
|
|
81787
|
+
return void 0;
|
|
81788
|
+
const sameValue = elements.filter((candidate) => hybridIdentityValue(candidate) === chosenValue);
|
|
81789
|
+
const matchIndex = sameValue.indexOf(element);
|
|
81790
|
+
return matchIndex >= 0 && sameValue.length > 1 ? { matchIndex, expectedMatchCount: sameValue.length } : void 0;
|
|
81791
|
+
}
|
|
81792
|
+
function metadataForHybridElement(element, screenName, elements = []) {
|
|
81552
81793
|
const editable = isEditableHybridElement(element);
|
|
81553
81794
|
const label = labelForHybridElement(element);
|
|
81554
81795
|
const safeText = editable ? void 0 : nonEmpty3(element.text);
|
|
81796
|
+
const match = hybridIdentityMatchInfo(element, elements);
|
|
81555
81797
|
return {
|
|
81556
81798
|
screenName,
|
|
81557
81799
|
targetLabel: label,
|
|
@@ -81560,11 +81802,16 @@ var require_mobile_generator = __commonJS({
|
|
|
81560
81802
|
webviewContext: element.webviewContext,
|
|
81561
81803
|
bounds: element.bounds,
|
|
81562
81804
|
locatorBundle: {
|
|
81563
|
-
|
|
81805
|
+
// WebView controls resolve via `webCss` (above) in the WEBVIEW context, not
|
|
81806
|
+
// by xpath. `LocatorBundle.xpath` is contractually an XPath, so never store a
|
|
81807
|
+
// `css=` selector here — an empty xpath keeps the fallback chain honest and
|
|
81808
|
+
// prevents the css selector being misread as an xpath at replay.
|
|
81809
|
+
xpath: "",
|
|
81564
81810
|
className: element.tag || "webview",
|
|
81565
81811
|
...element.id ? { accessibilityId: element.id } : {},
|
|
81566
81812
|
...safeText ? { text: safeText } : {}
|
|
81567
|
-
}
|
|
81813
|
+
},
|
|
81814
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81568
81815
|
};
|
|
81569
81816
|
}
|
|
81570
81817
|
function isEditableHybridElement(element) {
|
|
@@ -82282,7 +82529,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82282
82529
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
82283
82530
|
recordAction: (action) => {
|
|
82284
82531
|
const screenHash = getLatest()?.signal.screenHash;
|
|
82285
|
-
const
|
|
82532
|
+
const structuralKey = structuralScreenKey(getLatest());
|
|
82533
|
+
const stamped = screenHash && !action.metadata.screenHash || structuralKey && !action.metadata.structuralKey ? {
|
|
82534
|
+
...action,
|
|
82535
|
+
metadata: {
|
|
82536
|
+
...action.metadata,
|
|
82537
|
+
...screenHash && !action.metadata.screenHash ? { screenHash } : {},
|
|
82538
|
+
...structuralKey && !action.metadata.structuralKey ? { structuralKey } : {}
|
|
82539
|
+
}
|
|
82540
|
+
} : action;
|
|
82286
82541
|
actions.push((0, shared_1.placeholderizeRecordedMobileAction)(finalizeAction(stamped), credentials));
|
|
82287
82542
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
82288
82543
|
assertCount++;
|
|
@@ -82388,7 +82643,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82388
82643
|
const result2 = await driver.webTap(hybridTap.webviewContext, hybridTap.css);
|
|
82389
82644
|
const absorbed2 = await absorbActionResult(result2, "mobile_tap(webview)", opensAuthenticationSurface(hybridTap));
|
|
82390
82645
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82391
|
-
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2);
|
|
82646
|
+
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2, hybridElementsOf(before2));
|
|
82392
82647
|
const label = metadata.targetLabel ?? hybridTap.css;
|
|
82393
82648
|
const tapAction = {
|
|
82394
82649
|
type: "TAP",
|
|
@@ -82440,7 +82695,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82440
82695
|
const tapAction = {
|
|
82441
82696
|
type: "TAP",
|
|
82442
82697
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82443
|
-
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
82698
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before)),
|
|
82444
82699
|
innerText: null,
|
|
82445
82700
|
url: "",
|
|
82446
82701
|
value: null
|
|
@@ -82480,10 +82735,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82480
82735
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82481
82736
|
}
|
|
82482
82737
|
const beforeScreenName2 = screenName();
|
|
82738
|
+
const before2 = latest();
|
|
82483
82739
|
const result2 = await driver.webType(hybridType.webviewContext, hybridType.css, value);
|
|
82484
82740
|
const absorbed2 = await absorbActionResult(result2, "mobile_type(webview)");
|
|
82485
82741
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82486
|
-
const metadata = metadataForHybridElement(hybridType, beforeScreenName2);
|
|
82742
|
+
const metadata = metadataForHybridElement(hybridType, beforeScreenName2, hybridElementsOf(before2));
|
|
82487
82743
|
const label = metadata.targetLabel ?? hybridType.css;
|
|
82488
82744
|
recordAction({
|
|
82489
82745
|
type: "TYPE",
|
|
@@ -82498,7 +82754,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82498
82754
|
const element = resolveRef(toolArgs.ref);
|
|
82499
82755
|
if (!element)
|
|
82500
82756
|
return unknownRef(toolArgs.ref);
|
|
82757
|
+
if (alreadyFocusedWithEqualValue(element, value)) {
|
|
82758
|
+
return {
|
|
82759
|
+
ok: false,
|
|
82760
|
+
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.`
|
|
82761
|
+
};
|
|
82762
|
+
}
|
|
82501
82763
|
const beforeScreenName = screenName();
|
|
82764
|
+
const before = latest();
|
|
82502
82765
|
const result = await driver.type(value, element.bounds);
|
|
82503
82766
|
const absorbed = await absorbActionResult(result, "mobile_type");
|
|
82504
82767
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82506,7 +82769,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82506
82769
|
type: "TYPE",
|
|
82507
82770
|
value,
|
|
82508
82771
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82509
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82772
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82510
82773
|
});
|
|
82511
82774
|
}
|
|
82512
82775
|
const payload = actionPayload(result, absorbed);
|
|
@@ -82524,6 +82787,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82524
82787
|
return { ok: false, error: "This driver does not support non-clearing WEBVIEW keyboard submit." };
|
|
82525
82788
|
}
|
|
82526
82789
|
const beforeScreenName2 = screenName();
|
|
82790
|
+
const before2 = latest();
|
|
82527
82791
|
const result2 = await driver.webSubmitKeyboard(hybridField.webviewContext, hybridField.css);
|
|
82528
82792
|
const absorbed2 = await absorbActionResult(result2, "mobile_submit_keyboard(webview)");
|
|
82529
82793
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
@@ -82531,7 +82795,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82531
82795
|
type: "TYPE",
|
|
82532
82796
|
value: "\n",
|
|
82533
82797
|
selector: `css=${hybridField.css}`,
|
|
82534
|
-
metadata: metadataForHybridElement(hybridField, beforeScreenName2)
|
|
82798
|
+
metadata: metadataForHybridElement(hybridField, beforeScreenName2, hybridElementsOf(before2))
|
|
82535
82799
|
});
|
|
82536
82800
|
}
|
|
82537
82801
|
return actionPayload(result2, absorbed2);
|
|
@@ -82543,6 +82807,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82543
82807
|
return { ok: false, error: "This driver does not support non-clearing keyboard submit." };
|
|
82544
82808
|
}
|
|
82545
82809
|
const beforeScreenName = screenName();
|
|
82810
|
+
const before = latest();
|
|
82546
82811
|
const result = await driver.submitKeyboard(element.bounds);
|
|
82547
82812
|
const absorbed = await absorbActionResult(result, "mobile_submit_keyboard");
|
|
82548
82813
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82550,7 +82815,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82550
82815
|
type: "TYPE",
|
|
82551
82816
|
value: "\n",
|
|
82552
82817
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82553
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82818
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82554
82819
|
});
|
|
82555
82820
|
}
|
|
82556
82821
|
return actionPayload(result, absorbed);
|
|
@@ -82575,10 +82840,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82575
82840
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82576
82841
|
}
|
|
82577
82842
|
const beforeScreenName2 = screenName();
|
|
82843
|
+
const before2 = latest();
|
|
82578
82844
|
const result2 = await driver.webType(hybridFill.webviewContext, hybridFill.css, secret);
|
|
82579
82845
|
const absorbed2 = await absorbActionResult(result2, "mobile_fill_credential(webview)");
|
|
82580
82846
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82581
|
-
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2);
|
|
82847
|
+
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2, hybridElementsOf(before2));
|
|
82582
82848
|
const label = metadata.targetLabel ?? hybridFill.css;
|
|
82583
82849
|
recordAction({
|
|
82584
82850
|
type: "TYPE",
|
|
@@ -82593,7 +82859,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82593
82859
|
const element = resolveRef(toolArgs.ref);
|
|
82594
82860
|
if (!element)
|
|
82595
82861
|
return unknownRef(toolArgs.ref);
|
|
82862
|
+
if (alreadyFocusedWithEqualValue(element, secret)) {
|
|
82863
|
+
return {
|
|
82864
|
+
ok: false,
|
|
82865
|
+
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.`
|
|
82866
|
+
};
|
|
82867
|
+
}
|
|
82596
82868
|
const beforeScreenName = screenName();
|
|
82869
|
+
const before = latest();
|
|
82597
82870
|
const result = await driver.type(secret, element.bounds);
|
|
82598
82871
|
const absorbed = await absorbActionResult(result, "mobile_fill_credential");
|
|
82599
82872
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82601,7 +82874,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82601
82874
|
type: "TYPE",
|
|
82602
82875
|
value: (0, shared_1.mobileCredentialPlaceholder)(key),
|
|
82603
82876
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82604
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82877
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82605
82878
|
});
|
|
82606
82879
|
log2(` fill_credential: recorded {{${key}}} into ${labelForElement(element)}`);
|
|
82607
82880
|
}
|
|
@@ -82629,13 +82902,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82629
82902
|
if (!element)
|
|
82630
82903
|
return unknownRef(toolArgs.ref);
|
|
82631
82904
|
const beforeScreenName = screenName();
|
|
82905
|
+
const before = latest();
|
|
82632
82906
|
const result = await driver.clear(element.bounds);
|
|
82633
82907
|
const absorbed = await absorbActionResult(result, "mobile_clear");
|
|
82634
82908
|
if (shouldRecordAction(result, absorbed)) {
|
|
82635
82909
|
recordAction({
|
|
82636
82910
|
type: "CLEAR",
|
|
82637
82911
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82638
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82912
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82639
82913
|
});
|
|
82640
82914
|
}
|
|
82641
82915
|
return actionPayload(result, absorbed);
|
|
@@ -82698,7 +82972,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82698
82972
|
case "mobile_assert_visible": {
|
|
82699
82973
|
const hybrid = resolveHybridRef(toolArgs.ref);
|
|
82700
82974
|
if (hybrid) {
|
|
82701
|
-
const metadata = metadataForHybridElement(hybrid, screenName());
|
|
82975
|
+
const metadata = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82702
82976
|
recordAction({
|
|
82703
82977
|
type: "ASSERT_VISIBLE",
|
|
82704
82978
|
selector: `css=${hybrid.css}`,
|
|
@@ -82738,7 +83012,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82738
83012
|
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
83013
|
};
|
|
82740
83014
|
}
|
|
82741
|
-
const metadata2 = metadataForHybridElement(hybrid, screenName());
|
|
83015
|
+
const metadata2 = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82742
83016
|
const stableWebIdentity = Boolean(nonEmpty3(hybrid.id) || nonEmpty3(hybrid.name));
|
|
82743
83017
|
if (stableWebIdentity) {
|
|
82744
83018
|
recordAction({
|
|
@@ -82791,7 +83065,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82791
83065
|
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
83066
|
};
|
|
82793
83067
|
}
|
|
82794
|
-
const metadata = metadataForElement(element, screenName(), platform3);
|
|
83068
|
+
const metadata = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82795
83069
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82796
83070
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
82797
83071
|
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
@@ -82837,7 +83111,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82837
83111
|
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
83112
|
};
|
|
82839
83113
|
}
|
|
82840
|
-
const metadata2 = metadataForElement(element, screenName(), platform3);
|
|
83114
|
+
const metadata2 = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82841
83115
|
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata: metadata2 });
|
|
82842
83116
|
return { ok: true, label: metadata2.targetLabel ?? "element" };
|
|
82843
83117
|
}
|
|
@@ -82887,7 +83161,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82887
83161
|
error: `The live element matching ${JSON.stringify(accessibilityId ?? text)} is not a durable assertion target (${weakReason}).`
|
|
82888
83162
|
};
|
|
82889
83163
|
}
|
|
82890
|
-
const metadata = metadataForElement(liveElement, screenName(), platform3);
|
|
83164
|
+
const metadata = metadataForElement(liveElement, screenName(), platform3, live.screen.elements);
|
|
82891
83165
|
recordAction({
|
|
82892
83166
|
type: "ASSERT_VISIBLE",
|
|
82893
83167
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(liveElement),
|
|
@@ -83015,7 +83289,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83015
83289
|
actions.push(finalizeAction({
|
|
83016
83290
|
type: "ASSERT_VISIBLE",
|
|
83017
83291
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(anchor),
|
|
83018
|
-
metadata: metadataForElement(anchor, screen, platform3)
|
|
83292
|
+
metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
|
|
83019
83293
|
}));
|
|
83020
83294
|
return true;
|
|
83021
83295
|
}
|
|
@@ -83358,6 +83632,168 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83358
83632
|
}
|
|
83359
83633
|
return out;
|
|
83360
83634
|
}
|
|
83635
|
+
function isDurableMutation(action) {
|
|
83636
|
+
if (action.type === "TYPE" || action.type === "CLEAR")
|
|
83637
|
+
return true;
|
|
83638
|
+
if (action.type === "TAP")
|
|
83639
|
+
return (0, shared_1.isStateValueMobileClassName)(action.metadata.locatorBundle?.className);
|
|
83640
|
+
return false;
|
|
83641
|
+
}
|
|
83642
|
+
function mobileWanderTraceEnabled2() {
|
|
83643
|
+
const v = (process.env.VALIDATEQA_MOBILE_WANDER_TRACE || "").trim().toLowerCase();
|
|
83644
|
+
return v === "true" || v === "1";
|
|
83645
|
+
}
|
|
83646
|
+
function wanderTraceLabel(action) {
|
|
83647
|
+
const identity = action.metadata.targetLabel || action.selector || action.metadata.locatorBundle?.accessibilityId || action.metadata.locatorBundle?.resourceId || "(no identity)";
|
|
83648
|
+
return `${action.type} ${identity}`;
|
|
83649
|
+
}
|
|
83650
|
+
function logWanderDiff(pass, before, after, log2) {
|
|
83651
|
+
if (after.length >= before.length)
|
|
83652
|
+
return;
|
|
83653
|
+
log2(` ${pass}: collapsed ${before.length - after.length} step(s) (${before.length} \u2192 ${after.length}).`);
|
|
83654
|
+
if (!mobileWanderTraceEnabled2())
|
|
83655
|
+
return;
|
|
83656
|
+
const survivors = new Set(after);
|
|
83657
|
+
for (const action of before) {
|
|
83658
|
+
if (!survivors.has(action)) {
|
|
83659
|
+
log2(` [wander] ${pass}: dropped ${wanderTraceLabel(action)}`);
|
|
83660
|
+
}
|
|
83661
|
+
}
|
|
83662
|
+
}
|
|
83663
|
+
function coalesceSameFieldTypes(actions) {
|
|
83664
|
+
if (actions.length <= 1)
|
|
83665
|
+
return actions;
|
|
83666
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83667
|
+
const lastTypeIndexBySelector = /* @__PURE__ */ new Map();
|
|
83668
|
+
const refocusTapIndexesBySelector = /* @__PURE__ */ new Map();
|
|
83669
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83670
|
+
const current = actions[i];
|
|
83671
|
+
if (current.type === "ASSERT_VISIBLE" || current.type === "ASSERT_TEXT") {
|
|
83672
|
+
lastTypeIndexBySelector.clear();
|
|
83673
|
+
refocusTapIndexesBySelector.clear();
|
|
83674
|
+
continue;
|
|
83675
|
+
}
|
|
83676
|
+
if (current.type === "CLEAR") {
|
|
83677
|
+
lastTypeIndexBySelector.delete(current.selector);
|
|
83678
|
+
refocusTapIndexesBySelector.delete(current.selector);
|
|
83679
|
+
continue;
|
|
83680
|
+
}
|
|
83681
|
+
if (isFormCommitAction(current)) {
|
|
83682
|
+
lastTypeIndexBySelector.clear();
|
|
83683
|
+
refocusTapIndexesBySelector.clear();
|
|
83684
|
+
continue;
|
|
83685
|
+
}
|
|
83686
|
+
if (current.type === "TAP") {
|
|
83687
|
+
if (current.selector) {
|
|
83688
|
+
const list = refocusTapIndexesBySelector.get(current.selector) ?? [];
|
|
83689
|
+
list.push(i);
|
|
83690
|
+
refocusTapIndexesBySelector.set(current.selector, list);
|
|
83691
|
+
}
|
|
83692
|
+
continue;
|
|
83693
|
+
}
|
|
83694
|
+
if (current.type !== "TYPE" || !current.selector)
|
|
83695
|
+
continue;
|
|
83696
|
+
const priorIndex = lastTypeIndexBySelector.get(current.selector);
|
|
83697
|
+
if (priorIndex !== void 0) {
|
|
83698
|
+
toDrop.add(priorIndex);
|
|
83699
|
+
for (const tapIndex of refocusTapIndexesBySelector.get(current.selector) ?? []) {
|
|
83700
|
+
if (tapIndex > priorIndex && tapIndex < i)
|
|
83701
|
+
toDrop.add(tapIndex);
|
|
83702
|
+
}
|
|
83703
|
+
}
|
|
83704
|
+
lastTypeIndexBySelector.set(current.selector, i);
|
|
83705
|
+
refocusTapIndexesBySelector.set(current.selector, []);
|
|
83706
|
+
}
|
|
83707
|
+
if (toDrop.size === 0)
|
|
83708
|
+
return actions;
|
|
83709
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83710
|
+
}
|
|
83711
|
+
function coalesceRepeatedTaps(actions) {
|
|
83712
|
+
if (actions.length <= 1)
|
|
83713
|
+
return actions;
|
|
83714
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83715
|
+
const lastTapIndexBySelector = /* @__PURE__ */ new Map();
|
|
83716
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83717
|
+
const action = actions[i];
|
|
83718
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT") {
|
|
83719
|
+
lastTapIndexBySelector.clear();
|
|
83720
|
+
continue;
|
|
83721
|
+
}
|
|
83722
|
+
if (isDurableMutation(action)) {
|
|
83723
|
+
lastTapIndexBySelector.clear();
|
|
83724
|
+
continue;
|
|
83725
|
+
}
|
|
83726
|
+
if (action.type !== "TAP" || !action.selector)
|
|
83727
|
+
continue;
|
|
83728
|
+
const key = action.metadata.structuralKey;
|
|
83729
|
+
const priorIndex = lastTapIndexBySelector.get(action.selector);
|
|
83730
|
+
if (priorIndex !== void 0 && key && actions[priorIndex].metadata.structuralKey === key) {
|
|
83731
|
+
const priorHash = actions[priorIndex].metadata.screenHash;
|
|
83732
|
+
const currentHash = action.metadata.screenHash;
|
|
83733
|
+
const screenAdvanced = !!priorHash && !!currentHash && priorHash !== currentHash;
|
|
83734
|
+
if (!screenAdvanced) {
|
|
83735
|
+
toDrop.add(priorIndex);
|
|
83736
|
+
}
|
|
83737
|
+
}
|
|
83738
|
+
lastTapIndexBySelector.set(action.selector, i);
|
|
83739
|
+
}
|
|
83740
|
+
if (toDrop.size === 0)
|
|
83741
|
+
return actions;
|
|
83742
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83743
|
+
}
|
|
83744
|
+
var MAX_DETOUR_LENGTH = 6;
|
|
83745
|
+
function isDetourClosingAction(action) {
|
|
83746
|
+
if (action.type === "BACK" || action.type === "HOME")
|
|
83747
|
+
return true;
|
|
83748
|
+
return isNavigationChromeTap(action);
|
|
83749
|
+
}
|
|
83750
|
+
function isEvidenceAction(action) {
|
|
83751
|
+
return action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "WAIT_FOR_ELEMENT";
|
|
83752
|
+
}
|
|
83753
|
+
function elideNoOpDetours(actions) {
|
|
83754
|
+
if (actions.length < 2)
|
|
83755
|
+
return actions;
|
|
83756
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83757
|
+
for (let j = 0; j < actions.length; j++) {
|
|
83758
|
+
if (toDrop.has(j))
|
|
83759
|
+
continue;
|
|
83760
|
+
if (!isDetourClosingAction(actions[j]))
|
|
83761
|
+
continue;
|
|
83762
|
+
const keyAfter = actions[j].metadata.structuralKey;
|
|
83763
|
+
if (!keyAfter)
|
|
83764
|
+
continue;
|
|
83765
|
+
const next = actions[j + 1];
|
|
83766
|
+
if (next && isEvidenceAction(next))
|
|
83767
|
+
continue;
|
|
83768
|
+
let bestStart = null;
|
|
83769
|
+
let leftScreen = false;
|
|
83770
|
+
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83771
|
+
for (let i = j; i >= earliestStart; i--) {
|
|
83772
|
+
if (i < j) {
|
|
83773
|
+
const inside = actions[i];
|
|
83774
|
+
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83775
|
+
break;
|
|
83776
|
+
}
|
|
83777
|
+
const insideKey = inside.metadata.structuralKey;
|
|
83778
|
+
if (insideKey && insideKey !== keyAfter)
|
|
83779
|
+
leftScreen = true;
|
|
83780
|
+
}
|
|
83781
|
+
if (i === 0)
|
|
83782
|
+
continue;
|
|
83783
|
+
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83784
|
+
if (i < j && leftScreen && keyBefore && keyBefore === keyAfter) {
|
|
83785
|
+
bestStart = i;
|
|
83786
|
+
}
|
|
83787
|
+
}
|
|
83788
|
+
if (bestStart !== null) {
|
|
83789
|
+
for (let k = bestStart; k <= j; k++)
|
|
83790
|
+
toDrop.add(k);
|
|
83791
|
+
}
|
|
83792
|
+
}
|
|
83793
|
+
if (toDrop.size === 0)
|
|
83794
|
+
return actions;
|
|
83795
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83796
|
+
}
|
|
83361
83797
|
function mobileActionsToPseudoPlaywright(actions) {
|
|
83362
83798
|
const escape = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
83363
83799
|
const lines = ["test('mobile', async ({ page }) => {"];
|
|
@@ -83445,6 +83881,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83445
83881
|
if (actions.length < beforeCycleCollapse) {
|
|
83446
83882
|
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
83447
83883
|
}
|
|
83884
|
+
const beforeTypeCoalesce = actions;
|
|
83885
|
+
actions = coalesceSameFieldTypes(actions);
|
|
83886
|
+
logWanderDiff("coalesceSameFieldTypes", beforeTypeCoalesce, actions, log2);
|
|
83887
|
+
const beforeTapCoalesce = actions;
|
|
83888
|
+
actions = coalesceRepeatedTaps(actions);
|
|
83889
|
+
logWanderDiff("coalesceRepeatedTaps", beforeTapCoalesce, actions, log2);
|
|
83890
|
+
const beforeDetourElision = actions;
|
|
83891
|
+
actions = elideNoOpDetours(actions);
|
|
83892
|
+
logWanderDiff("elideNoOpDetours", beforeDetourElision, actions, log2);
|
|
83448
83893
|
const beforeContainerDrop = actions.length;
|
|
83449
83894
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
83450
83895
|
if (actions.length < beforeContainerDrop) {
|
|
@@ -83629,48 +84074,29 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83629
84074
|
log2(" app data reset skipped by policy \u2014 scenario records after relaunch with preserved data.");
|
|
83630
84075
|
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: false, coldResetUnavailable: false, refuseGenerate: false, reason: "preserve_policy" }, log2);
|
|
83631
84076
|
}
|
|
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);
|
|
84077
|
+
const appDataCleared = await driver.resetAppState?.().catch((err) => {
|
|
84078
|
+
log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
84079
|
+
return false;
|
|
84080
|
+
}) ?? false;
|
|
84081
|
+
if (appDataCleared) {
|
|
84082
|
+
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
|
|
83657
84083
|
}
|
|
83658
84084
|
await driver.relaunch();
|
|
83659
84085
|
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
83660
|
-
log2(" CLEAR_APP_DATA required but iOS
|
|
84086
|
+
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
84087
|
return {
|
|
83662
84088
|
appDataCleared: false,
|
|
83663
84089
|
coldResetUnavailable: true,
|
|
83664
84090
|
refuseGenerate: true,
|
|
83665
|
-
reason: "ios_clear_app_data_unavailable"
|
|
84091
|
+
reason: platform3 === "IOS" ? "ios_clear_app_data_unavailable" : "clear_app_data_unavailable"
|
|
83666
84092
|
};
|
|
83667
84093
|
}
|
|
83668
|
-
log2(" iOS
|
|
84094
|
+
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
84095
|
return applyScenarioEntryDeepLink(driver, target, {
|
|
83670
84096
|
appDataCleared: false,
|
|
83671
84097
|
coldResetUnavailable: true,
|
|
83672
84098
|
refuseGenerate: false,
|
|
83673
|
-
reason: "ios_no_clear_app"
|
|
84099
|
+
reason: platform3 === "IOS" ? "ios_no_clear_app" : platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
|
|
83674
84100
|
}, log2);
|
|
83675
84101
|
}
|
|
83676
84102
|
async function runMobileGeneratePhase(args) {
|
|
@@ -84273,6 +84699,8 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
84273
84699
|
"use strict";
|
|
84274
84700
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
84275
84701
|
exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = void 0;
|
|
84702
|
+
exports2.timed = timed;
|
|
84703
|
+
exports2.timedSync = timedSync;
|
|
84276
84704
|
exports2.executeMobileScriptDrivenHeal = executeMobileScriptDrivenHeal;
|
|
84277
84705
|
exports2.executeMobileTestWithHealing = executeMobileTestWithHealing2;
|
|
84278
84706
|
exports2.executeMobileLiveHeal = executeMobileLiveHeal2;
|
|
@@ -84482,6 +84910,32 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84482
84910
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
84483
84911
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
84484
84912
|
}
|
|
84913
|
+
function formatHealTimingLine(category, label, ms, failed, errMessage) {
|
|
84914
|
+
const base2 = `\u{1FA7A}\u23F1 [${category}] ${label} \u2014 ${ms}ms`;
|
|
84915
|
+
return failed ? `${base2} (FAILED: ${errMessage ?? "unknown error"})` : base2;
|
|
84916
|
+
}
|
|
84917
|
+
async function timed(category, label, onProgress, fn) {
|
|
84918
|
+
const start = Date.now();
|
|
84919
|
+
try {
|
|
84920
|
+
const result = await fn();
|
|
84921
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84922
|
+
return result;
|
|
84923
|
+
} catch (err) {
|
|
84924
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84925
|
+
throw err;
|
|
84926
|
+
}
|
|
84927
|
+
}
|
|
84928
|
+
function timedSync(category, label, onProgress, fn) {
|
|
84929
|
+
const start = Date.now();
|
|
84930
|
+
try {
|
|
84931
|
+
const result = fn();
|
|
84932
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84933
|
+
return result;
|
|
84934
|
+
} catch (err) {
|
|
84935
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84936
|
+
throw err;
|
|
84937
|
+
}
|
|
84938
|
+
}
|
|
84485
84939
|
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
84486
84940
|
function isIdentityEqualityXpath(value) {
|
|
84487
84941
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
@@ -84699,21 +85153,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84699
85153
|
return JSON.stringify(base2?.target) !== JSON.stringify(healed.target);
|
|
84700
85154
|
}
|
|
84701
85155
|
function findMissingChangedLocators(baseSteps, healedSteps, screen) {
|
|
84702
|
-
const
|
|
85156
|
+
const results = [];
|
|
84703
85157
|
healedSteps.forEach((healed, index) => {
|
|
84704
85158
|
if (!stepTargetChanged(baseSteps[index], healed))
|
|
84705
85159
|
return;
|
|
84706
85160
|
const candidate = stepToLocatorCandidate(healed);
|
|
84707
85161
|
if (!candidate)
|
|
84708
85162
|
return;
|
|
84709
|
-
|
|
84710
|
-
|
|
84711
|
-
|
|
84712
|
-
|
|
84713
|
-
});
|
|
85163
|
+
const matchCount = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen).length;
|
|
85164
|
+
const label = healed.target?.primary ?? JSON.stringify(candidate);
|
|
85165
|
+
const expressions = stepIdentityExpressions(healed);
|
|
85166
|
+
if (matchCount === 0) {
|
|
85167
|
+
results.push({ label, expressions, kind: "missing" });
|
|
85168
|
+
} else if (matchCount > 1) {
|
|
85169
|
+
results.push({ label, expressions, kind: "ambiguous", matchCount });
|
|
84714
85170
|
}
|
|
84715
85171
|
});
|
|
84716
|
-
return
|
|
85172
|
+
return results;
|
|
84717
85173
|
}
|
|
84718
85174
|
function changedStepExpressions(baseSteps, healedSteps) {
|
|
84719
85175
|
const out = [];
|
|
@@ -84942,10 +85398,10 @@ ${args.directive}`);
|
|
|
84942
85398
|
let content;
|
|
84943
85399
|
let finishReason;
|
|
84944
85400
|
try {
|
|
84945
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
85401
|
+
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
85402
|
arm();
|
|
84947
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
84948
|
-
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
85403
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85404
|
+
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true }));
|
|
84949
85405
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84950
85406
|
finishReason = response?.choices?.[0]?.finish_reason ?? void 0;
|
|
84951
85407
|
} catch {
|
|
@@ -84988,14 +85444,18 @@ ${args.directive}`);
|
|
|
84988
85444
|
continue;
|
|
84989
85445
|
}
|
|
84990
85446
|
if (activeFailingSource && isLocatorNotFound(activeError) && !isDrift(activeError)) {
|
|
84991
|
-
const
|
|
84992
|
-
const missing =
|
|
85447
|
+
const failingSourceSnapshot = activeFailingSource;
|
|
85448
|
+
const missing = timedSync("device-op", `phase1-attempt-${attempt}-static-verify`, options.onProgress, () => {
|
|
85449
|
+
const screen = (0, mobile_source_parser_js_1.parseMobileSource)(failingSourceSnapshot, platform3);
|
|
85450
|
+
return findMissingChangedLocators(baseSteps, healedSteps, screen);
|
|
85451
|
+
});
|
|
84993
85452
|
if (missing.length > 0) {
|
|
84994
85453
|
for (const entry of missing)
|
|
84995
85454
|
pushUnique(triedLocators, entry.expressions);
|
|
84996
85455
|
markRecoverableTurn();
|
|
84997
85456
|
lastVerdict = "fix";
|
|
84998
|
-
|
|
85457
|
+
const first = missing[0];
|
|
85458
|
+
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
85459
|
continue;
|
|
85000
85460
|
}
|
|
85001
85461
|
}
|
|
@@ -85009,9 +85469,10 @@ ${args.directive}`);
|
|
|
85009
85469
|
pendingDirective = "The proposed fix could not be device-verified here. If it is not clearly correct, revise it.";
|
|
85010
85470
|
continue;
|
|
85011
85471
|
}
|
|
85472
|
+
const runNativeTest = options.runNativeTest;
|
|
85012
85473
|
let verify;
|
|
85013
85474
|
try {
|
|
85014
|
-
verify = await options.runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables });
|
|
85475
|
+
verify = await timed("reexec", `phase1-attempt-${attempt}-device-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables }));
|
|
85015
85476
|
} catch {
|
|
85016
85477
|
return {
|
|
85017
85478
|
passed: false,
|
|
@@ -85118,6 +85579,9 @@ ${args.directive}`);
|
|
|
85118
85579
|
}
|
|
85119
85580
|
}
|
|
85120
85581
|
async function executeMobileTestWithHealing2(steps, target, ctx, options) {
|
|
85582
|
+
return timed("total", "heal-total", options.onProgress, () => executeMobileTestWithHealingImpl(steps, target, ctx, options));
|
|
85583
|
+
}
|
|
85584
|
+
async function executeMobileTestWithHealingImpl(steps, target, ctx, options) {
|
|
85121
85585
|
const invalidAuthSetup = detectInvalidMobileAuthSetup(steps, ctx.error);
|
|
85122
85586
|
if (invalidAuthSetup) {
|
|
85123
85587
|
return { passed: false, verdict: "test_invalid", healReason: invalidAuthSetup };
|
|
@@ -85244,9 +85708,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85244
85708
|
const candidate = partsToCandidate(parts);
|
|
85245
85709
|
if (!hasIdentitySignal(candidate))
|
|
85246
85710
|
return;
|
|
85247
|
-
const
|
|
85248
|
-
|
|
85249
|
-
|
|
85711
|
+
const matches = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen);
|
|
85712
|
+
const target = matches.find((el) => el.bounds.width > 0 && el.bounds.height > 0) ?? matches[0];
|
|
85713
|
+
if (target)
|
|
85714
|
+
await driver.tap(target.bounds);
|
|
85250
85715
|
}
|
|
85251
85716
|
function buildLiveUserContent(args) {
|
|
85252
85717
|
const parts = [];
|
|
@@ -85311,7 +85776,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85311
85776
|
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
85312
85777
|
let snap;
|
|
85313
85778
|
try {
|
|
85314
|
-
snap = await liveDriver.snapshot();
|
|
85779
|
+
snap = await timed("device-op", `phase2-pass-${pass}-snapshot`, options.onProgress, () => liveDriver.snapshot());
|
|
85315
85780
|
} catch {
|
|
85316
85781
|
return {
|
|
85317
85782
|
passed: false,
|
|
@@ -85337,10 +85802,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85337
85802
|
];
|
|
85338
85803
|
let content;
|
|
85339
85804
|
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)(() => {
|
|
85805
|
+
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
85806
|
arm();
|
|
85342
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
85343
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
85807
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85808
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true })));
|
|
85344
85809
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
85345
85810
|
} catch {
|
|
85346
85811
|
continue;
|
|
@@ -85372,7 +85837,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85372
85837
|
const healedSteps = parsed.steps;
|
|
85373
85838
|
let verify;
|
|
85374
85839
|
try {
|
|
85375
|
-
verify = await runNativeTest({ steps: healedSteps, target, testVariables });
|
|
85840
|
+
verify = await timed("reexec", `phase2-pass-${pass}-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables }));
|
|
85376
85841
|
} catch {
|
|
85377
85842
|
return {
|
|
85378
85843
|
passed: false,
|
|
@@ -89226,7 +89691,7 @@ var require_kill_tree = __commonJS({
|
|
|
89226
89691
|
var node_child_process_1 = require("child_process");
|
|
89227
89692
|
var node_util_1 = require("util");
|
|
89228
89693
|
var node_fs_1 = require("fs");
|
|
89229
|
-
var
|
|
89694
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
89230
89695
|
var PS_TIMEOUT_MS = 2e3;
|
|
89231
89696
|
var PS_BUFFER = 1024 * 1024;
|
|
89232
89697
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -89280,7 +89745,7 @@ var require_kill_tree = __commonJS({
|
|
|
89280
89745
|
timer.unref?.();
|
|
89281
89746
|
let stdout;
|
|
89282
89747
|
try {
|
|
89283
|
-
const result = await
|
|
89748
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89284
89749
|
maxBuffer: PS_BUFFER,
|
|
89285
89750
|
signal: controller.signal
|
|
89286
89751
|
});
|
|
@@ -89318,7 +89783,7 @@ var require_kill_tree = __commonJS({
|
|
|
89318
89783
|
timer.unref?.();
|
|
89319
89784
|
let stdout;
|
|
89320
89785
|
try {
|
|
89321
|
-
const result = await
|
|
89786
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89322
89787
|
maxBuffer: PS_BUFFER,
|
|
89323
89788
|
signal: controller.signal
|
|
89324
89789
|
});
|
|
@@ -89395,7 +89860,7 @@ var require_kill_tree = __commonJS({
|
|
|
89395
89860
|
timer.unref?.();
|
|
89396
89861
|
let stdout;
|
|
89397
89862
|
try {
|
|
89398
|
-
const result = await
|
|
89863
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid="], {
|
|
89399
89864
|
maxBuffer: PS_BUFFER,
|
|
89400
89865
|
signal: controller.signal
|
|
89401
89866
|
});
|
|
@@ -99274,8 +99739,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99274
99739
|
onRetry: (attempt, reason, delayMs) => logs2.push(` \u26A0 scenario-gen AI call retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
99275
99740
|
});
|
|
99276
99741
|
} catch (aiErr) {
|
|
99277
|
-
const
|
|
99278
|
-
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${
|
|
99742
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
99743
|
+
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg3}`);
|
|
99279
99744
|
throw (0, ai_retry_js_1.markAIProviderError)(aiErr);
|
|
99280
99745
|
}
|
|
99281
99746
|
const iterDuration = Date.now() - iterStart;
|
|
@@ -99318,8 +99783,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99318
99783
|
try {
|
|
99319
99784
|
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
99320
99785
|
} catch (parseErr) {
|
|
99321
|
-
const
|
|
99322
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content:
|
|
99786
|
+
const errMsg3 = `Invalid tool arguments: ${parseErr.message}`;
|
|
99787
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg3 });
|
|
99323
99788
|
continue;
|
|
99324
99789
|
}
|
|
99325
99790
|
if (toolName === "finish_generate") {
|
|
@@ -100577,7 +101042,7 @@ var require_util4 = __commonJS({
|
|
|
100577
101042
|
}
|
|
100578
101043
|
path = url.path;
|
|
100579
101044
|
}
|
|
100580
|
-
var
|
|
101045
|
+
var isAbsolute2 = exports2.isAbsolute(path);
|
|
100581
101046
|
var parts = path.split(/\/+/);
|
|
100582
101047
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
100583
101048
|
part = parts[i];
|
|
@@ -100597,7 +101062,7 @@ var require_util4 = __commonJS({
|
|
|
100597
101062
|
}
|
|
100598
101063
|
path = parts.join("/");
|
|
100599
101064
|
if (path === "") {
|
|
100600
|
-
path =
|
|
101065
|
+
path = isAbsolute2 ? "/" : ".";
|
|
100601
101066
|
}
|
|
100602
101067
|
if (url) {
|
|
100603
101068
|
url.path = path;
|
|
@@ -100606,7 +101071,7 @@ var require_util4 = __commonJS({
|
|
|
100606
101071
|
return path;
|
|
100607
101072
|
}
|
|
100608
101073
|
exports2.normalize = normalize;
|
|
100609
|
-
function
|
|
101074
|
+
function join8(aRoot, aPath) {
|
|
100610
101075
|
if (aRoot === "") {
|
|
100611
101076
|
aRoot = ".";
|
|
100612
101077
|
}
|
|
@@ -100638,7 +101103,7 @@ var require_util4 = __commonJS({
|
|
|
100638
101103
|
}
|
|
100639
101104
|
return joined;
|
|
100640
101105
|
}
|
|
100641
|
-
exports2.join =
|
|
101106
|
+
exports2.join = join8;
|
|
100642
101107
|
exports2.isAbsolute = function(aPath) {
|
|
100643
101108
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
100644
101109
|
};
|
|
@@ -100811,7 +101276,7 @@ var require_util4 = __commonJS({
|
|
|
100811
101276
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
100812
101277
|
}
|
|
100813
101278
|
}
|
|
100814
|
-
sourceURL =
|
|
101279
|
+
sourceURL = join8(urlGenerate(parsed), sourceURL);
|
|
100815
101280
|
}
|
|
100816
101281
|
return normalize(sourceURL);
|
|
100817
101282
|
}
|
|
@@ -314524,8 +314989,8 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314524
314989
|
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel)
|
|
314525
314990
|
})), { label: `script-heal-${attempt}` });
|
|
314526
314991
|
} catch (aiErr) {
|
|
314527
|
-
const
|
|
314528
|
-
logs2.push(`\u26A0 AI call failed: ${
|
|
314992
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
314993
|
+
logs2.push(`\u26A0 AI call failed: ${errMsg3}`);
|
|
314529
314994
|
attemptHistory.push({
|
|
314530
314995
|
phase: "script",
|
|
314531
314996
|
attempt,
|
|
@@ -314533,13 +314998,13 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314533
314998
|
passed: false,
|
|
314534
314999
|
changedCode: false,
|
|
314535
315000
|
summary: "AI call failed before returning a heal result",
|
|
314536
|
-
errorSummary: truncateForPrompt(
|
|
315001
|
+
errorSummary: truncateForPrompt(errMsg3),
|
|
314537
315002
|
failedStep: summarizeFailedStep(currentStepResults)
|
|
314538
315003
|
});
|
|
314539
315004
|
if (feedbackEnabled) {
|
|
314540
|
-
lastTransportFailure = truncateForPrompt(
|
|
315005
|
+
lastTransportFailure = truncateForPrompt(errMsg3, 200) ?? "the AI request failed";
|
|
314541
315006
|
} else {
|
|
314542
|
-
currentError = `AI call failed on attempt ${attempt}: ${
|
|
315007
|
+
currentError = `AI call failed on attempt ${attempt}: ${errMsg3}`;
|
|
314543
315008
|
}
|
|
314544
315009
|
markRecoverableTurn();
|
|
314545
315010
|
continue;
|
|
@@ -314823,8 +315288,8 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314823
315288
|
// item 5b
|
|
314824
315289
|
});
|
|
314825
315290
|
} catch (runErr) {
|
|
314826
|
-
const
|
|
314827
|
-
logs2.push(`\u{1F4A5} Native execution threw: ${
|
|
315291
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
315292
|
+
logs2.push(`\u{1F4A5} Native execution threw: ${errMsg3}`);
|
|
314828
315293
|
attemptHistory.push({
|
|
314829
315294
|
phase: "script",
|
|
314830
315295
|
attempt,
|
|
@@ -314832,9 +315297,9 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314832
315297
|
passed: false,
|
|
314833
315298
|
changedCode: true,
|
|
314834
315299
|
summary: truncateForPrompt(changeDesc) ?? "Applied code fix, but native execution threw before verification completed",
|
|
314835
|
-
errorSummary: truncateForPrompt(
|
|
315300
|
+
errorSummary: truncateForPrompt(errMsg3)
|
|
314836
315301
|
});
|
|
314837
|
-
currentError = `Native execution threw: ${
|
|
315302
|
+
currentError = `Native execution threw: ${errMsg3}`;
|
|
314838
315303
|
currentStepResults = [];
|
|
314839
315304
|
continue;
|
|
314840
315305
|
}
|
|
@@ -315590,9 +316055,9 @@ Last error (after heal attempts): ${lastErrorSnippet}` : `Heal aborted: same err
|
|
|
315590
316055
|
previousErrorSignature = errorSig;
|
|
315591
316056
|
}
|
|
315592
316057
|
} catch (runErr) {
|
|
315593
|
-
const
|
|
315594
|
-
logs2.push(`\u{1F4A5} Native run threw: ${
|
|
315595
|
-
currentError =
|
|
316058
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
316059
|
+
logs2.push(`\u{1F4A5} Native run threw: ${errMsg3}`);
|
|
316060
|
+
currentError = errMsg3;
|
|
315596
316061
|
currentStepResults = [];
|
|
315597
316062
|
}
|
|
315598
316063
|
} else {
|
|
@@ -316154,8 +316619,8 @@ When you call finish_fix, preserve locators only when they are stable structural
|
|
|
316154
316619
|
}
|
|
316155
316620
|
}
|
|
316156
316621
|
} catch (toolErr) {
|
|
316157
|
-
const
|
|
316158
|
-
let errorContent = `Error: ${
|
|
316622
|
+
const errMsg3 = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
316623
|
+
let errorContent = `Error: ${errMsg3}`;
|
|
316159
316624
|
try {
|
|
316160
316625
|
const consoleResult = await mcpClient.callTool({ name: "browser_console_messages", arguments: {} });
|
|
316161
316626
|
const consoleText = Array.isArray(consoleResult.content) ? consoleResult.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n") : String(consoleResult.content ?? "");
|
|
@@ -316176,8 +316641,8 @@ ${errorContent}`;
|
|
|
316176
316641
|
}
|
|
316177
316642
|
}
|
|
316178
316643
|
} catch (mcpErr) {
|
|
316179
|
-
const
|
|
316180
|
-
logs2.push(`\u{1F4A5} MCP exploration failed: ${
|
|
316644
|
+
const errMsg3 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
|
|
316645
|
+
logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg3}`);
|
|
316181
316646
|
}
|
|
316182
316647
|
if (verdict === "app_bug") {
|
|
316183
316648
|
phase2AttemptHistory.push({
|
|
@@ -316771,9 +317236,9 @@ ${testBlocks}
|
|
|
316771
317236
|
}
|
|
316772
317237
|
emit(` \u2713 ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
316773
317238
|
} catch (err) {
|
|
316774
|
-
const
|
|
316775
|
-
toolResultContent = `ERROR: ${
|
|
316776
|
-
emit(` \u2717 ${
|
|
317239
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
317240
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
317241
|
+
emit(` \u2717 ${errMsg3}`);
|
|
316777
317242
|
}
|
|
316778
317243
|
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
|
|
316779
317244
|
}
|
|
@@ -323057,8 +323522,8 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
323057
323522
|
const snapItems = Array.isArray(snapResult.content) ? snapResult.content : [];
|
|
323058
323523
|
snapshotText = extractInlineSnapshotYaml(snapItems.find((c) => c.type === "text")?.text || null);
|
|
323059
323524
|
} catch (err) {
|
|
323060
|
-
const
|
|
323061
|
-
const isTimeout = /timeout|timed out|TimeoutError/i.test(
|
|
323525
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
323526
|
+
const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg3);
|
|
323062
323527
|
return {
|
|
323063
323528
|
snapshotText: null,
|
|
323064
323529
|
meta: {
|
|
@@ -327134,9 +327599,9 @@ ${currentSummary}`;
|
|
|
327134
327599
|
});
|
|
327135
327600
|
}
|
|
327136
327601
|
} catch (err) {
|
|
327137
|
-
const
|
|
327138
|
-
logs2.push(` Error persisting page: ${
|
|
327139
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${
|
|
327602
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327603
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327604
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg3}` });
|
|
327140
327605
|
}
|
|
327141
327606
|
continue;
|
|
327142
327607
|
}
|
|
@@ -327338,12 +327803,12 @@ ${currentSummary}`;
|
|
|
327338
327803
|
})
|
|
327339
327804
|
});
|
|
327340
327805
|
} catch (err) {
|
|
327341
|
-
const
|
|
327342
|
-
logs2.push(` Error persisting page: ${
|
|
327806
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327807
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327343
327808
|
toolResults.push({
|
|
327344
327809
|
role: "tool",
|
|
327345
327810
|
tool_call_id: toolCall.id,
|
|
327346
|
-
content: `ERROR persisting page: ${
|
|
327811
|
+
content: `ERROR persisting page: ${errMsg3}`
|
|
327347
327812
|
});
|
|
327348
327813
|
}
|
|
327349
327814
|
continue;
|
|
@@ -327378,12 +327843,12 @@ ${currentSummary}`;
|
|
|
327378
327843
|
content: JSON.stringify({ acknowledged: true })
|
|
327379
327844
|
});
|
|
327380
327845
|
} catch (err) {
|
|
327381
|
-
const
|
|
327382
|
-
logs2.push(` Error persisting transition: ${
|
|
327846
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327847
|
+
logs2.push(` Error persisting transition: ${errMsg3}`);
|
|
327383
327848
|
toolResults.push({
|
|
327384
327849
|
role: "tool",
|
|
327385
327850
|
tool_call_id: toolCall.id,
|
|
327386
|
-
content: `ERROR persisting transition: ${
|
|
327851
|
+
content: `ERROR persisting transition: ${errMsg3}`
|
|
327387
327852
|
});
|
|
327388
327853
|
}
|
|
327389
327854
|
continue;
|
|
@@ -328079,15 +328544,15 @@ Exploration complete: ${summary}`);
|
|
|
328079
328544
|
toolResultContent = (0, snapshot_context_compactor_js_1.formatMCPToolResult)(toolResultContent, MAX_TOOL_RESULT_CHARS);
|
|
328080
328545
|
logs2.push(` OK: ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
328081
328546
|
} catch (err) {
|
|
328082
|
-
const
|
|
328083
|
-
toolResultContent = `ERROR: ${
|
|
328084
|
-
logs2.push(` Error: ${
|
|
328547
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
328548
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
328549
|
+
logs2.push(` Error: ${errMsg3}`);
|
|
328085
328550
|
if (useV2) {
|
|
328086
328551
|
(0, exploration_state_js_1.reduceEvent)(state2, {
|
|
328087
328552
|
type: "ACTION_FAILED",
|
|
328088
328553
|
route: state2.currentRoute,
|
|
328089
328554
|
action: `${toolName}(${JSON.stringify(toolArgs).slice(0, 100)})`,
|
|
328090
|
-
error:
|
|
328555
|
+
error: errMsg3
|
|
328091
328556
|
});
|
|
328092
328557
|
}
|
|
328093
328558
|
}
|
|
@@ -330936,10 +331401,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
330936
331401
|
onLog: options.onLog,
|
|
330937
331402
|
onProvenCommands: recordProvenCommands
|
|
330938
331403
|
});
|
|
330939
|
-
const
|
|
331404
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
330940
331405
|
const fixPrompt = [
|
|
330941
331406
|
"Your previous attempt did not produce a valid result file.",
|
|
330942
|
-
`Error: ${
|
|
331407
|
+
`Error: ${errMsg3}`,
|
|
330943
331408
|
`Re-read BRIEF.md and write a valid SurveyorOutput JSON to ${resultFilePath}.`,
|
|
330944
331409
|
"Single JSON object. No prose. Then exit."
|
|
330945
331410
|
].join("\n");
|
|
@@ -333080,7 +333545,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333080
333545
|
log2(`First-pass discovery-result invalid or missing: ${firstError.message}`);
|
|
333081
333546
|
}
|
|
333082
333547
|
if (!parsedResult) {
|
|
333083
|
-
const
|
|
333548
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
333084
333549
|
if (planDebounceTimerRef.current) {
|
|
333085
333550
|
clearTimeout(planDebounceTimerRef.current);
|
|
333086
333551
|
planDebounceTimerRef.current = void 0;
|
|
@@ -333105,7 +333570,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333105
333570
|
""
|
|
333106
333571
|
].join("\n");
|
|
333107
333572
|
const fixPrompt = [
|
|
333108
|
-
`Your previous attempt did not produce a valid result file. Error: ${
|
|
333573
|
+
`Your previous attempt did not produce a valid result file. Error: ${errMsg3}`,
|
|
333109
333574
|
"Re-read BRIEF.md.",
|
|
333110
333575
|
phaseContext,
|
|
333111
333576
|
`Continue from where you left off \u2014 already verified tests are in ${testsDirPath}/`,
|
|
@@ -334734,10 +335199,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
334734
335199
|
onLog: parserLog,
|
|
334735
335200
|
onProvenCommands: recordProvenCommands
|
|
334736
335201
|
});
|
|
334737
|
-
const
|
|
335202
|
+
const errMsg3 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
|
|
334738
335203
|
const fixPrompt = [
|
|
334739
335204
|
"Your previous attempt did not leave a valid result file.",
|
|
334740
|
-
`Error: ${
|
|
335205
|
+
`Error: ${errMsg3}`,
|
|
334741
335206
|
`Writing a valid SurveyorOutput JSON object to ${resultFilePath} is your ONLY priority now.`,
|
|
334742
335207
|
"Procedure: navigate to at most 6-8 of the main pages, then IMMEDIATELY use",
|
|
334743
335208
|
`the Write tool to save the SurveyorOutput JSON to ${resultFilePath}. Do this`,
|
|
@@ -338618,8 +339083,8 @@ var require_resource_governor = __commonJS({
|
|
|
338618
339083
|
function isDiscoveryLike(mode) {
|
|
338619
339084
|
return mode === "discovery";
|
|
338620
339085
|
}
|
|
338621
|
-
function evaluateRunAdmission2(options, request) {
|
|
338622
|
-
const snapshot = getResourceSnapshot2();
|
|
339086
|
+
function evaluateRunAdmission2(options, request, snapshotOverride) {
|
|
339087
|
+
const snapshot = snapshotOverride ?? getResourceSnapshot2();
|
|
338623
339088
|
const candidateWeight = modeWeight(request.mode, options.weights);
|
|
338624
339089
|
const currentUnits = activeUnits(request.activeByType, options.weights);
|
|
338625
339090
|
const memory = pressureFactor(snapshot.memoryPct, options.memorySoftPct, options.memoryHardPct);
|
|
@@ -341117,8 +341582,8 @@ var require_adm_zip = __commonJS({
|
|
|
341117
341582
|
return null;
|
|
341118
341583
|
}
|
|
341119
341584
|
function fixPath(zipPath) {
|
|
341120
|
-
const { join:
|
|
341121
|
-
return
|
|
341585
|
+
const { join: join8, normalize, sep } = pth.posix;
|
|
341586
|
+
return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
|
|
341122
341587
|
}
|
|
341123
341588
|
function filenameFilter(filterfn) {
|
|
341124
341589
|
if (filterfn instanceof RegExp) {
|
|
@@ -343953,11 +344418,11 @@ var require_winchars = __commonJS({
|
|
|
343953
344418
|
var require_strip_absolute_path = __commonJS({
|
|
343954
344419
|
"../../node_modules/tar/lib/strip-absolute-path.js"(exports2, module2) {
|
|
343955
344420
|
"use strict";
|
|
343956
|
-
var { isAbsolute, parse } = require("path").win32;
|
|
344421
|
+
var { isAbsolute: isAbsolute2, parse } = require("path").win32;
|
|
343957
344422
|
module2.exports = (path) => {
|
|
343958
344423
|
let r = "";
|
|
343959
344424
|
let parsed = parse(path);
|
|
343960
|
-
while (
|
|
344425
|
+
while (isAbsolute2(path) || parsed.root) {
|
|
343961
344426
|
const root = path.charAt(0) === "/" && path.slice(0, 4) !== "//?/" ? "/" : parsed.root;
|
|
343962
344427
|
path = path.slice(root.length);
|
|
343963
344428
|
r += root;
|
|
@@ -346469,7 +346934,7 @@ var require_update = __commonJS({
|
|
|
346469
346934
|
var require_opts_arg = __commonJS({
|
|
346470
346935
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports2, module2) {
|
|
346471
346936
|
"use strict";
|
|
346472
|
-
var { promisify:
|
|
346937
|
+
var { promisify: promisify4 } = require("util");
|
|
346473
346938
|
var fs = require("fs");
|
|
346474
346939
|
var optsArg = (opts) => {
|
|
346475
346940
|
if (!opts)
|
|
@@ -346483,9 +346948,9 @@ var require_opts_arg = __commonJS({
|
|
|
346483
346948
|
else
|
|
346484
346949
|
throw new TypeError("invalid options argument");
|
|
346485
346950
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
346486
|
-
opts.mkdirAsync =
|
|
346951
|
+
opts.mkdirAsync = promisify4(opts.mkdir);
|
|
346487
346952
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
346488
|
-
opts.statAsync =
|
|
346953
|
+
opts.statAsync = promisify4(opts.stat);
|
|
346489
346954
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
346490
346955
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
346491
346956
|
return opts;
|
|
@@ -346738,10 +347203,10 @@ var require_chownr = __commonJS({
|
|
|
346738
347203
|
}
|
|
346739
347204
|
} : (path2, uid, gid) => lchownSync(path2, uid, gid);
|
|
346740
347205
|
var nodeVersion = process.version;
|
|
346741
|
-
var
|
|
347206
|
+
var readdir3 = (path2, options, cb) => fs.readdir(path2, options, cb);
|
|
346742
347207
|
var readdirSync = (path2, options) => fs.readdirSync(path2, options);
|
|
346743
347208
|
if (/^v4\./.test(nodeVersion))
|
|
346744
|
-
|
|
347209
|
+
readdir3 = (path2, options, cb) => fs.readdir(path2, cb);
|
|
346745
347210
|
var chown = (cpath, uid, gid, cb) => {
|
|
346746
347211
|
fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, (er) => {
|
|
346747
347212
|
cb(er && er.code !== "ENOENT" ? er : null);
|
|
@@ -346768,7 +347233,7 @@ var require_chownr = __commonJS({
|
|
|
346768
347233
|
}
|
|
346769
347234
|
};
|
|
346770
347235
|
var chownr = (p, uid, gid, cb) => {
|
|
346771
|
-
|
|
347236
|
+
readdir3(p, { withFileTypes: true }, (er, children) => {
|
|
346772
347237
|
if (er) {
|
|
346773
347238
|
if (er.code === "ENOENT")
|
|
346774
347239
|
return cb();
|
|
@@ -347042,7 +347507,7 @@ var require_path_reservations = __commonJS({
|
|
|
347042
347507
|
var assert = require("assert");
|
|
347043
347508
|
var normalize = require_normalize_unicode();
|
|
347044
347509
|
var stripSlashes = require_strip_trailing_slashes();
|
|
347045
|
-
var { join:
|
|
347510
|
+
var { join: join8 } = require("path");
|
|
347046
347511
|
var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
|
|
347047
347512
|
var isWindows = platform3 === "win32";
|
|
347048
347513
|
module2.exports = () => {
|
|
@@ -347051,7 +347516,7 @@ var require_path_reservations = __commonJS({
|
|
|
347051
347516
|
const getDirs = (path) => {
|
|
347052
347517
|
const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
|
|
347053
347518
|
if (set.length) {
|
|
347054
|
-
path2 =
|
|
347519
|
+
path2 = join8(set[set.length - 1], path2);
|
|
347055
347520
|
}
|
|
347056
347521
|
set.push(path2 || "/");
|
|
347057
347522
|
return set;
|
|
@@ -347119,7 +347584,7 @@ var require_path_reservations = __commonJS({
|
|
|
347119
347584
|
};
|
|
347120
347585
|
const reserve = (paths, fn) => {
|
|
347121
347586
|
paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
|
|
347122
|
-
return stripSlashes(
|
|
347587
|
+
return stripSlashes(join8(normalize(p))).toLowerCase();
|
|
347123
347588
|
});
|
|
347124
347589
|
const dirs = new Set(
|
|
347125
347590
|
paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
|
|
@@ -347174,7 +347639,7 @@ var require_unpack = __commonJS({
|
|
|
347174
347639
|
var fs = require("fs");
|
|
347175
347640
|
var fsm = require_fs_minipass();
|
|
347176
347641
|
var path = require("path");
|
|
347177
|
-
var
|
|
347642
|
+
var mkdir3 = require_mkdir();
|
|
347178
347643
|
var wc = require_winchars();
|
|
347179
347644
|
var pathReservations = require_path_reservations();
|
|
347180
347645
|
var stripAbsolutePath = require_strip_absolute_path();
|
|
@@ -347425,7 +347890,7 @@ var require_unpack = __commonJS({
|
|
|
347425
347890
|
}
|
|
347426
347891
|
}
|
|
347427
347892
|
[MKDIR](dir, mode, cb) {
|
|
347428
|
-
|
|
347893
|
+
mkdir3(normPath(dir), {
|
|
347429
347894
|
uid: this.uid,
|
|
347430
347895
|
gid: this.gid,
|
|
347431
347896
|
processUid: this.processUid,
|
|
@@ -347828,7 +348293,7 @@ var require_unpack = __commonJS({
|
|
|
347828
348293
|
}
|
|
347829
348294
|
[MKDIR](dir, mode) {
|
|
347830
348295
|
try {
|
|
347831
|
-
return
|
|
348296
|
+
return mkdir3.sync(normPath(dir), {
|
|
347832
348297
|
uid: this.uid,
|
|
347833
348298
|
gid: this.gid,
|
|
347834
348299
|
processUid: this.processUid,
|
|
@@ -351334,7 +351799,7 @@ var require_process_inventory = __commonJS({
|
|
|
351334
351799
|
var node_child_process_1 = require("child_process");
|
|
351335
351800
|
var node_os_1 = __importDefault(require("os"));
|
|
351336
351801
|
var node_util_1 = require("util");
|
|
351337
|
-
var
|
|
351802
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
351338
351803
|
var DEFAULT_LIMIT = 60;
|
|
351339
351804
|
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
351340
351805
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -351396,7 +351861,7 @@ var require_process_inventory = __commonJS({
|
|
|
351396
351861
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
351397
351862
|
timer.unref?.();
|
|
351398
351863
|
try {
|
|
351399
|
-
const { stdout } = await
|
|
351864
|
+
const { stdout } = await execFileAsync4(command, args, {
|
|
351400
351865
|
maxBuffer: 1024 * 1024,
|
|
351401
351866
|
signal: controller.signal
|
|
351402
351867
|
});
|
|
@@ -352542,7 +353007,7 @@ var require_package4 = __commonJS({
|
|
|
352542
353007
|
"package.json"(exports2, module2) {
|
|
352543
353008
|
module2.exports = {
|
|
352544
353009
|
name: "@validate.qa/runner",
|
|
352545
|
-
version: "1.0.
|
|
353010
|
+
version: "1.0.72",
|
|
352546
353011
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
352547
353012
|
bin: {
|
|
352548
353013
|
"validate-runner": "dist/cli.js"
|
|
@@ -352551,7 +353016,7 @@ var require_package4 = __commonJS({
|
|
|
352551
353016
|
build: "tsup",
|
|
352552
353017
|
dev: "tsx src/cli.ts",
|
|
352553
353018
|
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",
|
|
353019
|
+
"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
353020
|
prepublishOnly: "npm run build"
|
|
352556
353021
|
},
|
|
352557
353022
|
files: [
|
|
@@ -352629,7 +353094,7 @@ function getConfigDir() {
|
|
|
352629
353094
|
}
|
|
352630
353095
|
|
|
352631
353096
|
// src/runner.ts
|
|
352632
|
-
var
|
|
353097
|
+
var import_path5 = require("path");
|
|
352633
353098
|
var import_os6 = require("os");
|
|
352634
353099
|
var import_node_crypto2 = require("crypto");
|
|
352635
353100
|
|
|
@@ -353224,6 +353689,69 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
353224
353689
|
};
|
|
353225
353690
|
}
|
|
353226
353691
|
|
|
353692
|
+
// src/services/mobile/ios-simulator-reset.ts
|
|
353693
|
+
var import_child_process2 = require("child_process");
|
|
353694
|
+
var import_util = require("util");
|
|
353695
|
+
var import_promises = require("fs/promises");
|
|
353696
|
+
var import_path3 = require("path");
|
|
353697
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
353698
|
+
var SIMCTL_TIMEOUT_MS = 15e3;
|
|
353699
|
+
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;
|
|
353700
|
+
function isIosSimulatorUdid(udid) {
|
|
353701
|
+
return typeof udid === "string" && SIMULATOR_UDID_RE.test(udid.trim());
|
|
353702
|
+
}
|
|
353703
|
+
function errMsg(err) {
|
|
353704
|
+
return err instanceof Error ? err.message : String(err);
|
|
353705
|
+
}
|
|
353706
|
+
async function simctl(args) {
|
|
353707
|
+
const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
|
|
353708
|
+
timeout: SIMCTL_TIMEOUT_MS,
|
|
353709
|
+
encoding: "utf-8"
|
|
353710
|
+
});
|
|
353711
|
+
return stdout.trim();
|
|
353712
|
+
}
|
|
353713
|
+
async function clearIosSimulatorAppData(udid, bundleId, env = process.env) {
|
|
353714
|
+
if (!isIosSimulatorUdid(udid)) {
|
|
353715
|
+
return { ok: false, detail: `'${udid}' is not a simulator UDID \u2014 physical iOS devices cannot be cleared` };
|
|
353716
|
+
}
|
|
353717
|
+
let container;
|
|
353718
|
+
try {
|
|
353719
|
+
container = await simctl(["get_app_container", udid, bundleId, "data"]);
|
|
353720
|
+
} catch (err) {
|
|
353721
|
+
return { ok: false, detail: `simctl get_app_container failed (app not installed?): ${errMsg(err)}` };
|
|
353722
|
+
}
|
|
353723
|
+
if (!(0, import_path3.isAbsolute)(container) || !container.includes("/Containers/Data/Application/")) {
|
|
353724
|
+
return { ok: false, detail: `unexpected app container path: ${container}` };
|
|
353725
|
+
}
|
|
353726
|
+
try {
|
|
353727
|
+
await simctl(["spawn", udid, "defaults", "delete", bundleId]);
|
|
353728
|
+
} catch {
|
|
353729
|
+
}
|
|
353730
|
+
try {
|
|
353731
|
+
const entries = await (0, import_promises.readdir)(container);
|
|
353732
|
+
for (const entry of entries) {
|
|
353733
|
+
await (0, import_promises.rm)((0, import_path3.join)(container, entry), { recursive: true, force: true });
|
|
353734
|
+
}
|
|
353735
|
+
for (const dir of ["Documents", "Library/Caches", "Library/Preferences", "tmp"]) {
|
|
353736
|
+
await (0, import_promises.mkdir)((0, import_path3.join)(container, dir), { recursive: true });
|
|
353737
|
+
}
|
|
353738
|
+
} catch (err) {
|
|
353739
|
+
return { ok: false, detail: `app container wipe failed: ${errMsg(err)}` };
|
|
353740
|
+
}
|
|
353741
|
+
const keychainPolicy = (env.VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET ?? "").trim().toLowerCase();
|
|
353742
|
+
if (keychainPolicy !== "never") {
|
|
353743
|
+
try {
|
|
353744
|
+
await simctl(["keychain", udid, "reset"]);
|
|
353745
|
+
} catch (err) {
|
|
353746
|
+
return {
|
|
353747
|
+
ok: false,
|
|
353748
|
+
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.`
|
|
353749
|
+
};
|
|
353750
|
+
}
|
|
353751
|
+
}
|
|
353752
|
+
return { ok: true };
|
|
353753
|
+
}
|
|
353754
|
+
|
|
353227
353755
|
// src/services/mobile/appium-actions.ts
|
|
353228
353756
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
353229
353757
|
var APPIUM_WEBVIEW_CONTEXT_TIMEOUT_MS = 12e4;
|
|
@@ -353406,9 +353934,12 @@ async function tapAt(sessionPath, bounds) {
|
|
|
353406
353934
|
}
|
|
353407
353935
|
}
|
|
353408
353936
|
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
353409
|
-
if (platform3 !== "ANDROID") return false;
|
|
353410
353937
|
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
353411
353938
|
if (policy === "never") return false;
|
|
353939
|
+
if (platform3 === "IOS") {
|
|
353940
|
+
return isIosSimulatorUdid(deviceId);
|
|
353941
|
+
}
|
|
353942
|
+
if (platform3 !== "ANDROID") return false;
|
|
353412
353943
|
if (policy === "always") return true;
|
|
353413
353944
|
return !!deviceId && deviceId.startsWith("emulator-");
|
|
353414
353945
|
}
|
|
@@ -354287,6 +354818,45 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354287
354818
|
});
|
|
354288
354819
|
}
|
|
354289
354820
|
|
|
354821
|
+
// src/services/device-quarantine.ts
|
|
354822
|
+
var QUARANTINE_MAX_MS = (() => {
|
|
354823
|
+
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
354824
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354825
|
+
})();
|
|
354826
|
+
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354827
|
+
var DeviceQuarantinedError = class extends Error {
|
|
354828
|
+
constructor(deviceId) {
|
|
354829
|
+
super(
|
|
354830
|
+
`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.`
|
|
354831
|
+
);
|
|
354832
|
+
this.name = "DeviceQuarantinedError";
|
|
354833
|
+
}
|
|
354834
|
+
};
|
|
354835
|
+
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354836
|
+
quarantinedAt.set(deviceId, now);
|
|
354837
|
+
}
|
|
354838
|
+
function unquarantineDevice(deviceId) {
|
|
354839
|
+
quarantinedAt.delete(deviceId);
|
|
354840
|
+
}
|
|
354841
|
+
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354842
|
+
if (typeof deviceId !== "string") return false;
|
|
354843
|
+
const since = quarantinedAt.get(deviceId);
|
|
354844
|
+
if (since === void 0) return false;
|
|
354845
|
+
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354846
|
+
quarantinedAt.delete(deviceId);
|
|
354847
|
+
console.warn(
|
|
354848
|
+
`[device-quarantine] Quarantine on ${deviceId} exceeded its ${Math.round(QUARANTINE_MAX_MS / 6e4)}min ceiling without the original run settling \u2014 lifting it so the device is usable again. If this recurs, the zombie run is wedged in non-device work (check the runner log).`
|
|
354849
|
+
);
|
|
354850
|
+
return false;
|
|
354851
|
+
}
|
|
354852
|
+
return true;
|
|
354853
|
+
}
|
|
354854
|
+
function assertDeviceNotQuarantined(deviceId, now = Date.now()) {
|
|
354855
|
+
if (isDeviceQuarantined(deviceId, now)) {
|
|
354856
|
+
throw new DeviceQuarantinedError(deviceId);
|
|
354857
|
+
}
|
|
354858
|
+
}
|
|
354859
|
+
|
|
354290
354860
|
// src/services/appium-executor.ts
|
|
354291
354861
|
var import_node_child_process2 = require("child_process");
|
|
354292
354862
|
var import_node_util2 = require("util");
|
|
@@ -354295,11 +354865,11 @@ var import_node_path2 = require("path");
|
|
|
354295
354865
|
var import_runner_core8 = __toESM(require_dist2());
|
|
354296
354866
|
|
|
354297
354867
|
// src/services/device-discovery.ts
|
|
354298
|
-
var
|
|
354868
|
+
var import_child_process3 = require("child_process");
|
|
354299
354869
|
var import_os3 = require("os");
|
|
354300
354870
|
function runCommand(command, args, timeout = 1e4) {
|
|
354301
354871
|
try {
|
|
354302
|
-
return (0,
|
|
354872
|
+
return (0, import_child_process3.execFileSync)(command, args, {
|
|
354303
354873
|
encoding: "utf-8",
|
|
354304
354874
|
timeout,
|
|
354305
354875
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -354599,9 +355169,6 @@ function countReadyDevicesByKind(platform3, snapshot = getCachedDeviceSnapshot()
|
|
|
354599
355169
|
function selectFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(), requirements = {}) {
|
|
354600
355170
|
const ready = snapshot.filter((d) => d.platform === platform3 && d.isAvailable && d.state === "BOOTED");
|
|
354601
355171
|
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
355172
|
let candidates = requirements.resetPolicy === "CLEAR_APP_DATA" ? ready.filter((d) => !d.isPhysical) : [...ready];
|
|
354606
355173
|
const appId = requirements.appId?.trim();
|
|
354607
355174
|
if (appId) {
|
|
@@ -354651,13 +355218,13 @@ function pickFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(),
|
|
|
354651
355218
|
// src/services/mobile/mobile-network-capture.ts
|
|
354652
355219
|
var import_node_child_process = require("child_process");
|
|
354653
355220
|
var import_node_crypto = require("crypto");
|
|
354654
|
-
var
|
|
355221
|
+
var import_promises2 = require("fs/promises");
|
|
354655
355222
|
var import_node_os = require("os");
|
|
354656
355223
|
var import_node_path = require("path");
|
|
354657
355224
|
var import_node_util = require("util");
|
|
354658
355225
|
var import_mockttp = require("mockttp");
|
|
354659
355226
|
var import_runner_core7 = __toESM(require_dist2());
|
|
354660
|
-
var
|
|
355227
|
+
var execFileAsync2 = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
354661
355228
|
var MAX_CALLS = 1e3;
|
|
354662
355229
|
var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
|
|
354663
355230
|
var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
@@ -354845,7 +355412,7 @@ var MitmProxy = class {
|
|
|
354845
355412
|
try {
|
|
354846
355413
|
await server3.stop();
|
|
354847
355414
|
} catch (err) {
|
|
354848
|
-
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${
|
|
355415
|
+
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg2(err)}`);
|
|
354849
355416
|
}
|
|
354850
355417
|
this.onLog?.(
|
|
354851
355418
|
`[mobile-net] proxy closed: ${this.calls.length} calls captured` + (this.dropped > 0 ? ` (${this.dropped} dropped over cap)` : "")
|
|
@@ -354870,7 +355437,7 @@ var MitmProxy = class {
|
|
|
354870
355437
|
isHttps
|
|
354871
355438
|
});
|
|
354872
355439
|
} catch (err) {
|
|
354873
|
-
this.onLog?.(`[mobile-net] request capture error (ignored): ${
|
|
355440
|
+
this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg2(err)}`);
|
|
354874
355441
|
}
|
|
354875
355442
|
}
|
|
354876
355443
|
// ── response → finalize the matched request ───────────────────────────────
|
|
@@ -354913,7 +355480,7 @@ var MitmProxy = class {
|
|
|
354913
355480
|
intercepted: true
|
|
354914
355481
|
});
|
|
354915
355482
|
} catch (err) {
|
|
354916
|
-
this.onLog?.(`[mobile-net] response capture error (ignored): ${
|
|
355483
|
+
this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg2(err)}`);
|
|
354917
355484
|
}
|
|
354918
355485
|
}
|
|
354919
355486
|
// ── tls-client-error → record WHY interception failed ─────────────────────
|
|
@@ -354991,16 +355558,16 @@ async function generateCaCert(dir, onLog) {
|
|
|
354991
355558
|
// rather than on a short expiry.
|
|
354992
355559
|
});
|
|
354993
355560
|
const caCertPath = (0, import_node_path.join)(dir, "validateqa-mobile-ca.pem");
|
|
354994
|
-
await (0,
|
|
355561
|
+
await (0, import_promises2.writeFile)(caCertPath, cert, "utf-8");
|
|
354995
355562
|
return { caCertPath, caKeyPem: key, caCertPem: cert };
|
|
354996
355563
|
} catch (err) {
|
|
354997
|
-
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${
|
|
355564
|
+
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg2(err)}`);
|
|
354998
355565
|
return null;
|
|
354999
355566
|
}
|
|
355000
355567
|
}
|
|
355001
355568
|
async function androidReadProxy(deviceId) {
|
|
355002
355569
|
try {
|
|
355003
|
-
const { stdout } = await
|
|
355570
|
+
const { stdout } = await execFileAsync2(
|
|
355004
355571
|
"adb",
|
|
355005
355572
|
["-s", deviceId, "shell", "settings", "get", "global", "http_proxy"],
|
|
355006
355573
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
@@ -355013,33 +355580,33 @@ async function androidReadProxy(deviceId) {
|
|
|
355013
355580
|
}
|
|
355014
355581
|
async function androidSetProxy(deviceId, hostPort, onLog) {
|
|
355015
355582
|
try {
|
|
355016
|
-
await
|
|
355583
|
+
await execFileAsync2(
|
|
355017
355584
|
"adb",
|
|
355018
355585
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", hostPort],
|
|
355019
355586
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355020
355587
|
);
|
|
355021
355588
|
return true;
|
|
355022
355589
|
} catch (err) {
|
|
355023
|
-
onLog?.(`[mobile-net] adb set http_proxy failed: ${
|
|
355590
|
+
onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg2(err)}`);
|
|
355024
355591
|
return false;
|
|
355025
355592
|
}
|
|
355026
355593
|
}
|
|
355027
355594
|
async function androidClearProxy(deviceId, originalProxy, onLog) {
|
|
355028
355595
|
const restoreValue = originalProxy && originalProxy.length > 0 ? originalProxy : ":0";
|
|
355029
355596
|
try {
|
|
355030
|
-
await
|
|
355597
|
+
await execFileAsync2(
|
|
355031
355598
|
"adb",
|
|
355032
355599
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", restoreValue],
|
|
355033
355600
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355034
355601
|
);
|
|
355035
355602
|
} catch (err) {
|
|
355036
|
-
onLog?.(`[mobile-net] adb restore http_proxy failed: ${
|
|
355603
|
+
onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg2(err)}`);
|
|
355037
355604
|
}
|
|
355038
355605
|
}
|
|
355039
355606
|
async function androidInstallCa(deviceId, caCertPath) {
|
|
355040
355607
|
const devicePath = "/sdcard/validateqa-mobile-ca.pem";
|
|
355041
355608
|
try {
|
|
355042
|
-
await
|
|
355609
|
+
await execFileAsync2("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
|
|
355043
355610
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355044
355611
|
});
|
|
355045
355612
|
return {
|
|
@@ -355047,23 +355614,23 @@ async function androidInstallCa(deviceId, caCertPath) {
|
|
|
355047
355614
|
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
355615
|
};
|
|
355049
355616
|
} catch (err) {
|
|
355050
|
-
return { installed: false, note: `CA push failed: ${
|
|
355617
|
+
return { installed: false, note: `CA push failed: ${errMsg2(err)}` };
|
|
355051
355618
|
}
|
|
355052
355619
|
}
|
|
355053
355620
|
async function androidRemoveCa(deviceId, onLog) {
|
|
355054
355621
|
try {
|
|
355055
|
-
await
|
|
355622
|
+
await execFileAsync2(
|
|
355056
355623
|
"adb",
|
|
355057
355624
|
["-s", deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"],
|
|
355058
355625
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355059
355626
|
);
|
|
355060
355627
|
} catch (err) {
|
|
355061
|
-
onLog?.(`[mobile-net] adb remove CA failed: ${
|
|
355628
|
+
onLog?.(`[mobile-net] adb remove CA failed: ${errMsg2(err)}`);
|
|
355062
355629
|
}
|
|
355063
355630
|
}
|
|
355064
355631
|
async function iosSimTrustCa(deviceId, caCertPath) {
|
|
355065
355632
|
try {
|
|
355066
|
-
await
|
|
355633
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
|
|
355067
355634
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355068
355635
|
});
|
|
355069
355636
|
return {
|
|
@@ -355071,18 +355638,18 @@ async function iosSimTrustCa(deviceId, caCertPath) {
|
|
|
355071
355638
|
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
355639
|
};
|
|
355073
355640
|
} catch (err) {
|
|
355074
|
-
return { trusted: false, note: `simctl add-root-cert failed: ${
|
|
355641
|
+
return { trusted: false, note: `simctl add-root-cert failed: ${errMsg2(err)}` };
|
|
355075
355642
|
}
|
|
355076
355643
|
}
|
|
355077
355644
|
async function iosSimRemoveCa(deviceId, onLog) {
|
|
355078
355645
|
if (process.env.VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET === "1") {
|
|
355079
355646
|
try {
|
|
355080
|
-
await
|
|
355647
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "reset"], {
|
|
355081
355648
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355082
355649
|
});
|
|
355083
355650
|
onLog?.(`[mobile-net] simctl keychain reset run for ${deviceId} (opt-in; cleared ALL sim certs).`);
|
|
355084
355651
|
} catch (err) {
|
|
355085
|
-
onLog?.(`[mobile-net] simctl keychain reset failed: ${
|
|
355652
|
+
onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg2(err)}`);
|
|
355086
355653
|
}
|
|
355087
355654
|
return;
|
|
355088
355655
|
}
|
|
@@ -355090,7 +355657,7 @@ async function iosSimRemoveCa(deviceId, onLog) {
|
|
|
355090
355657
|
`[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
355658
|
);
|
|
355092
355659
|
}
|
|
355093
|
-
function
|
|
355660
|
+
function errMsg2(err) {
|
|
355094
355661
|
return err instanceof Error ? err.message : String(err);
|
|
355095
355662
|
}
|
|
355096
355663
|
function isProcessAlive(pid) {
|
|
@@ -355181,7 +355748,7 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355181
355748
|
let service;
|
|
355182
355749
|
let prior;
|
|
355183
355750
|
try {
|
|
355184
|
-
const { stdout: order } = await
|
|
355751
|
+
const { stdout: order } = await execFileAsync2("networksetup", ["-listnetworkserviceorder"], {
|
|
355185
355752
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355186
355753
|
});
|
|
355187
355754
|
const svc = parsePrimaryNetworkService(order);
|
|
@@ -355190,26 +355757,26 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355190
355757
|
}
|
|
355191
355758
|
service = svc;
|
|
355192
355759
|
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
355193
|
-
|
|
355194
|
-
|
|
355760
|
+
execFileAsync2("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
355761
|
+
execFileAsync2("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
355195
355762
|
]);
|
|
355196
355763
|
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
355197
355764
|
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
355198
|
-
await
|
|
355765
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355199
355766
|
}
|
|
355200
355767
|
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
355201
355768
|
return { configured: true, service, prior };
|
|
355202
355769
|
} catch (err) {
|
|
355203
355770
|
if (service && prior) {
|
|
355204
|
-
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${
|
|
355771
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg2(err)}`);
|
|
355205
355772
|
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
355206
355773
|
try {
|
|
355207
|
-
await
|
|
355774
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355208
355775
|
} catch {
|
|
355209
355776
|
}
|
|
355210
355777
|
}
|
|
355211
355778
|
}
|
|
355212
|
-
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${
|
|
355779
|
+
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg2(err)}` };
|
|
355213
355780
|
}
|
|
355214
355781
|
}
|
|
355215
355782
|
async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
@@ -355219,9 +355786,9 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
|
355219
355786
|
};
|
|
355220
355787
|
for (const args of buildHostProxyRestoreCommands(service, restore)) {
|
|
355221
355788
|
try {
|
|
355222
|
-
await
|
|
355789
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355223
355790
|
} catch (err) {
|
|
355224
|
-
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${
|
|
355791
|
+
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg2(err)}`);
|
|
355225
355792
|
}
|
|
355226
355793
|
}
|
|
355227
355794
|
onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
|
|
@@ -355233,14 +355800,14 @@ function markerPath(sessionKey) {
|
|
|
355233
355800
|
}
|
|
355234
355801
|
async function writePendingCleanup(sessionKey, marker) {
|
|
355235
355802
|
try {
|
|
355236
|
-
await (0,
|
|
355237
|
-
await (0,
|
|
355803
|
+
await (0, import_promises2.mkdir)(PENDING_CLEANUP_DIR, { recursive: true });
|
|
355804
|
+
await (0, import_promises2.writeFile)(markerPath(sessionKey), JSON.stringify(marker), "utf-8");
|
|
355238
355805
|
} catch {
|
|
355239
355806
|
}
|
|
355240
355807
|
}
|
|
355241
355808
|
async function removePendingCleanup(sessionKey) {
|
|
355242
355809
|
try {
|
|
355243
|
-
await (0,
|
|
355810
|
+
await (0, import_promises2.unlink)(markerPath(sessionKey));
|
|
355244
355811
|
} catch {
|
|
355245
355812
|
}
|
|
355246
355813
|
}
|
|
@@ -355273,7 +355840,7 @@ function restoreDeviceSync(marker, onLog) {
|
|
|
355273
355840
|
async function reconcilePendingCleanups(onLog) {
|
|
355274
355841
|
let files;
|
|
355275
355842
|
try {
|
|
355276
|
-
files = await (0,
|
|
355843
|
+
files = await (0, import_promises2.readdir)(PENDING_CLEANUP_DIR);
|
|
355277
355844
|
} catch {
|
|
355278
355845
|
return;
|
|
355279
355846
|
}
|
|
@@ -355283,7 +355850,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
355283
355850
|
if (activeCleanups.has(sessionKey)) continue;
|
|
355284
355851
|
const full = (0, import_node_path.join)(PENDING_CLEANUP_DIR, file);
|
|
355285
355852
|
try {
|
|
355286
|
-
const raw = await (0,
|
|
355853
|
+
const raw = await (0, import_promises2.readFile)(full, "utf-8");
|
|
355287
355854
|
const marker = JSON.parse(raw);
|
|
355288
355855
|
if (marker && typeof marker.deviceId === "string" && marker.platform) {
|
|
355289
355856
|
if (!shouldReconcilePendingCleanup(marker)) {
|
|
@@ -355296,7 +355863,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
355296
355863
|
} catch {
|
|
355297
355864
|
}
|
|
355298
355865
|
try {
|
|
355299
|
-
await (0,
|
|
355866
|
+
await (0, import_promises2.unlink)(full);
|
|
355300
355867
|
} catch {
|
|
355301
355868
|
}
|
|
355302
355869
|
}
|
|
@@ -355334,7 +355901,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355334
355901
|
let caKeyPem = null;
|
|
355335
355902
|
let caCertPem = null;
|
|
355336
355903
|
try {
|
|
355337
|
-
tmpDir = await (0,
|
|
355904
|
+
tmpDir = await (0, import_promises2.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "validateqa-mobile-ca-"));
|
|
355338
355905
|
const ca = await generateCaCert(tmpDir, onLog);
|
|
355339
355906
|
if (ca) {
|
|
355340
355907
|
caCertPath = ca.caCertPath;
|
|
@@ -355342,7 +355909,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355342
355909
|
caCertPem = ca.caCertPem;
|
|
355343
355910
|
}
|
|
355344
355911
|
} catch (err) {
|
|
355345
|
-
onLog?.(`[mobile-net] CA setup degraded: ${
|
|
355912
|
+
onLog?.(`[mobile-net] CA setup degraded: ${errMsg2(err)}`);
|
|
355346
355913
|
}
|
|
355347
355914
|
if (!caKeyPem || !caCertPem) {
|
|
355348
355915
|
try {
|
|
@@ -355350,9 +355917,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355350
355917
|
caKeyPem = fallback.key;
|
|
355351
355918
|
caCertPem = fallback.cert;
|
|
355352
355919
|
} catch (err) {
|
|
355353
|
-
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${
|
|
355920
|
+
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg2(err)}`);
|
|
355354
355921
|
if (tmpDir) {
|
|
355355
|
-
await (0,
|
|
355922
|
+
await (0, import_promises2.rm)(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
355356
355923
|
}
|
|
355357
355924
|
throw err instanceof Error ? err : new Error(String(err));
|
|
355358
355925
|
}
|
|
@@ -355407,7 +355974,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355407
355974
|
}
|
|
355408
355975
|
}
|
|
355409
355976
|
} catch (err) {
|
|
355410
|
-
wiringReason = `device wiring degraded: ${
|
|
355977
|
+
wiringReason = `device wiring degraded: ${errMsg2(err)}`;
|
|
355411
355978
|
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
355412
355979
|
}
|
|
355413
355980
|
if (proxySet || caInstalledOnDevice || hostProxySet) {
|
|
@@ -355496,9 +356063,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355496
356063
|
await proxy.close();
|
|
355497
356064
|
if (capturedTmpDir) {
|
|
355498
356065
|
try {
|
|
355499
|
-
await (0,
|
|
356066
|
+
await (0, import_promises2.rm)(capturedTmpDir, { recursive: true, force: true });
|
|
355500
356067
|
} catch (err) {
|
|
355501
|
-
onLog?.(`[mobile-net] temp CA cleanup failed: ${
|
|
356068
|
+
onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg2(err)}`);
|
|
355502
356069
|
}
|
|
355503
356070
|
}
|
|
355504
356071
|
const apiCount = finalCalls.filter((c) => c.isApi).length;
|
|
@@ -355531,11 +356098,11 @@ var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
|
355531
356098
|
})();
|
|
355532
356099
|
var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
355533
356100
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
355534
|
-
var WD_SESSION_CREATE_TIMEOUT_MS =
|
|
356101
|
+
var WD_SESSION_CREATE_TIMEOUT_MS = Number(process.env.VALIDATEQA_WD_SESSION_CREATE_TIMEOUT_MS) || 3e5;
|
|
355535
356102
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
355536
356103
|
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
355537
356104
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
355538
|
-
var
|
|
356105
|
+
var execFileAsync3 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
355539
356106
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
355540
356107
|
var WebDriverTransportError = class extends Error {
|
|
355541
356108
|
isTransport = true;
|
|
@@ -355547,6 +356114,13 @@ var WebDriverTransportError = class extends Error {
|
|
|
355547
356114
|
function isTransportError(err) {
|
|
355548
356115
|
return err instanceof WebDriverTransportError || typeof err === "object" && err !== null && err.isTransport === true;
|
|
355549
356116
|
}
|
|
356117
|
+
var MobileAmbiguousMatchError = class extends Error {
|
|
356118
|
+
isAmbiguousMatch = true;
|
|
356119
|
+
constructor(message) {
|
|
356120
|
+
super(message);
|
|
356121
|
+
this.name = "MobileAmbiguousMatchError";
|
|
356122
|
+
}
|
|
356123
|
+
};
|
|
355550
356124
|
function crossPlatformRunReadinessError(steps, authoringPlatform, runPlatform) {
|
|
355551
356125
|
if (!authoringPlatform || authoringPlatform === runPlatform) return null;
|
|
355552
356126
|
const missing = stepsMissingPlatformLocators(steps, runPlatform);
|
|
@@ -355692,6 +356266,7 @@ function androidSystemPortForwardRemovalArgs(target, error2, env = process.env)
|
|
|
355692
356266
|
return ["-s", deviceId, "forward", "--remove", `tcp:${systemPort}`];
|
|
355693
356267
|
}
|
|
355694
356268
|
async function createSession(target) {
|
|
356269
|
+
assertDeviceNotQuarantined(target.deviceId);
|
|
355695
356270
|
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform) : {};
|
|
355696
356271
|
const parallelCaps = mobileParallelSessionCapabilities(
|
|
355697
356272
|
target.platform,
|
|
@@ -355713,6 +356288,13 @@ async function createSession(target) {
|
|
|
355713
356288
|
// whole survey. The runner owns session teardown explicitly, so a long
|
|
355714
356289
|
// idle window is safe. Env-overridable.
|
|
355715
356290
|
"appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
|
|
356291
|
+
// Xcode 27+ relocates Simulator.app out of <DEVELOPER_DIR>/Applications, so
|
|
356292
|
+
// XCUITest's default "open Simulator UI" step fails ("Simulator.app does not
|
|
356293
|
+
// exist") on this toolchain. The simulator is already booted and driven
|
|
356294
|
+
// headlessly via CoreSimulator, so tell XCUITest to skip opening the GUI.
|
|
356295
|
+
// Scoped to simulator UDIDs (a physical iOS device has no such step);
|
|
356296
|
+
// override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
|
|
356297
|
+
...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
355716
356298
|
...parallelCaps,
|
|
355717
356299
|
...realCaps
|
|
355718
356300
|
} : {
|
|
@@ -355752,7 +356334,7 @@ async function removeStaleAndroidSystemPortForward(target, error2) {
|
|
|
355752
356334
|
const args = androidSystemPortForwardRemovalArgs(target, error2);
|
|
355753
356335
|
if (!args) return false;
|
|
355754
356336
|
try {
|
|
355755
|
-
await
|
|
356337
|
+
await execFileAsync3("adb", args, { timeout: 1e4, windowsHide: true });
|
|
355756
356338
|
return true;
|
|
355757
356339
|
} catch {
|
|
355758
356340
|
return false;
|
|
@@ -355919,6 +356501,9 @@ async function deleteSession(sessionId) {
|
|
|
355919
356501
|
} catch {
|
|
355920
356502
|
}
|
|
355921
356503
|
}
|
|
356504
|
+
async function deleteAppiumSession(sessionId) {
|
|
356505
|
+
await deleteSession(sessionId);
|
|
356506
|
+
}
|
|
355922
356507
|
async function takeScreenshot(sessionId) {
|
|
355923
356508
|
try {
|
|
355924
356509
|
const result = await wdRequest(`/session/${sessionId}/screenshot`);
|
|
@@ -355990,10 +356575,9 @@ async function stopScreenRecording(sessionId, log2) {
|
|
|
355990
356575
|
return void 0;
|
|
355991
356576
|
}
|
|
355992
356577
|
}
|
|
355993
|
-
function
|
|
355994
|
-
|
|
355995
|
-
|
|
355996
|
-
const parsed = bounds;
|
|
356578
|
+
function parseMobileBoundsLike(value) {
|
|
356579
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.x === "number" && typeof value.y === "number" && typeof value.width === "number" && typeof value.height === "number") {
|
|
356580
|
+
const parsed = value;
|
|
355997
356581
|
if ([parsed.x, parsed.y, parsed.width, parsed.height].every(Number.isFinite)) {
|
|
355998
356582
|
return {
|
|
355999
356583
|
x: parsed.x,
|
|
@@ -356005,6 +356589,9 @@ function boundsFromStep(step) {
|
|
|
356005
356589
|
}
|
|
356006
356590
|
return null;
|
|
356007
356591
|
}
|
|
356592
|
+
function boundsFromStep(step) {
|
|
356593
|
+
return parseMobileBoundsLike(step.metadata?.bounds);
|
|
356594
|
+
}
|
|
356008
356595
|
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
356009
356596
|
if (isCrossPlatform) return null;
|
|
356010
356597
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
@@ -356156,6 +356743,131 @@ async function revealLikelyOffscreenElement(sessionId) {
|
|
|
356156
356743
|
});
|
|
356157
356744
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
356158
356745
|
}
|
|
356746
|
+
function strictMatchMode() {
|
|
356747
|
+
const raw = (process.env.VALIDATEQA_MOBILE_STRICT_MATCH ?? "").trim().toLowerCase();
|
|
356748
|
+
if (raw === "strict") return "strict";
|
|
356749
|
+
if (raw === "off") return "off";
|
|
356750
|
+
return "lenient";
|
|
356751
|
+
}
|
|
356752
|
+
var MOBILE_MAX_DISAMBIG_CANDIDATES = 20;
|
|
356753
|
+
var COINCIDENT_CENTER_EPSILON_PX = 2;
|
|
356754
|
+
function isRealElementRect(rect) {
|
|
356755
|
+
return rect.width > 1 && rect.height > 1;
|
|
356756
|
+
}
|
|
356757
|
+
function rectContainsPoint(rect, x, y) {
|
|
356758
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
356759
|
+
}
|
|
356760
|
+
function rectCenter(rect) {
|
|
356761
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
356762
|
+
}
|
|
356763
|
+
function centerDistance(rect, point) {
|
|
356764
|
+
const center = rectCenter(rect);
|
|
356765
|
+
return Math.hypot(center.x - point.x, center.y - point.y);
|
|
356766
|
+
}
|
|
356767
|
+
function chooseRecordedMatch(candidates, opts) {
|
|
356768
|
+
if (candidates.length === 0) return { kind: "ambiguous", count: 0 };
|
|
356769
|
+
if (candidates.length === 1) {
|
|
356770
|
+
return { kind: "matched", elementId: candidates[0].elementId, reason: "single" };
|
|
356771
|
+
}
|
|
356772
|
+
if (typeof opts.matchIndex === "number" && Number.isInteger(opts.matchIndex) && opts.matchIndex >= 0 && opts.matchIndex < candidates.length && typeof opts.expectedMatchCount === "number" && opts.expectedMatchCount === candidates.length) {
|
|
356773
|
+
return { kind: "matched", elementId: candidates[opts.matchIndex].elementId, reason: "matchIndex" };
|
|
356774
|
+
}
|
|
356775
|
+
{
|
|
356776
|
+
const withRect = candidates.filter(
|
|
356777
|
+
(candidate) => candidate.rect != null
|
|
356778
|
+
);
|
|
356779
|
+
if (withRect.length === candidates.length) {
|
|
356780
|
+
const real = withRect.filter((candidate) => isRealElementRect(candidate.rect));
|
|
356781
|
+
if (real.length === 1) {
|
|
356782
|
+
return { kind: "matched", elementId: real[0].elementId, reason: "visible" };
|
|
356783
|
+
}
|
|
356784
|
+
const centers = withRect.map((candidate) => rectCenter(candidate.rect));
|
|
356785
|
+
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);
|
|
356786
|
+
if (coincident) {
|
|
356787
|
+
return { kind: "matched", elementId: withRect[0].elementId, reason: "coincident" };
|
|
356788
|
+
}
|
|
356789
|
+
}
|
|
356790
|
+
}
|
|
356791
|
+
if (opts.recordedBounds) {
|
|
356792
|
+
const bounds = opts.recordedBounds;
|
|
356793
|
+
const center = rectCenter(bounds);
|
|
356794
|
+
const withRect = candidates.filter(
|
|
356795
|
+
(candidate) => candidate.rect != null
|
|
356796
|
+
);
|
|
356797
|
+
const containing = withRect.filter((candidate) => rectContainsPoint(candidate.rect, center.x, center.y));
|
|
356798
|
+
if (containing.length > 0) {
|
|
356799
|
+
const best = [...containing].sort(
|
|
356800
|
+
(a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height || centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356801
|
+
)[0];
|
|
356802
|
+
return { kind: "matched", elementId: best.elementId, reason: "bounds" };
|
|
356803
|
+
}
|
|
356804
|
+
if (withRect.length > 0) {
|
|
356805
|
+
const tolerance = Math.max(bounds.height, 1);
|
|
356806
|
+
const nearest = [...withRect].sort(
|
|
356807
|
+
(a, b) => centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356808
|
+
)[0];
|
|
356809
|
+
if (centerDistance(nearest.rect, center) <= tolerance) {
|
|
356810
|
+
return { kind: "matched", elementId: nearest.elementId, reason: "bounds" };
|
|
356811
|
+
}
|
|
356812
|
+
}
|
|
356813
|
+
}
|
|
356814
|
+
return opts.strict === "strict" ? { kind: "ambiguous", count: candidates.length } : { kind: "fallback", elementId: candidates[0].elementId, count: candidates.length };
|
|
356815
|
+
}
|
|
356816
|
+
function extractElementIdsFromPluralResponse(result) {
|
|
356817
|
+
const rawList = result?.value;
|
|
356818
|
+
if (!Array.isArray(rawList)) return [];
|
|
356819
|
+
const ids = [];
|
|
356820
|
+
for (const entry of rawList) {
|
|
356821
|
+
const record = entry;
|
|
356822
|
+
const id = validateElementId(record?.ELEMENT ?? record?.["element-6066-11e4-a52e-4f735466cecf"]);
|
|
356823
|
+
if (id) ids.push(id);
|
|
356824
|
+
}
|
|
356825
|
+
return ids;
|
|
356826
|
+
}
|
|
356827
|
+
async function findElements(sessionId, strategy) {
|
|
356828
|
+
const result = await wdRequest(`/session/${sessionId}/elements`, {
|
|
356829
|
+
method: "POST",
|
|
356830
|
+
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
356831
|
+
});
|
|
356832
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356833
|
+
}
|
|
356834
|
+
async function findWebViewElements(sessionPath, css) {
|
|
356835
|
+
const result = await wdRequest(`${sessionPath}/elements`, {
|
|
356836
|
+
method: "POST",
|
|
356837
|
+
body: JSON.stringify({ using: "css selector", value: css })
|
|
356838
|
+
});
|
|
356839
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356840
|
+
}
|
|
356841
|
+
async function readElementRect(sessionId, elementId) {
|
|
356842
|
+
try {
|
|
356843
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/rect`);
|
|
356844
|
+
return parseMobileBoundsLike(result?.value);
|
|
356845
|
+
} catch (err) {
|
|
356846
|
+
if (isTransportError(err)) throw err;
|
|
356847
|
+
return null;
|
|
356848
|
+
}
|
|
356849
|
+
}
|
|
356850
|
+
async function disambiguateElementIds(sessionId, candidateIds, opts) {
|
|
356851
|
+
const indexOnly = candidateIds.map((elementId) => ({ elementId }));
|
|
356852
|
+
const indexResult = chooseRecordedMatch(indexOnly, {
|
|
356853
|
+
recordedBounds: null,
|
|
356854
|
+
matchIndex: opts.matchIndex,
|
|
356855
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356856
|
+
strict: opts.strict
|
|
356857
|
+
});
|
|
356858
|
+
if (indexResult.kind === "matched" || candidateIds.length > MOBILE_MAX_DISAMBIG_CANDIDATES) {
|
|
356859
|
+
return indexResult;
|
|
356860
|
+
}
|
|
356861
|
+
const withRects = await Promise.all(
|
|
356862
|
+
candidateIds.map(async (elementId) => ({ elementId, rect: await readElementRect(sessionId, elementId) }))
|
|
356863
|
+
);
|
|
356864
|
+
return chooseRecordedMatch(withRects, {
|
|
356865
|
+
recordedBounds: opts.recordedBounds ?? null,
|
|
356866
|
+
matchIndex: opts.matchIndex,
|
|
356867
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356868
|
+
strict: opts.strict
|
|
356869
|
+
});
|
|
356870
|
+
}
|
|
356159
356871
|
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
356160
356872
|
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
356161
356873
|
if (strategies.length === 0) {
|
|
@@ -356163,22 +356875,45 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356163
356875
|
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
356876
|
);
|
|
356165
356877
|
}
|
|
356878
|
+
const mode = strictMatchMode();
|
|
356879
|
+
const matchIndex = typeof target?.matchIndex === "number" ? target.matchIndex : void 0;
|
|
356880
|
+
const expectedMatchCount = typeof target?.expectedMatchCount === "number" ? target.expectedMatchCount : void 0;
|
|
356166
356881
|
const startTime = Date.now();
|
|
356167
356882
|
let lastError = "";
|
|
356883
|
+
let lastAmbiguity = null;
|
|
356168
356884
|
let revealAttempted = false;
|
|
356169
356885
|
while (Date.now() - startTime < timeoutMs) {
|
|
356170
356886
|
for (const strategy of strategies) {
|
|
356171
356887
|
try {
|
|
356172
|
-
const
|
|
356173
|
-
|
|
356174
|
-
|
|
356888
|
+
const ids = await findElements(sessionId, strategy);
|
|
356889
|
+
if (ids.length === 0) continue;
|
|
356890
|
+
if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
356891
|
+
return ids[0];
|
|
356892
|
+
}
|
|
356893
|
+
const result = await disambiguateElementIds(sessionId, ids, {
|
|
356894
|
+
recordedBounds: opts?.recordedBounds,
|
|
356895
|
+
matchIndex,
|
|
356896
|
+
expectedMatchCount,
|
|
356897
|
+
strict: mode
|
|
356175
356898
|
});
|
|
356176
|
-
|
|
356177
|
-
|
|
356899
|
+
if (result.kind === "matched") return result.elementId;
|
|
356900
|
+
const shouldFailClosed = opts?.failClosedOnAmbiguity || result.kind === "ambiguous" && !opts?.tolerateAmbiguity;
|
|
356901
|
+
if (shouldFailClosed) {
|
|
356902
|
+
throw new MobileAmbiguousMatchError(
|
|
356903
|
+
`ambiguous locator: ${result.count} matches, cannot identify the recorded instance (strategy=${strategy.using} value=${strategy.value})`
|
|
356904
|
+
);
|
|
356905
|
+
}
|
|
356906
|
+
opts?.log?.(
|
|
356907
|
+
`\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
356908
|
);
|
|
356179
|
-
|
|
356909
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
356180
356910
|
} catch (err) {
|
|
356181
356911
|
if (isTransportError(err)) throw err;
|
|
356912
|
+
if (err instanceof MobileAmbiguousMatchError) {
|
|
356913
|
+
lastAmbiguity = err;
|
|
356914
|
+
lastError = err.message;
|
|
356915
|
+
continue;
|
|
356916
|
+
}
|
|
356182
356917
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356183
356918
|
}
|
|
356184
356919
|
}
|
|
@@ -356193,6 +356928,7 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356193
356928
|
}
|
|
356194
356929
|
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
356195
356930
|
}
|
|
356931
|
+
if (lastAmbiguity) throw lastAmbiguity;
|
|
356196
356932
|
throw new Error(`Element not found after ${timeoutMs}ms. Tried ${strategies.length} strategies${revealAttempted ? " plus one reveal scroll" : ""}. Last error: ${lastError}`);
|
|
356197
356933
|
}
|
|
356198
356934
|
async function tapCoordinates(sessionId, bounds) {
|
|
@@ -356226,7 +356962,7 @@ async function prepareAndroidInputEnvironment(deviceId) {
|
|
|
356226
356962
|
];
|
|
356227
356963
|
for (const [namespace, key, value] of settingsToDisable) {
|
|
356228
356964
|
try {
|
|
356229
|
-
await
|
|
356965
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
356230
356966
|
timeout: 8e3,
|
|
356231
356967
|
windowsHide: true
|
|
356232
356968
|
});
|
|
@@ -356238,14 +356974,14 @@ async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
|
356238
356974
|
if (!deviceId) {
|
|
356239
356975
|
throw new Error("Android keyboard fallback requires a device id");
|
|
356240
356976
|
}
|
|
356241
|
-
await
|
|
356977
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
|
|
356242
356978
|
timeout: 1e4,
|
|
356243
356979
|
windowsHide: true
|
|
356244
356980
|
});
|
|
356245
356981
|
}
|
|
356246
356982
|
async function pressAndroidKeyboardEnter(deviceId) {
|
|
356247
356983
|
if (!deviceId) throw new Error("Android keyboard Enter fallback requires a device id");
|
|
356248
|
-
await
|
|
356984
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
|
|
356249
356985
|
timeout: 1e4,
|
|
356250
356986
|
windowsHide: true
|
|
356251
356987
|
});
|
|
@@ -356333,21 +357069,40 @@ async function executeInWebViewContext(sessionId, step, css, body) {
|
|
|
356333
357069
|
const cleanCss = css.startsWith("css=") ? css.slice(4) : css;
|
|
356334
357070
|
await withWebViewContext(sessionPath, webview, () => body(sessionPath, cleanCss));
|
|
356335
357071
|
}
|
|
356336
|
-
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS) {
|
|
357072
|
+
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
357073
|
+
const mode = strictMatchMode();
|
|
356337
357074
|
const startedAt = Date.now();
|
|
356338
357075
|
let lastError = "";
|
|
356339
357076
|
while (Date.now() - startedAt < timeoutMs) {
|
|
356340
357077
|
try {
|
|
356341
|
-
const
|
|
356342
|
-
|
|
356343
|
-
|
|
356344
|
-
})
|
|
356345
|
-
|
|
356346
|
-
|
|
356347
|
-
|
|
356348
|
-
|
|
357078
|
+
const ids = await findWebViewElements(sessionPath, css);
|
|
357079
|
+
if (ids.length === 0) {
|
|
357080
|
+
lastError = `no match for css=${css}`;
|
|
357081
|
+
} else if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
357082
|
+
return ids[0];
|
|
357083
|
+
} else {
|
|
357084
|
+
const candidates = ids.map((elementId) => ({ elementId }));
|
|
357085
|
+
const result = chooseRecordedMatch(candidates, {
|
|
357086
|
+
recordedBounds: null,
|
|
357087
|
+
matchIndex: opts?.matchIndex,
|
|
357088
|
+
expectedMatchCount: opts?.expectedMatchCount,
|
|
357089
|
+
strict: mode
|
|
357090
|
+
});
|
|
357091
|
+
if (result.kind === "matched") {
|
|
357092
|
+
return result.elementId;
|
|
357093
|
+
}
|
|
357094
|
+
if (result.kind === "ambiguous" && !opts?.tolerateAmbiguity) {
|
|
357095
|
+
throw new MobileAmbiguousMatchError(
|
|
357096
|
+
`ambiguous WEBVIEW locator: ${result.count} matches for css=${css}, cannot identify the recorded instance`
|
|
357097
|
+
);
|
|
357098
|
+
}
|
|
357099
|
+
opts?.log?.(
|
|
357100
|
+
`\u26A0 ambiguous WEBVIEW locator (${result.count} matches for css=${css}) \u2014 using the first match. Set VALIDATEQA_MOBILE_STRICT_MATCH=strict to fail instead.`
|
|
357101
|
+
);
|
|
357102
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
357103
|
+
}
|
|
356349
357104
|
} catch (err) {
|
|
356350
|
-
if (isTransportError(err)) throw err;
|
|
357105
|
+
if (isTransportError(err) || err instanceof MobileAmbiguousMatchError) throw err;
|
|
356351
357106
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356352
357107
|
}
|
|
356353
357108
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
@@ -356356,21 +357111,42 @@ async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIM
|
|
|
356356
357111
|
`WEBVIEW element not found for css=${css} after ${timeoutMs}ms` + (lastError ? `. Last error: ${lastError}` : "")
|
|
356357
357112
|
);
|
|
356358
357113
|
}
|
|
357114
|
+
function isWebViewHydrationTransient(err) {
|
|
357115
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357116
|
+
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);
|
|
357117
|
+
}
|
|
357118
|
+
var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
|
|
357119
|
+
async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
|
|
357120
|
+
const deadline = Date.now() + Math.max(1e3, timeoutMs);
|
|
357121
|
+
for (; ; ) {
|
|
357122
|
+
try {
|
|
357123
|
+
await executeInWebViewContext(sessionId, step, css, body);
|
|
357124
|
+
return;
|
|
357125
|
+
} catch (err) {
|
|
357126
|
+
if (!isWebViewHydrationTransient(err) || Date.now() >= deadline) throw err;
|
|
357127
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
357128
|
+
}
|
|
357129
|
+
}
|
|
357130
|
+
}
|
|
356359
357131
|
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356360
357132
|
const webCss = stepWebViewCss(step);
|
|
356361
357133
|
if (webCss) {
|
|
356362
|
-
await
|
|
357134
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356363
357135
|
sessionId,
|
|
356364
357136
|
step,
|
|
356365
357137
|
webCss,
|
|
356366
|
-
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css)
|
|
357138
|
+
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css),
|
|
357139
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356367
357140
|
);
|
|
356368
357141
|
return;
|
|
356369
357142
|
}
|
|
356370
357143
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356371
357144
|
if (step.target) {
|
|
356372
357145
|
try {
|
|
356373
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357146
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357147
|
+
platform: platform3,
|
|
357148
|
+
recordedBounds: bounds
|
|
357149
|
+
});
|
|
356374
357150
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356375
357151
|
method: "POST",
|
|
356376
357152
|
body: "{}"
|
|
@@ -356392,11 +357168,12 @@ async function executeType(sessionId, step, context) {
|
|
|
356392
357168
|
const keysValue = keyboardSubmitOnly ? WEBDRIVER_ENTER_KEY : value;
|
|
356393
357169
|
const webCss = stepWebViewCss(step);
|
|
356394
357170
|
if (webCss) {
|
|
356395
|
-
await
|
|
357171
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356396
357172
|
sessionId,
|
|
356397
357173
|
step,
|
|
356398
357174
|
webCss,
|
|
356399
|
-
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value)
|
|
357175
|
+
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value),
|
|
357176
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356400
357177
|
);
|
|
356401
357178
|
return;
|
|
356402
357179
|
}
|
|
@@ -356407,7 +357184,9 @@ async function executeType(sessionId, step, context) {
|
|
|
356407
357184
|
const locatorStrategies = buildLocatorStrategies(step.target, context.platform);
|
|
356408
357185
|
if (step.target && locatorStrategies.length > 0) {
|
|
356409
357186
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
356410
|
-
platform: context.platform
|
|
357187
|
+
platform: context.platform,
|
|
357188
|
+
recordedBounds: bounds,
|
|
357189
|
+
log: context.log
|
|
356411
357190
|
});
|
|
356412
357191
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356413
357192
|
method: "POST",
|
|
@@ -356448,22 +357227,28 @@ async function executeType(sessionId, step, context) {
|
|
|
356448
357227
|
}
|
|
356449
357228
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356450
357229
|
const webCss = stepWebViewCss(step);
|
|
357230
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356451
357231
|
if (webCss) {
|
|
356452
357232
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356453
|
-
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357233
|
+
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357234
|
+
matchIndex: step.target?.matchIndex,
|
|
357235
|
+
expectedMatchCount: step.target?.expectedMatchCount
|
|
357236
|
+
});
|
|
356454
357237
|
await wdRequest(`${sessionPath}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356455
357238
|
});
|
|
356456
357239
|
return;
|
|
356457
357240
|
}
|
|
356458
357241
|
const locatorStrategies = buildLocatorStrategies(step.target, platform3);
|
|
356459
357242
|
if (step.target && locatorStrategies.length > 0) {
|
|
356460
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357243
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357244
|
+
platform: platform3,
|
|
357245
|
+
recordedBounds: bounds
|
|
357246
|
+
});
|
|
356461
357247
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
356462
357248
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
356463
357249
|
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356464
357250
|
return;
|
|
356465
357251
|
}
|
|
356466
|
-
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356467
357252
|
await clearField(`/session/${sessionId}`, bounds ?? void 0);
|
|
356468
357253
|
}
|
|
356469
357254
|
async function executeSwipe(sessionId, step) {
|
|
@@ -356524,34 +357309,47 @@ async function executeWaitForElement(sessionId, step, platform3) {
|
|
|
356524
357309
|
sessionId,
|
|
356525
357310
|
step,
|
|
356526
357311
|
webCss,
|
|
356527
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout).then(() => void 0)
|
|
357312
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout, { disambiguate: false }).then(() => void 0)
|
|
356528
357313
|
);
|
|
356529
357314
|
return;
|
|
356530
357315
|
}
|
|
356531
|
-
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
357316
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3, disambiguate: false });
|
|
356532
357317
|
}
|
|
356533
|
-
async function executeAssertVisible(sessionId, step, platform3) {
|
|
357318
|
+
async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356534
357319
|
const webCss = stepWebViewCss(step);
|
|
356535
357320
|
if (webCss) {
|
|
356536
357321
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356537
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357322
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357323
|
+
matchIndex: step.target?.matchIndex,
|
|
357324
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357325
|
+
tolerateAmbiguity: true
|
|
357326
|
+
});
|
|
356538
357327
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/displayed`);
|
|
356539
357328
|
if (!result2?.value) throw new Error(`WEBVIEW element found but not visible: css=${css}`);
|
|
356540
357329
|
});
|
|
356541
357330
|
return;
|
|
356542
357331
|
}
|
|
356543
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357332
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357333
|
+
identityOnly: true,
|
|
357334
|
+
platform: platform3,
|
|
357335
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357336
|
+
tolerateAmbiguity: true
|
|
357337
|
+
});
|
|
356544
357338
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
356545
357339
|
if (!result?.value) {
|
|
356546
357340
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
356547
357341
|
}
|
|
356548
357342
|
}
|
|
356549
|
-
async function executeAssertText(sessionId, step, platform3) {
|
|
357343
|
+
async function executeAssertText(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356550
357344
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
356551
357345
|
const webCss = stepWebViewCss(step);
|
|
356552
357346
|
if (webCss) {
|
|
356553
357347
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356554
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357348
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357349
|
+
matchIndex: step.target?.matchIndex,
|
|
357350
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357351
|
+
tolerateAmbiguity: true
|
|
357352
|
+
});
|
|
356555
357353
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/text`);
|
|
356556
357354
|
const actualText2 = typeof result2?.value === "string" ? result2.value : "";
|
|
356557
357355
|
const textCandidates2 = await readElementTextCandidates(sessionId, elementId2, actualText2);
|
|
@@ -356563,7 +357361,12 @@ async function executeAssertText(sessionId, step, platform3) {
|
|
|
356563
357361
|
});
|
|
356564
357362
|
return;
|
|
356565
357363
|
}
|
|
356566
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357364
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357365
|
+
identityOnly: true,
|
|
357366
|
+
platform: platform3,
|
|
357367
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357368
|
+
tolerateAmbiguity: true
|
|
357369
|
+
});
|
|
356567
357370
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
356568
357371
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
356569
357372
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -356603,6 +357406,8 @@ function acquireControlPortSlot(deviceId) {
|
|
|
356603
357406
|
function releaseControlPortSlot(deviceId) {
|
|
356604
357407
|
controlPortSlotsByDevice.delete(deviceId);
|
|
356605
357408
|
}
|
|
357409
|
+
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357410
|
+
var pinnedDeviceSnapshot = null;
|
|
356606
357411
|
async function executeMobileTest(options) {
|
|
356607
357412
|
const logLines = [];
|
|
356608
357413
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -356610,6 +357415,7 @@ async function executeMobileTest(options) {
|
|
|
356610
357415
|
const { steps: runSteps, missingKeys: missingVariableKeys } = resolveMobileStepValuePlaceholders(options.steps, options.testVariables);
|
|
356611
357416
|
const runPlatform = options.target.platform;
|
|
356612
357417
|
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
357418
|
+
const captureStepScreenshots = options.captureStepScreenshots !== false;
|
|
356613
357419
|
if (missingVariableKeys.length > 0) {
|
|
356614
357420
|
log2(`\u26A0 Unresolved step-value placeholder(s): ${missingVariableKeys.map((k) => `{{${k}}}`).join(", ")} \u2014 no matching test variable configured for this project/run.`);
|
|
356615
357421
|
}
|
|
@@ -356674,32 +357480,42 @@ async function executeMobileTest(options) {
|
|
|
356674
357480
|
await startAppiumServer();
|
|
356675
357481
|
log2("Appium server started");
|
|
356676
357482
|
}
|
|
356677
|
-
log2("Discovering devices...");
|
|
356678
|
-
const devices = discoverAllDevices({ includeApps: true });
|
|
356679
357483
|
const platformFilter = options.target.platform;
|
|
356680
|
-
const
|
|
356681
|
-
|
|
356682
|
-
);
|
|
356683
|
-
if (
|
|
356684
|
-
|
|
356685
|
-
|
|
357484
|
+
const pinnedDeviceId = options.target.assignedDeviceId;
|
|
357485
|
+
const candidateSnapshot = pinnedDeviceSnapshot;
|
|
357486
|
+
const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && candidateSnapshot.deviceId === pinnedDeviceId && Date.now() - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
|
|
357487
|
+
if (reusablePinnedSnapshot) {
|
|
357488
|
+
assertDeviceNotQuarantined(reusablePinnedSnapshot.device.id);
|
|
357489
|
+
selectedDevice = reusablePinnedSnapshot.device;
|
|
357490
|
+
log2(`Reusing known device (skipped device re-scan): ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357491
|
+
} else {
|
|
357492
|
+
log2("Discovering devices...");
|
|
357493
|
+
const devices = discoverAllDevices({ includeApps: true });
|
|
357494
|
+
const matchingDevices = devices.filter(
|
|
357495
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
356686
357496
|
);
|
|
356687
|
-
|
|
356688
|
-
if (options.target.assignedDeviceId) {
|
|
356689
|
-
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
356690
|
-
if (!selectedDevice) {
|
|
357497
|
+
if (matchingDevices.length === 0) {
|
|
356691
357498
|
throw new Error(
|
|
356692
|
-
`
|
|
357499
|
+
`No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
|
|
356693
357500
|
);
|
|
356694
357501
|
}
|
|
357502
|
+
if (options.target.assignedDeviceId) {
|
|
357503
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
357504
|
+
if (!selectedDevice) {
|
|
357505
|
+
throw new Error(
|
|
357506
|
+
`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.`
|
|
357507
|
+
);
|
|
357508
|
+
}
|
|
357509
|
+
}
|
|
357510
|
+
if (!selectedDevice && options.target.recordedDeviceId) {
|
|
357511
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
|
|
357512
|
+
}
|
|
357513
|
+
if (!selectedDevice) {
|
|
357514
|
+
selectedDevice = matchingDevices[0];
|
|
357515
|
+
}
|
|
357516
|
+
log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357517
|
+
pinnedDeviceSnapshot = options.target.assignedDeviceId ? { deviceId: options.target.assignedDeviceId, device: selectedDevice, at: Date.now() } : null;
|
|
356695
357518
|
}
|
|
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
357519
|
try {
|
|
356704
357520
|
capture = await startMobileNetworkCapture({
|
|
356705
357521
|
platform: options.target.platform,
|
|
@@ -356766,7 +357582,14 @@ async function executeMobileTest(options) {
|
|
|
356766
357582
|
const resetAllowed = shouldResetMobileAppData(options.target.platform, selectedDevice.id);
|
|
356767
357583
|
if (resetAllowed) {
|
|
356768
357584
|
try {
|
|
356769
|
-
|
|
357585
|
+
if (options.target.platform === "IOS") {
|
|
357586
|
+
await terminateTargetApp(sessionId, "IOS", options.target.appId).catch(() => {
|
|
357587
|
+
});
|
|
357588
|
+
const wiped = await clearIosSimulatorAppData(selectedDevice.id, options.target.appId);
|
|
357589
|
+
if (!wiped.ok) throw new Error(wiped.detail);
|
|
357590
|
+
} else {
|
|
357591
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
357592
|
+
}
|
|
356770
357593
|
clearedAppDataBeforeLaunch = true;
|
|
356771
357594
|
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
357595
|
} catch (err) {
|
|
@@ -356819,17 +357642,21 @@ async function executeMobileTest(options) {
|
|
|
356819
357642
|
const entryStep = selectEntryDriftGateStep(runSteps, runPlatform);
|
|
356820
357643
|
if (entryStep) {
|
|
356821
357644
|
try {
|
|
356822
|
-
const entryTimeout =
|
|
357645
|
+
const entryTimeout = Math.max(DEFAULT_STEP_TIMEOUT_MS, COLD_BOOT_ENTRY_TIMEOUT_MS);
|
|
356823
357646
|
const entryWebCss = stepWebViewCss(entryStep);
|
|
356824
357647
|
if (entryWebCss) {
|
|
356825
357648
|
await executeInWebViewContext(
|
|
356826
357649
|
sessionId,
|
|
356827
357650
|
entryStep,
|
|
356828
357651
|
entryWebCss,
|
|
356829
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout).then(() => void 0)
|
|
357652
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout, { disambiguate: false }).then(() => void 0)
|
|
356830
357653
|
);
|
|
356831
357654
|
} else {
|
|
356832
|
-
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357655
|
+
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357656
|
+
identityOnly: true,
|
|
357657
|
+
platform: runPlatform,
|
|
357658
|
+
disambiguate: false
|
|
357659
|
+
});
|
|
356833
357660
|
}
|
|
356834
357661
|
} catch (err) {
|
|
356835
357662
|
if (isTransportError(err)) throw err;
|
|
@@ -356889,10 +357716,10 @@ async function executeMobileTest(options) {
|
|
|
356889
357716
|
await executeWaitForElement(sessionId, step, runPlatform);
|
|
356890
357717
|
break;
|
|
356891
357718
|
case "assertVisible":
|
|
356892
|
-
await executeAssertVisible(sessionId, step, runPlatform);
|
|
357719
|
+
await executeAssertVisible(sessionId, step, runPlatform, isCrossPlatform);
|
|
356893
357720
|
break;
|
|
356894
357721
|
case "assertText":
|
|
356895
|
-
await executeAssertText(sessionId, step, runPlatform);
|
|
357722
|
+
await executeAssertText(sessionId, step, runPlatform, isCrossPlatform);
|
|
356896
357723
|
break;
|
|
356897
357724
|
default: {
|
|
356898
357725
|
const _exhaustive = step.action;
|
|
@@ -356911,7 +357738,7 @@ async function executeMobileTest(options) {
|
|
|
356911
357738
|
environmental = stepError;
|
|
356912
357739
|
}
|
|
356913
357740
|
}
|
|
356914
|
-
const screenshot = await takeScreenshot(sessionId).catch(() => null);
|
|
357741
|
+
const screenshot = captureStepScreenshots ? await takeScreenshot(sessionId).catch(() => null) : null;
|
|
356915
357742
|
screenshots.push(screenshot ?? "");
|
|
356916
357743
|
if (status === "failed") {
|
|
356917
357744
|
await recordFirstFailingSource();
|
|
@@ -357173,10 +358000,10 @@ async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
|
357173
358000
|
await executeWaitForElement(sessionId, step, platform3);
|
|
357174
358001
|
return true;
|
|
357175
358002
|
case "assertVisible":
|
|
357176
|
-
await executeAssertVisible(sessionId, step, platform3);
|
|
358003
|
+
await executeAssertVisible(sessionId, step, platform3, true);
|
|
357177
358004
|
return true;
|
|
357178
358005
|
case "assertText":
|
|
357179
|
-
await executeAssertText(sessionId, step, platform3);
|
|
358006
|
+
await executeAssertText(sessionId, step, platform3, true);
|
|
357180
358007
|
return true;
|
|
357181
358008
|
default:
|
|
357182
358009
|
break;
|
|
@@ -357258,7 +358085,15 @@ async function harvestPlatformLocators(options) {
|
|
|
357258
358085
|
const identityStrategies = webViewCss ? [] : buildIdentityLocatorStrategies(step.target);
|
|
357259
358086
|
if (!webViewCss && identityStrategies.length > 0 && step.target) {
|
|
357260
358087
|
try {
|
|
357261
|
-
elementId = await findElement(
|
|
358088
|
+
elementId = await findElement(
|
|
358089
|
+
session.sessionId,
|
|
358090
|
+
step.target,
|
|
358091
|
+
Math.max(DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS),
|
|
358092
|
+
{
|
|
358093
|
+
identityOnly: true,
|
|
358094
|
+
failClosedOnAmbiguity: true
|
|
358095
|
+
}
|
|
358096
|
+
);
|
|
357262
358097
|
} catch (err) {
|
|
357263
358098
|
if (isTransportError(err)) throw err;
|
|
357264
358099
|
elementId = null;
|
|
@@ -357354,6 +358189,86 @@ function validateRunnerMobileTarget(value) {
|
|
|
357354
358189
|
};
|
|
357355
358190
|
}
|
|
357356
358191
|
|
|
358192
|
+
// src/services/heartbeat-keepalive.ts
|
|
358193
|
+
var import_node_worker_threads = require("worker_threads");
|
|
358194
|
+
var KEEPALIVE_INTERVAL_MS = (() => {
|
|
358195
|
+
const raw = Number(process.env.VALIDATEQA_HEARTBEAT_KEEPALIVE_MS);
|
|
358196
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2e4;
|
|
358197
|
+
})();
|
|
358198
|
+
function isHeartbeatKeepaliveEnabled(env = process.env) {
|
|
358199
|
+
return (env.VALIDATEQA_HEARTBEAT_KEEPALIVE ?? "").trim().toLowerCase() !== "off";
|
|
358200
|
+
}
|
|
358201
|
+
var WORKER_SOURCE = `
|
|
358202
|
+
const { parentPort } = require('node:worker_threads');
|
|
358203
|
+
let url = null;
|
|
358204
|
+
let headers = null;
|
|
358205
|
+
let body = null;
|
|
358206
|
+
let intervalMs = 20000;
|
|
358207
|
+
let timer = null;
|
|
358208
|
+
|
|
358209
|
+
async function post() {
|
|
358210
|
+
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358211
|
+
try {
|
|
358212
|
+
await fetch(url, {
|
|
358213
|
+
method: 'POST',
|
|
358214
|
+
headers,
|
|
358215
|
+
body,
|
|
358216
|
+
signal: AbortSignal.timeout(10000),
|
|
358217
|
+
});
|
|
358218
|
+
} catch {
|
|
358219
|
+
// Best-effort by design: the next tick retries; the main loop's own
|
|
358220
|
+
// heartbeat error handling owns logging/upgrade signals.
|
|
358221
|
+
}
|
|
358222
|
+
}
|
|
358223
|
+
|
|
358224
|
+
parentPort.on('message', (msg) => {
|
|
358225
|
+
if (!msg || typeof msg !== 'object') return;
|
|
358226
|
+
if (msg.type === 'update') {
|
|
358227
|
+
url = msg.url;
|
|
358228
|
+
headers = msg.headers;
|
|
358229
|
+
body = msg.body;
|
|
358230
|
+
intervalMs = msg.intervalMs || intervalMs;
|
|
358231
|
+
if (!timer) {
|
|
358232
|
+
timer = setInterval(post, intervalMs);
|
|
358233
|
+
if (timer.unref) timer.unref();
|
|
358234
|
+
}
|
|
358235
|
+
} else if (msg.type === 'stop') {
|
|
358236
|
+
if (timer) clearInterval(timer);
|
|
358237
|
+
timer = null;
|
|
358238
|
+
parentPort.close();
|
|
358239
|
+
}
|
|
358240
|
+
});
|
|
358241
|
+
`;
|
|
358242
|
+
var NOOP_KEEPALIVE = { update: () => {
|
|
358243
|
+
}, stop: () => {
|
|
358244
|
+
} };
|
|
358245
|
+
function startHeartbeatKeepalive(options = {}) {
|
|
358246
|
+
if (!isHeartbeatKeepaliveEnabled(options.env)) return NOOP_KEEPALIVE;
|
|
358247
|
+
const intervalMs = options.intervalMs ?? KEEPALIVE_INTERVAL_MS;
|
|
358248
|
+
let worker;
|
|
358249
|
+
try {
|
|
358250
|
+
worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true });
|
|
358251
|
+
worker.unref();
|
|
358252
|
+
worker.on("error", () => {
|
|
358253
|
+
worker = null;
|
|
358254
|
+
});
|
|
358255
|
+
} catch {
|
|
358256
|
+
return NOOP_KEEPALIVE;
|
|
358257
|
+
}
|
|
358258
|
+
return {
|
|
358259
|
+
update(url, headers, body) {
|
|
358260
|
+
worker?.postMessage({ type: "update", url, headers, body, intervalMs });
|
|
358261
|
+
},
|
|
358262
|
+
stop() {
|
|
358263
|
+
try {
|
|
358264
|
+
worker?.postMessage({ type: "stop" });
|
|
358265
|
+
} catch {
|
|
358266
|
+
}
|
|
358267
|
+
worker = null;
|
|
358268
|
+
}
|
|
358269
|
+
};
|
|
358270
|
+
}
|
|
358271
|
+
|
|
357357
358272
|
// src/mobile-device-queue.ts
|
|
357358
358273
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
357359
358274
|
if (!ctx) return false;
|
|
@@ -357424,10 +358339,10 @@ function decideMobileDeviceGate(input) {
|
|
|
357424
358339
|
|
|
357425
358340
|
// src/services/mobile-bridge.ts
|
|
357426
358341
|
var import_http = require("http");
|
|
357427
|
-
var
|
|
358342
|
+
var import_child_process4 = require("child_process");
|
|
357428
358343
|
var import_crypto = require("crypto");
|
|
357429
358344
|
var import_os4 = require("os");
|
|
357430
|
-
var
|
|
358345
|
+
var import_path4 = require("path");
|
|
357431
358346
|
var import_fs3 = require("fs");
|
|
357432
358347
|
init_dist();
|
|
357433
358348
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
@@ -357534,7 +358449,7 @@ async function getWindowSize3() {
|
|
|
357534
358449
|
}
|
|
357535
358450
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
357536
358451
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
357537
|
-
var BRIDGE_PID_FILE = (0,
|
|
358452
|
+
var BRIDGE_PID_FILE = (0, import_path4.join)((0, import_os4.homedir)(), ".validate.qa", "mobile-bridge.pid");
|
|
357538
358453
|
function readBridgePidFile() {
|
|
357539
358454
|
try {
|
|
357540
358455
|
const raw = (0, import_fs3.readFileSync)(BRIDGE_PID_FILE, "utf-8").trim();
|
|
@@ -357546,7 +358461,7 @@ function readBridgePidFile() {
|
|
|
357546
358461
|
}
|
|
357547
358462
|
function writeBridgePidFile(pid) {
|
|
357548
358463
|
try {
|
|
357549
|
-
(0, import_fs3.mkdirSync)((0,
|
|
358464
|
+
(0, import_fs3.mkdirSync)((0, import_path4.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
|
|
357550
358465
|
(0, import_fs3.writeFileSync)(BRIDGE_PID_FILE, String(pid), { mode: 384 });
|
|
357551
358466
|
} catch {
|
|
357552
358467
|
}
|
|
@@ -357561,19 +358476,19 @@ function killStaleBridge() {
|
|
|
357561
358476
|
const pid = readBridgePidFile();
|
|
357562
358477
|
if (!pid) return;
|
|
357563
358478
|
try {
|
|
357564
|
-
(0,
|
|
358479
|
+
(0, import_child_process4.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
|
|
357565
358480
|
} catch {
|
|
357566
358481
|
clearBridgePidFile();
|
|
357567
358482
|
return;
|
|
357568
358483
|
}
|
|
357569
358484
|
try {
|
|
357570
|
-
const pidsOnPort = (0,
|
|
358485
|
+
const pidsOnPort = (0, import_child_process4.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
|
|
357571
358486
|
const pidList = pidsOnPort.split("\n").filter(Boolean);
|
|
357572
358487
|
if (!pidList.includes(String(pid))) {
|
|
357573
358488
|
return;
|
|
357574
358489
|
}
|
|
357575
|
-
(0,
|
|
357576
|
-
(0,
|
|
358490
|
+
(0, import_child_process4.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
|
|
358491
|
+
(0, import_child_process4.execFileSync)("sleep", ["1"], { stdio: "ignore" });
|
|
357577
358492
|
} catch {
|
|
357578
358493
|
} finally {
|
|
357579
358494
|
clearBridgePidFile();
|
|
@@ -357846,6 +358761,13 @@ async function startSession(validated, res) {
|
|
|
357846
358761
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
357847
358762
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
357848
358763
|
"appium:noReset": true,
|
|
358764
|
+
// Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
|
|
358765
|
+
// Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
|
|
358766
|
+
// "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
|
|
358767
|
+
// skip the GUI open. Scoped to simulator UDIDs; APPIUM_IOS_HEADLESS=false
|
|
358768
|
+
// overrides. Verified live: without this, recorder/mirror sessions on iOS
|
|
358769
|
+
// die with "Simulator.app does not exist" while executor sessions work.
|
|
358770
|
+
...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
357849
358771
|
...realCaps
|
|
357850
358772
|
} : {
|
|
357851
358773
|
platformName: "Android",
|
|
@@ -357864,7 +358786,12 @@ async function startSession(validated, res) {
|
|
|
357864
358786
|
capabilities: {
|
|
357865
358787
|
alwaysMatch: capabilities
|
|
357866
358788
|
}
|
|
357867
|
-
})
|
|
358789
|
+
}),
|
|
358790
|
+
// Session creation is the one legitimately slow WebDriver call (iOS WDA
|
|
358791
|
+
// build/launch can take minutes on a cold toolchain) — same ceiling as
|
|
358792
|
+
// the executor's wdRequest session-create path. Without this, the shared
|
|
358793
|
+
// appiumRequest 10s default aborts every iOS recorder session.
|
|
358794
|
+
signal: AbortSignal.timeout(WD_SESSION_CREATE_TIMEOUT_MS)
|
|
357868
358795
|
});
|
|
357869
358796
|
state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
|
|
357870
358797
|
if (!state.appiumSessionId) {
|
|
@@ -358642,6 +359569,9 @@ function detachMirrorSession(deviceId) {
|
|
|
358642
359569
|
stopAllMirrorClients();
|
|
358643
359570
|
stopSessionReaper();
|
|
358644
359571
|
}
|
|
359572
|
+
function getMirrorSessionId(deviceId) {
|
|
359573
|
+
return runnerChannels.get(deviceId)?.sessionId;
|
|
359574
|
+
}
|
|
358645
359575
|
|
|
358646
359576
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
358647
359577
|
var import_runner_core9 = __toESM(require_dist2());
|
|
@@ -358672,6 +359602,7 @@ var MobileBridgeDriver = class {
|
|
|
358672
359602
|
sessionId;
|
|
358673
359603
|
platform;
|
|
358674
359604
|
appId;
|
|
359605
|
+
deviceId;
|
|
358675
359606
|
allowAppReset;
|
|
358676
359607
|
recoverSession;
|
|
358677
359608
|
maxSessionRecoveries;
|
|
@@ -358692,6 +359623,7 @@ var MobileBridgeDriver = class {
|
|
|
358692
359623
|
this.sessionId = config.sessionId;
|
|
358693
359624
|
this.platform = config.platform;
|
|
358694
359625
|
this.appId = config.appId;
|
|
359626
|
+
this.deviceId = config.deviceId;
|
|
358695
359627
|
this.allowAppReset = config.allowAppReset === true;
|
|
358696
359628
|
this.recoverSession = config.recoverSession;
|
|
358697
359629
|
this.maxSessionRecoveries = Math.max(0, Math.floor(config.maxSessionRecoveries ?? 1));
|
|
@@ -358989,17 +359921,34 @@ var MobileBridgeDriver = class {
|
|
|
358989
359921
|
});
|
|
358990
359922
|
}
|
|
358991
359923
|
/**
|
|
358992
|
-
* Deterministic state reset: clear app data + relaunch
|
|
358993
|
-
*
|
|
358994
|
-
*
|
|
358995
|
-
*
|
|
358996
|
-
*
|
|
359924
|
+
* Deterministic state reset: clear app data + relaunch, per the
|
|
359925
|
+
* runner-computed allowAppReset policy (Android emulators / iOS simulators
|
|
359926
|
+
* only — a personal physical device is never wiped). Android clears via
|
|
359927
|
+
* `mobile: clearApp`; iOS terminates the app and wipes its simulator data
|
|
359928
|
+
* container + keychain through `simctl` (see ios-simulator-reset.ts).
|
|
359929
|
+
* Returns false when unsupported/disallowed or when the clear fails —
|
|
359930
|
+
* callers fall back to relaunch(), preserving today's behavior. On success,
|
|
359931
|
+
* waits for cold-boot content so recording and replay agree on the true
|
|
359932
|
+
* entry screen.
|
|
358997
359933
|
*/
|
|
358998
359934
|
async resetAppState() {
|
|
358999
|
-
if (!this.allowAppReset
|
|
359935
|
+
if (!this.allowAppReset) return false;
|
|
359936
|
+
if (this.platform === "IOS" && !isIosSimulatorUdid(this.deviceId)) return false;
|
|
359000
359937
|
return this.withSessionRecovery("app data reset", async () => {
|
|
359001
359938
|
try {
|
|
359002
|
-
|
|
359939
|
+
if (this.platform === "IOS") {
|
|
359940
|
+
try {
|
|
359941
|
+
await this.executeScript("mobile: terminateApp", [{ ...this.appLifecycleArgs(), timeout: 5e3 }]);
|
|
359942
|
+
} catch (err) {
|
|
359943
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
359944
|
+
}
|
|
359945
|
+
const wiped = await clearIosSimulatorAppData(this.deviceId ?? "", this.appId);
|
|
359946
|
+
if (!wiped.ok) {
|
|
359947
|
+
throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
|
|
359948
|
+
}
|
|
359949
|
+
} else {
|
|
359950
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
359951
|
+
}
|
|
359003
359952
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
359004
359953
|
try {
|
|
359005
359954
|
await waitForColdBootSettle(this.sessionPath);
|
|
@@ -359138,7 +360087,7 @@ function readActiveBundleId2(result) {
|
|
|
359138
360087
|
}
|
|
359139
360088
|
|
|
359140
360089
|
// src/services/doctor.ts
|
|
359141
|
-
var
|
|
360090
|
+
var import_child_process5 = require("child_process");
|
|
359142
360091
|
var import_fs4 = require("fs");
|
|
359143
360092
|
var import_os5 = require("os");
|
|
359144
360093
|
var C = {
|
|
@@ -359158,7 +360107,7 @@ function computeDoctorReadiness(web, ios, android) {
|
|
|
359158
360107
|
}
|
|
359159
360108
|
function run(cmd) {
|
|
359160
360109
|
try {
|
|
359161
|
-
return (0,
|
|
360110
|
+
return (0, import_child_process5.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
359162
360111
|
} catch {
|
|
359163
360112
|
return null;
|
|
359164
360113
|
}
|
|
@@ -359590,6 +360539,113 @@ function envInt(name, fallback) {
|
|
|
359590
360539
|
}
|
|
359591
360540
|
var MOBILE_DEVICE_CEILING = Math.max(0, envInt("MOBILE_DEVICE_CEILING", 0));
|
|
359592
360541
|
var MOBILE_HEAL_HOLD_MS = Math.max(0, envInt("MOBILE_HEAL_HOLD_MS", 2e4));
|
|
360542
|
+
var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 30 * 6e4));
|
|
360543
|
+
var MOBILE_RUN_ABORT_GRACE_MS = Math.max(0, envInt("MOBILE_RUN_ABORT_GRACE_MS", 2 * 6e4));
|
|
360544
|
+
var MOBILE_CANCEL_POLL_MS = Math.max(0, envInt("MOBILE_CANCEL_POLL_MS", 15e3));
|
|
360545
|
+
var MobileRunWatchdog = class {
|
|
360546
|
+
constructor(options) {
|
|
360547
|
+
this.options = options;
|
|
360548
|
+
}
|
|
360549
|
+
phase = "idle";
|
|
360550
|
+
released = false;
|
|
360551
|
+
quarantined = false;
|
|
360552
|
+
cancelAbortTimer = null;
|
|
360553
|
+
cancelGraceTimer = null;
|
|
360554
|
+
/** Current phase (exposed for tests / observability). */
|
|
360555
|
+
getPhase() {
|
|
360556
|
+
return this.phase;
|
|
360557
|
+
}
|
|
360558
|
+
/** Arm the wall-clock abort timer. Call once, immediately after dispatch. */
|
|
360559
|
+
start() {
|
|
360560
|
+
if (this.phase !== "idle") return;
|
|
360561
|
+
this.phase = "active";
|
|
360562
|
+
if (this.options.abortMs > 0) {
|
|
360563
|
+
this.cancelAbortTimer = this.options.schedule(
|
|
360564
|
+
() => this.triggerAbort(`exceeded the ${Math.round(this.options.abortMs / 6e4)}min wall-clock ceiling`),
|
|
360565
|
+
this.options.abortMs
|
|
360566
|
+
);
|
|
360567
|
+
}
|
|
360568
|
+
}
|
|
360569
|
+
/** Stage 1. Idempotent (a no-op unless still ACTIVE). Also called by the
|
|
360570
|
+
* cancellation observer. Quarantines the device (blocking the run's own
|
|
360571
|
+
* session-recovery from resurrecting the killed session — observed live),
|
|
360572
|
+
* kills the run's session (best-effort), and starts the grace clock;
|
|
360573
|
+
* deliberately does NOT release the lease. */
|
|
360574
|
+
triggerAbort(reason) {
|
|
360575
|
+
if (this.phase !== "active") return;
|
|
360576
|
+
this.phase = "aborting";
|
|
360577
|
+
this.clearAbortTimer();
|
|
360578
|
+
if (this.options.deviceId) {
|
|
360579
|
+
this.quarantined = true;
|
|
360580
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360581
|
+
this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
|
|
360582
|
+
} else {
|
|
360583
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
|
|
360584
|
+
}
|
|
360585
|
+
if (this.options.graceMs > 0) {
|
|
360586
|
+
this.cancelGraceTimer = this.options.schedule(() => this.forceRelease(), this.options.graceMs);
|
|
360587
|
+
} else {
|
|
360588
|
+
this.forceRelease();
|
|
360589
|
+
}
|
|
360590
|
+
}
|
|
360591
|
+
/** The run's promise settled (normal completion, or after an abort). The single
|
|
360592
|
+
* release owner: releases at most once and lifts any quarantine. Idempotent. */
|
|
360593
|
+
settle() {
|
|
360594
|
+
if (this.phase === "settled") return;
|
|
360595
|
+
const wasForceReleased = this.released;
|
|
360596
|
+
this.phase = "settled";
|
|
360597
|
+
this.clearAbortTimer();
|
|
360598
|
+
this.clearGraceTimer();
|
|
360599
|
+
if (!this.released) {
|
|
360600
|
+
this.released = true;
|
|
360601
|
+
this.options.callbacks.release();
|
|
360602
|
+
}
|
|
360603
|
+
if (this.quarantined && this.options.deviceId) {
|
|
360604
|
+
this.quarantined = false;
|
|
360605
|
+
this.options.callbacks.unquarantine(this.options.deviceId);
|
|
360606
|
+
if (wasForceReleased) {
|
|
360607
|
+
this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
|
|
360608
|
+
}
|
|
360609
|
+
}
|
|
360610
|
+
}
|
|
360611
|
+
/** Stage 2. The run did not settle within the grace window after the abort. Force-
|
|
360612
|
+
* release the slot to unwedge capacity; the device stays quarantined (since the
|
|
360613
|
+
* stage-1 abort) until the promise eventually settles. Idempotent and guarded
|
|
360614
|
+
* against a late settle. */
|
|
360615
|
+
forceRelease() {
|
|
360616
|
+
if (this.released || this.phase === "settled") return;
|
|
360617
|
+
this.clearGraceTimer();
|
|
360618
|
+
this.released = true;
|
|
360619
|
+
this.options.callbacks.release();
|
|
360620
|
+
if (this.options.deviceId) {
|
|
360621
|
+
if (!this.quarantined) {
|
|
360622
|
+
this.quarantined = true;
|
|
360623
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360624
|
+
}
|
|
360625
|
+
this.options.callbacks.log(
|
|
360626
|
+
"warn",
|
|
360627
|
+
`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.`
|
|
360628
|
+
);
|
|
360629
|
+
} else {
|
|
360630
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} did not settle after abort \u2014 force-released its slot to unwedge mobile capacity.`);
|
|
360631
|
+
}
|
|
360632
|
+
}
|
|
360633
|
+
clearAbortTimer() {
|
|
360634
|
+
if (this.cancelAbortTimer) {
|
|
360635
|
+
this.cancelAbortTimer();
|
|
360636
|
+
this.cancelAbortTimer = null;
|
|
360637
|
+
}
|
|
360638
|
+
}
|
|
360639
|
+
clearGraceTimer() {
|
|
360640
|
+
if (this.cancelGraceTimer) {
|
|
360641
|
+
this.cancelGraceTimer();
|
|
360642
|
+
this.cancelGraceTimer = null;
|
|
360643
|
+
}
|
|
360644
|
+
}
|
|
360645
|
+
};
|
|
360646
|
+
function isServerCancelledRunStatus(status) {
|
|
360647
|
+
return status === "ERROR" || status === "CANCELLED" || status === "CANCELLING";
|
|
360648
|
+
}
|
|
359593
360649
|
function scrubAuditMessages(messages, testVariables) {
|
|
359594
360650
|
if (messages === void 0 || messages === null) return messages;
|
|
359595
360651
|
const compacted = compactForAudit(messages);
|
|
@@ -359665,7 +360721,7 @@ var log = {
|
|
|
359665
360721
|
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
359666
360722
|
};
|
|
359667
360723
|
function getRunsDir() {
|
|
359668
|
-
const dir = process.env.RUNS_DIR || (0,
|
|
360724
|
+
const dir = process.env.RUNS_DIR || (0, import_path5.join)((0, import_os6.homedir)(), ".validate.qa", "runs");
|
|
359669
360725
|
(0, import_runner_core12.ensurePrivateDirectory)(dir);
|
|
359670
360726
|
return dir;
|
|
359671
360727
|
}
|
|
@@ -361151,9 +362207,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361151
362207
|
// flip the test to needs_manual_authoring), so an environmentally
|
|
361152
362208
|
// aborted pass — wrong screen, system dialog, transport flake —
|
|
361153
362209
|
// must not persist step divergences it could not fairly judge.
|
|
361154
|
-
//
|
|
361155
|
-
//
|
|
361156
|
-
|
|
362210
|
+
// The same applies to a harvest that ADVANCED THROUGH NOTHING
|
|
362211
|
+
// (reachedOrder null): it never got past the entry screen, so its
|
|
362212
|
+
// step judgments are cold-boot/entry noise, not app knowledge —
|
|
362213
|
+
// observed live: a slow WKWebView hydration marked step 1
|
|
362214
|
+
// divergent and permanently dead-ended the test in
|
|
362215
|
+
// needs_manual_authoring with no retry path. Harvested bundles
|
|
362216
|
+
// are still posted: they were positively resolved and the merge
|
|
362217
|
+
// is idempotent.
|
|
362218
|
+
divergentOrders: harvest.environmentalError || harvest.reachedOrder == null ? [] : harvest.divergentOrders,
|
|
361157
362219
|
reachedOrder: harvest.reachedOrder,
|
|
361158
362220
|
logs: harvest.logs,
|
|
361159
362221
|
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
@@ -361597,9 +362659,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361597
362659
|
sessionId: session.sessionId,
|
|
361598
362660
|
platform: platform3,
|
|
361599
362661
|
appId: session.appId,
|
|
361600
|
-
|
|
361601
|
-
//
|
|
361602
|
-
//
|
|
362662
|
+
deviceId: session.deviceId,
|
|
362663
|
+
// Deterministic-state authorization: Android emulators / iOS
|
|
362664
|
+
// simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
|
|
362665
|
+
// Android default) — a personal physical device must never have
|
|
362666
|
+
// its app data wiped.
|
|
361603
362667
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
361604
362668
|
maxSessionRecoveries: 1,
|
|
361605
362669
|
recoverSession: async (cause) => {
|
|
@@ -362115,6 +363179,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362115
363179
|
sessionId: opened.sessionId,
|
|
362116
363180
|
platform: target.platform,
|
|
362117
363181
|
appId: opened.appId,
|
|
363182
|
+
deviceId: opened.deviceId,
|
|
362118
363183
|
allowAppReset: target.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(target.platform, opened.deviceId)
|
|
362119
363184
|
});
|
|
362120
363185
|
};
|
|
@@ -362727,6 +363792,7 @@ async function startRunner(opts) {
|
|
|
362727
363792
|
else activeMobileOpsByPlatform.set(p, next);
|
|
362728
363793
|
};
|
|
362729
363794
|
const leasedDeviceIds = /* @__PURE__ */ new Map();
|
|
363795
|
+
const activeMobileRuns = /* @__PURE__ */ new Map();
|
|
362730
363796
|
let activeMobileDiscoveries = 0;
|
|
362731
363797
|
const mobileHealHold = /* @__PURE__ */ new Map();
|
|
362732
363798
|
const pruneMobileHealHolds = () => {
|
|
@@ -362807,6 +363873,7 @@ async function startRunner(opts) {
|
|
|
362807
363873
|
}
|
|
362808
363874
|
}
|
|
362809
363875
|
};
|
|
363876
|
+
const heartbeatKeepalive = startHeartbeatKeepalive();
|
|
362810
363877
|
const sendHeartbeat = async () => {
|
|
362811
363878
|
try {
|
|
362812
363879
|
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
@@ -362821,41 +363888,44 @@ async function startRunner(opts) {
|
|
|
362821
363888
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
362822
363889
|
lastInventoryKinds.clear();
|
|
362823
363890
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
362824
|
-
const
|
|
363891
|
+
const heartbeatUrl = `${serverUrl}/api/runner/heartbeat`;
|
|
363892
|
+
const heartbeatBody = JSON.stringify({
|
|
363893
|
+
runnerName: RUNNER_NAME,
|
|
363894
|
+
runnerVersion: RUNNER_VERSION,
|
|
363895
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
363896
|
+
runsCompleted,
|
|
363897
|
+
activeRuns,
|
|
363898
|
+
maxConcurrent: MAX_CONCURRENT,
|
|
363899
|
+
uptimeMs: Date.now() - startedAt,
|
|
363900
|
+
capabilities,
|
|
363901
|
+
devices,
|
|
363902
|
+
mobileBridge: bridge ? {
|
|
363903
|
+
online: bridge.running,
|
|
363904
|
+
port: bridge.port,
|
|
363905
|
+
tunnelUrl: bridge.tunnelUrl,
|
|
363906
|
+
tunnelToken: bridge.tunnelToken
|
|
363907
|
+
} : void 0,
|
|
363908
|
+
activeByType: Object.fromEntries(activeByType),
|
|
363909
|
+
activeByProject: Object.fromEntries(activeByProject),
|
|
363910
|
+
typeLimits: TYPE_LIMITS,
|
|
363911
|
+
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
363912
|
+
resourceSnapshot: resourceStatus.snapshot,
|
|
363913
|
+
resourceGovernor: resourceStatus.governor,
|
|
363914
|
+
processInventory,
|
|
363915
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
363916
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
363917
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
363918
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
363919
|
+
doctor: getCachedDoctor()
|
|
363920
|
+
});
|
|
363921
|
+
const response = await fetch(heartbeatUrl, {
|
|
362825
363922
|
method: "POST",
|
|
362826
363923
|
headers,
|
|
362827
|
-
body:
|
|
362828
|
-
runnerName: RUNNER_NAME,
|
|
362829
|
-
runnerVersion: RUNNER_VERSION,
|
|
362830
|
-
runnerBuildId: RUNNER_BUILD_ID,
|
|
362831
|
-
runsCompleted,
|
|
362832
|
-
activeRuns,
|
|
362833
|
-
maxConcurrent: MAX_CONCURRENT,
|
|
362834
|
-
uptimeMs: Date.now() - startedAt,
|
|
362835
|
-
capabilities,
|
|
362836
|
-
devices,
|
|
362837
|
-
mobileBridge: bridge ? {
|
|
362838
|
-
online: bridge.running,
|
|
362839
|
-
port: bridge.port,
|
|
362840
|
-
tunnelUrl: bridge.tunnelUrl,
|
|
362841
|
-
tunnelToken: bridge.tunnelToken
|
|
362842
|
-
} : void 0,
|
|
362843
|
-
activeByType: Object.fromEntries(activeByType),
|
|
362844
|
-
activeByProject: Object.fromEntries(activeByProject),
|
|
362845
|
-
typeLimits: TYPE_LIMITS,
|
|
362846
|
-
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
362847
|
-
resourceSnapshot: resourceStatus.snapshot,
|
|
362848
|
-
resourceGovernor: resourceStatus.governor,
|
|
362849
|
-
processInventory,
|
|
362850
|
-
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
362851
|
-
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
362852
|
-
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
362853
|
-
// Cached & cheap — see getCachedDoctor above.
|
|
362854
|
-
doctor: getCachedDoctor()
|
|
362855
|
-
}),
|
|
363924
|
+
body: heartbeatBody,
|
|
362856
363925
|
signal: AbortSignal.timeout(1e4)
|
|
362857
363926
|
});
|
|
362858
363927
|
if (response.ok) {
|
|
363928
|
+
heartbeatKeepalive.update(heartbeatUrl, headers, heartbeatBody);
|
|
362859
363929
|
const body = await response.json().catch(() => null);
|
|
362860
363930
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
362861
363931
|
void handleControlCommands(body.commands);
|
|
@@ -362926,6 +363996,7 @@ async function startRunner(opts) {
|
|
|
362926
363996
|
process.exit(1);
|
|
362927
363997
|
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
362928
363998
|
clearInterval(heartbeat);
|
|
363999
|
+
heartbeatKeepalive.stop();
|
|
362929
364000
|
if (liveBrowserHeartbeatTimer) {
|
|
362930
364001
|
clearTimeout(liveBrowserHeartbeatTimer);
|
|
362931
364002
|
liveBrowserHeartbeatTimer = null;
|
|
@@ -362977,6 +364048,32 @@ async function startRunner(opts) {
|
|
|
362977
364048
|
}
|
|
362978
364049
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
362979
364050
|
pidsWatchdog.unref();
|
|
364051
|
+
const pollMobileRunCancellations = async () => {
|
|
364052
|
+
if (shuttingDown || activeMobileRuns.size === 0) return;
|
|
364053
|
+
const runIds = [...activeMobileRuns.keys()];
|
|
364054
|
+
try {
|
|
364055
|
+
const res = await fetch(`${serverUrl}/api/runner/runs/status`, {
|
|
364056
|
+
method: "POST",
|
|
364057
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
364058
|
+
body: JSON.stringify({ runIds }),
|
|
364059
|
+
signal: AbortSignal.timeout(1e4)
|
|
364060
|
+
});
|
|
364061
|
+
if (!res.ok) return;
|
|
364062
|
+
const body = await res.json().catch(() => null);
|
|
364063
|
+
if (!body || !Array.isArray(body.runs)) return;
|
|
364064
|
+
for (const row of body.runs) {
|
|
364065
|
+
if (!row || typeof row.id !== "string" || typeof row.status !== "string") continue;
|
|
364066
|
+
if (!isServerCancelledRunStatus(row.status)) continue;
|
|
364067
|
+
const watchdog = activeMobileRuns.get(row.id);
|
|
364068
|
+
if (watchdog) watchdog.triggerAbort(`was cancelled server-side (status ${row.status})`);
|
|
364069
|
+
}
|
|
364070
|
+
} catch {
|
|
364071
|
+
}
|
|
364072
|
+
};
|
|
364073
|
+
const mobileCancelPoll = enableMobile && MOBILE_CANCEL_POLL_MS > 0 ? setInterval(() => {
|
|
364074
|
+
void pollMobileRunCancellations();
|
|
364075
|
+
}, MOBILE_CANCEL_POLL_MS) : null;
|
|
364076
|
+
mobileCancelPoll?.unref?.();
|
|
362980
364077
|
const deferralLogged = /* @__PURE__ */ new Map();
|
|
362981
364078
|
const logDeferredOnce = (runId, line) => {
|
|
362982
364079
|
if (deferralLogged.get(runId) === line) return;
|
|
@@ -363059,14 +364156,17 @@ async function startRunner(opts) {
|
|
|
363059
364156
|
});
|
|
363060
364157
|
const capacity = MOBILE_DEVICE_CEILING > 0 ? Math.min(derived, MOBILE_DEVICE_CEILING) : derived;
|
|
363061
364158
|
const leaseTarget = run2.target ?? run2.deviceRequirements;
|
|
363062
|
-
const
|
|
364159
|
+
const leaseExclusions = {
|
|
364160
|
+
has: (id) => leasedDeviceIds.has(id) || isDeviceQuarantined(id)
|
|
364161
|
+
};
|
|
364162
|
+
const leaseSelection = selectFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot, {
|
|
363063
364163
|
appId: typeof leaseTarget?.appId === "string" ? leaseTarget.appId : void 0,
|
|
363064
364164
|
recordedDeviceId: typeof leaseTarget?.recordedDeviceId === "string" ? leaseTarget.recordedDeviceId : void 0,
|
|
363065
364165
|
platformVersion: typeof leaseTarget?.platformVersion === "string" ? leaseTarget.platformVersion : void 0,
|
|
363066
364166
|
deviceName: typeof run2.deviceRequirements?.deviceName === "string" ? run2.deviceRequirements.deviceName : void 0,
|
|
363067
364167
|
resetPolicy: leaseTarget?.resetPolicy
|
|
363068
364168
|
});
|
|
363069
|
-
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform,
|
|
364169
|
+
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot) : null;
|
|
363070
364170
|
if (leaseSelection.kind === "unsatisfiable") {
|
|
363071
364171
|
mobileLeaseError = leaseSelection.reason;
|
|
363072
364172
|
log.warn(` Mobile requirements unsatisfiable for ${run2.runId}: ${leaseSelection.reason}; claiming once to report the configuration error`);
|
|
@@ -363124,6 +364224,54 @@ async function startRunner(opts) {
|
|
|
363124
364224
|
await new Promise((r) => setTimeout(r, staggerMs));
|
|
363125
364225
|
}
|
|
363126
364226
|
claimedThisPoll++;
|
|
364227
|
+
const applyRelease = () => {
|
|
364228
|
+
activeRuns--;
|
|
364229
|
+
decrementType(runMode);
|
|
364230
|
+
decrementProject(runProject);
|
|
364231
|
+
if (isMobileOp) {
|
|
364232
|
+
if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
|
|
364233
|
+
if (isDiscovery) activeMobileDiscoveries--;
|
|
364234
|
+
if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
|
|
364235
|
+
activeMobileRuns.delete(run2.runId);
|
|
364236
|
+
}
|
|
364237
|
+
};
|
|
364238
|
+
const runWatchdog = isMobileOp ? new MobileRunWatchdog({
|
|
364239
|
+
runId: run2.runId,
|
|
364240
|
+
deviceId: leasedDeviceId ?? void 0,
|
|
364241
|
+
abortMs: MOBILE_RUN_WATCHDOG_MS,
|
|
364242
|
+
graceMs: MOBILE_RUN_ABORT_GRACE_MS,
|
|
364243
|
+
// Real timers, unref'd so a pending watchdog never keeps the process
|
|
364244
|
+
// alive on shutdown.
|
|
364245
|
+
schedule: (fn, ms) => {
|
|
364246
|
+
const t = setTimeout(fn, ms);
|
|
364247
|
+
if (typeof t.unref === "function") t.unref();
|
|
364248
|
+
return () => clearTimeout(t);
|
|
364249
|
+
},
|
|
364250
|
+
callbacks: {
|
|
364251
|
+
abort: (deviceId, rid, reason) => {
|
|
364252
|
+
const sessionId = getMirrorSessionId(deviceId);
|
|
364253
|
+
log.warn(
|
|
364254
|
+
`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.`
|
|
364255
|
+
);
|
|
364256
|
+
if (sessionId) void deleteAppiumSession(sessionId);
|
|
364257
|
+
},
|
|
364258
|
+
release: applyRelease,
|
|
364259
|
+
quarantine: (deviceId) => {
|
|
364260
|
+
quarantineDevice(deviceId);
|
|
364261
|
+
},
|
|
364262
|
+
unquarantine: (deviceId) => {
|
|
364263
|
+
unquarantineDevice(deviceId);
|
|
364264
|
+
},
|
|
364265
|
+
log: (level, message) => {
|
|
364266
|
+
if (level === "warn") log.warn(message);
|
|
364267
|
+
else log.info(message);
|
|
364268
|
+
}
|
|
364269
|
+
}
|
|
364270
|
+
}) : null;
|
|
364271
|
+
if (runWatchdog) {
|
|
364272
|
+
activeMobileRuns.set(run2.runId, runWatchdog);
|
|
364273
|
+
runWatchdog.start();
|
|
364274
|
+
}
|
|
363127
364275
|
executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
|
|
363128
364276
|
if (mobilePlatform) {
|
|
363129
364277
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
@@ -363134,14 +364282,8 @@ async function startRunner(opts) {
|
|
|
363134
364282
|
const msg = err instanceof Error ? err.message : String(err);
|
|
363135
364283
|
log.warn(`Run ${run2.runId} crashed: ${msg}`);
|
|
363136
364284
|
}).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
|
-
}
|
|
364285
|
+
if (runWatchdog) runWatchdog.settle();
|
|
364286
|
+
else applyRelease();
|
|
363145
364287
|
});
|
|
363146
364288
|
}
|
|
363147
364289
|
}
|