@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.mjs
CHANGED
|
@@ -740,6 +740,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
740
740
|
exports.computeScreenHash = computeScreenHash;
|
|
741
741
|
exports.buildLocatorBundle = buildLocatorBundle;
|
|
742
742
|
exports.preferredLocator = preferredLocator;
|
|
743
|
+
exports.identityMatchInfo = identityMatchInfo;
|
|
743
744
|
exports.parseMobileSource = parseMobileSource2;
|
|
744
745
|
exports.resolveLocatorAgainstScreenDetailed = resolveLocatorAgainstScreenDetailed;
|
|
745
746
|
exports.resolveLocatorAgainstScreen = resolveLocatorAgainstScreen;
|
|
@@ -1354,16 +1355,34 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1354
1355
|
bundle.contentDesc = el.accessibilityId;
|
|
1355
1356
|
return bundle;
|
|
1356
1357
|
}
|
|
1357
|
-
function
|
|
1358
|
-
|
|
1358
|
+
function identityFieldValue(el, field) {
|
|
1359
|
+
if (field === "text") {
|
|
1360
|
+
return el.text && !isEditableElement(el) && !isStateValueElement(el) ? el.text : void 0;
|
|
1361
|
+
}
|
|
1362
|
+
return el[field];
|
|
1363
|
+
}
|
|
1364
|
+
function choosePreferredIdentityField(el) {
|
|
1359
1365
|
const candidates = [
|
|
1360
|
-
{ value: el
|
|
1361
|
-
{ value: el
|
|
1362
|
-
{ value:
|
|
1366
|
+
{ field: "accessibilityId", value: identityFieldValue(el, "accessibilityId"), unique: el.identityUnique?.accessibilityId },
|
|
1367
|
+
{ field: "resourceId", value: identityFieldValue(el, "resourceId"), unique: el.identityUnique?.resourceId },
|
|
1368
|
+
{ field: "text", value: identityFieldValue(el, "text"), unique: el.identityUnique?.text }
|
|
1363
1369
|
];
|
|
1364
1370
|
const uniqueFirst = candidates.find((c) => c.value && c.unique !== false);
|
|
1365
|
-
const chosen =
|
|
1366
|
-
return chosen
|
|
1371
|
+
const chosen = uniqueFirst ?? candidates.find((c) => c.value);
|
|
1372
|
+
return chosen?.value ? { field: chosen.field, value: chosen.value } : void 0;
|
|
1373
|
+
}
|
|
1374
|
+
function preferredLocator(el) {
|
|
1375
|
+
return choosePreferredIdentityField(el)?.value ?? el.xpath;
|
|
1376
|
+
}
|
|
1377
|
+
function identityMatchInfo(el, elements) {
|
|
1378
|
+
const chosen = choosePreferredIdentityField(el);
|
|
1379
|
+
if (!chosen)
|
|
1380
|
+
return void 0;
|
|
1381
|
+
if (el.identityUnique?.[chosen.field] !== false)
|
|
1382
|
+
return void 0;
|
|
1383
|
+
const sameValue = elements.filter((candidate) => identityFieldValue(candidate, chosen.field) === chosen.value);
|
|
1384
|
+
const matchIndex = sameValue.indexOf(el);
|
|
1385
|
+
return matchIndex >= 0 && sameValue.length > 1 ? { matchIndex, expectedMatchCount: sameValue.length } : void 0;
|
|
1367
1386
|
}
|
|
1368
1387
|
function buildScreenSignal(elements, root, platform3) {
|
|
1369
1388
|
const signal = {
|
|
@@ -2117,6 +2136,9 @@ function readMobileMetadata(value) {
|
|
|
2117
2136
|
return { screenName: "UnknownScreen" };
|
|
2118
2137
|
}
|
|
2119
2138
|
const locatorBundle = isPlainObject(value.locatorBundle) && typeof value.locatorBundle.xpath === "string" && typeof value.locatorBundle.className === "string" ? value.locatorBundle : void 0;
|
|
2139
|
+
const rawMatchIndex = value.matchIndex;
|
|
2140
|
+
const rawExpectedMatchCount = value.expectedMatchCount;
|
|
2141
|
+
const hasValidMatchPair = typeof rawMatchIndex === "number" && Number.isInteger(rawMatchIndex) && rawMatchIndex >= 0 && typeof rawExpectedMatchCount === "number" && Number.isInteger(rawExpectedMatchCount) && rawExpectedMatchCount > rawMatchIndex;
|
|
2120
2142
|
return {
|
|
2121
2143
|
screenName: typeof value.screenName === "string" && value.screenName.trim() ? value.screenName.trim() : "UnknownScreen",
|
|
2122
2144
|
targetLabel: typeof value.targetLabel === "string" && value.targetLabel.trim() ? value.targetLabel.trim() : void 0,
|
|
@@ -2131,7 +2153,9 @@ function readMobileMetadata(value) {
|
|
|
2131
2153
|
distance: typeof value.distance === "number" && Number.isFinite(value.distance) ? value.distance : void 0,
|
|
2132
2154
|
appContext: value.appContext === "WEBVIEW" ? "WEBVIEW" : value.appContext === "NATIVE" ? "NATIVE" : void 0,
|
|
2133
2155
|
webCss: typeof value.webCss === "string" && value.webCss.trim() ? value.webCss.trim() : void 0,
|
|
2134
|
-
webviewContext: typeof value.webviewContext === "string" && /^WEBVIEW/i.test(value.webviewContext.trim()) ? value.webviewContext.trim() : void 0
|
|
2156
|
+
webviewContext: typeof value.webviewContext === "string" && /^WEBVIEW/i.test(value.webviewContext.trim()) ? value.webviewContext.trim() : void 0,
|
|
2157
|
+
matchIndex: hasValidMatchPair ? rawMatchIndex : void 0,
|
|
2158
|
+
expectedMatchCount: hasValidMatchPair ? rawExpectedMatchCount : void 0
|
|
2135
2159
|
};
|
|
2136
2160
|
}
|
|
2137
2161
|
function nonEmpty(value) {
|
|
@@ -2140,6 +2164,9 @@ function nonEmpty(value) {
|
|
|
2140
2164
|
const trimmed = value.trim();
|
|
2141
2165
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
2142
2166
|
}
|
|
2167
|
+
function matchInstanceFields(metadata) {
|
|
2168
|
+
return metadata.matchIndex !== void 0 && metadata.expectedMatchCount !== void 0 ? { matchIndex: metadata.matchIndex, expectedMatchCount: metadata.expectedMatchCount } : {};
|
|
2169
|
+
}
|
|
2143
2170
|
function buildStepTarget(metadata, selector) {
|
|
2144
2171
|
const locatorBundle = metadata.locatorBundle;
|
|
2145
2172
|
const trimmedSelector = nonEmpty(selector);
|
|
@@ -2150,7 +2177,8 @@ function buildStepTarget(metadata, selector) {
|
|
|
2150
2177
|
return {
|
|
2151
2178
|
primary: trimmedSelector,
|
|
2152
2179
|
fallbacks: [],
|
|
2153
|
-
screenName: metadata.screenName
|
|
2180
|
+
screenName: metadata.screenName,
|
|
2181
|
+
...matchInstanceFields(metadata)
|
|
2154
2182
|
};
|
|
2155
2183
|
}
|
|
2156
2184
|
const webCss = nonEmpty(metadata.webCss) ?? (trimmedSelector?.startsWith("css=") ? trimmedSelector.slice(4) : void 0) ?? (nonEmpty(locatorBundle.xpath)?.startsWith("css=") ? locatorBundle.xpath.slice(4) : void 0);
|
|
@@ -2173,8 +2201,9 @@ function buildStepTarget(metadata, selector) {
|
|
|
2173
2201
|
primary,
|
|
2174
2202
|
fallbacks,
|
|
2175
2203
|
screenName: metadata.screenName,
|
|
2176
|
-
locatorBundle
|
|
2204
|
+
locatorBundle,
|
|
2177
2205
|
// Include the locatorBundle for better selector generation later
|
|
2206
|
+
...matchInstanceFields(metadata)
|
|
2178
2207
|
};
|
|
2179
2208
|
}
|
|
2180
2209
|
function labelForAction(action, metadata) {
|
|
@@ -2621,6 +2650,165 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
2621
2650
|
`main().catch((err) => { console.error(err); process.exitCode = 1; });`
|
|
2622
2651
|
].join("\n");
|
|
2623
2652
|
}
|
|
2653
|
+
function webviewStepLogicalId(step) {
|
|
2654
|
+
if (step.metadata?.appContext !== "WEBVIEW")
|
|
2655
|
+
return null;
|
|
2656
|
+
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) : "";
|
|
2657
|
+
const match = String(webCss ?? "").match(/^#([A-Za-z][\w-]*)$/);
|
|
2658
|
+
return match ? match[1].toLowerCase() : null;
|
|
2659
|
+
}
|
|
2660
|
+
function nativeStepResourceId(step) {
|
|
2661
|
+
const target = step.target;
|
|
2662
|
+
if (!target)
|
|
2663
|
+
return null;
|
|
2664
|
+
const xpaths = [target.locatorBundle?.xpath, ...target.fallbacks ?? []].filter((value) => typeof value === "string" && value.length > 0);
|
|
2665
|
+
for (const xpath of xpaths) {
|
|
2666
|
+
const match = xpath.match(/@resource-id=['"]([^'"]+)['"]/);
|
|
2667
|
+
if (match)
|
|
2668
|
+
return match[1].split("/").pop().toLowerCase();
|
|
2669
|
+
}
|
|
2670
|
+
const resourceId = nonEmpty(target.locatorBundle?.resourceId);
|
|
2671
|
+
if (resourceId)
|
|
2672
|
+
return resourceId.split("/").pop().toLowerCase();
|
|
2673
|
+
const primary = (target.primary ?? "").trim();
|
|
2674
|
+
if (/^[A-Za-z][\w-]*$/.test(primary) && !primary.startsWith("css="))
|
|
2675
|
+
return primary.toLowerCase();
|
|
2676
|
+
return null;
|
|
2677
|
+
}
|
|
2678
|
+
function renumberMobileSteps(steps) {
|
|
2679
|
+
steps.forEach((step, index) => {
|
|
2680
|
+
step.order = index;
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
function dropHybridNativeMirrorStepsInPlace(steps) {
|
|
2684
|
+
const webviewKeys = /* @__PURE__ */ new Set();
|
|
2685
|
+
for (const step of steps) {
|
|
2686
|
+
const id = webviewStepLogicalId(step);
|
|
2687
|
+
if (id)
|
|
2688
|
+
webviewKeys.add(`${step.target?.screenName ?? ""}::${id}`);
|
|
2689
|
+
}
|
|
2690
|
+
if (webviewKeys.size === 0)
|
|
2691
|
+
return 0;
|
|
2692
|
+
const kept = steps.filter((step) => {
|
|
2693
|
+
if (step.metadata?.appContext === "WEBVIEW")
|
|
2694
|
+
return true;
|
|
2695
|
+
if (step.action !== "tap" && step.action !== "type")
|
|
2696
|
+
return true;
|
|
2697
|
+
const id = nativeStepResourceId(step);
|
|
2698
|
+
if (id === null)
|
|
2699
|
+
return true;
|
|
2700
|
+
return !webviewKeys.has(`${step.target?.screenName ?? ""}::${id}`);
|
|
2701
|
+
});
|
|
2702
|
+
const removed = steps.length - kept.length;
|
|
2703
|
+
if (removed > 0) {
|
|
2704
|
+
steps.splice(0, steps.length, ...kept);
|
|
2705
|
+
renumberMobileSteps(steps);
|
|
2706
|
+
}
|
|
2707
|
+
return removed;
|
|
2708
|
+
}
|
|
2709
|
+
function strictOnMobileWanderFlag(value) {
|
|
2710
|
+
const v = (value || "").trim().toLowerCase();
|
|
2711
|
+
return v === "true" || v === "1";
|
|
2712
|
+
}
|
|
2713
|
+
function isMobileWanderSimplifyEnabled() {
|
|
2714
|
+
const value = (process.env.VALIDATEQA_MOBILE_WANDER_SIMPLIFY || "").trim().toLowerCase();
|
|
2715
|
+
return value === "on" || value === "true" || value === "1";
|
|
2716
|
+
}
|
|
2717
|
+
function mobileWanderTraceEnabled() {
|
|
2718
|
+
return strictOnMobileWanderFlag(process.env.VALIDATEQA_MOBILE_WANDER_TRACE);
|
|
2719
|
+
}
|
|
2720
|
+
function isMobileWanderCommitTapText(text) {
|
|
2721
|
+
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);
|
|
2722
|
+
if (isChooser)
|
|
2723
|
+
return false;
|
|
2724
|
+
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);
|
|
2725
|
+
}
|
|
2726
|
+
function isMobileWanderCommitStep(step) {
|
|
2727
|
+
if (step.action === "type" && typeof step.value === "string" && /\n$/.test(step.value))
|
|
2728
|
+
return true;
|
|
2729
|
+
if (step.action === "tap") {
|
|
2730
|
+
const text = [step.description, step.target?.primary].filter((v) => typeof v === "string" && v.length > 0).join(" ");
|
|
2731
|
+
return isMobileWanderCommitTapText(text);
|
|
2732
|
+
}
|
|
2733
|
+
return false;
|
|
2734
|
+
}
|
|
2735
|
+
function isMobileWanderStateToggleTap(step) {
|
|
2736
|
+
return step.action === "tap" && isStateValueMobileClassName(step.target?.locatorBundle?.className);
|
|
2737
|
+
}
|
|
2738
|
+
function simplifyMobileWanderingInPlace(steps, trace) {
|
|
2739
|
+
let removed = 0;
|
|
2740
|
+
const afterAdjacentDupes = [];
|
|
2741
|
+
for (const step of steps) {
|
|
2742
|
+
const previous = afterAdjacentDupes[afterAdjacentDupes.length - 1];
|
|
2743
|
+
const primary = step.target?.primary;
|
|
2744
|
+
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);
|
|
2745
|
+
if (isAdjacentDuplicateTap) {
|
|
2746
|
+
removed += 1;
|
|
2747
|
+
trace?.(`dropped adjacent duplicate tap "${primary}" (exact repeat, nothing between)`);
|
|
2748
|
+
afterAdjacentDupes[afterAdjacentDupes.length - 1] = step;
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
afterAdjacentDupes.push(step);
|
|
2752
|
+
}
|
|
2753
|
+
const lastTypeIndexByPrimary = /* @__PURE__ */ new Map();
|
|
2754
|
+
const refocusTapIndexesByPrimary = /* @__PURE__ */ new Map();
|
|
2755
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
2756
|
+
afterAdjacentDupes.forEach((step, index) => {
|
|
2757
|
+
if (step.action === "assertVisible" || step.action === "assertText" || step.action === "waitForElement") {
|
|
2758
|
+
lastTypeIndexByPrimary.clear();
|
|
2759
|
+
refocusTapIndexesByPrimary.clear();
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
if (step.action === "clear") {
|
|
2763
|
+
const primary2 = step.target?.primary;
|
|
2764
|
+
if (primary2) {
|
|
2765
|
+
lastTypeIndexByPrimary.delete(primary2);
|
|
2766
|
+
refocusTapIndexesByPrimary.delete(primary2);
|
|
2767
|
+
}
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (isMobileWanderCommitStep(step)) {
|
|
2771
|
+
lastTypeIndexByPrimary.clear();
|
|
2772
|
+
refocusTapIndexesByPrimary.clear();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (step.action === "tap") {
|
|
2776
|
+
const primary2 = step.target?.primary;
|
|
2777
|
+
if (primary2) {
|
|
2778
|
+
const list = refocusTapIndexesByPrimary.get(primary2) ?? [];
|
|
2779
|
+
list.push(index);
|
|
2780
|
+
refocusTapIndexesByPrimary.set(primary2, list);
|
|
2781
|
+
}
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
if (step.action !== "type")
|
|
2785
|
+
return;
|
|
2786
|
+
const primary = step.target?.primary;
|
|
2787
|
+
if (!primary)
|
|
2788
|
+
return;
|
|
2789
|
+
const priorIndex = lastTypeIndexByPrimary.get(primary);
|
|
2790
|
+
if (priorIndex !== void 0) {
|
|
2791
|
+
toDrop.add(priorIndex);
|
|
2792
|
+
removed += 1;
|
|
2793
|
+
trace?.(`dropped superseded type into "${primary}" (a later type into the same field wins)`);
|
|
2794
|
+
for (const tapIndex of refocusTapIndexesByPrimary.get(primary) ?? []) {
|
|
2795
|
+
if (tapIndex > priorIndex && tapIndex < index && !toDrop.has(tapIndex)) {
|
|
2796
|
+
toDrop.add(tapIndex);
|
|
2797
|
+
removed += 1;
|
|
2798
|
+
trace?.(`dropped redundant re-focus tap on "${primary}" (it only re-focused a field whose write was superseded)`);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
lastTypeIndexByPrimary.set(primary, index);
|
|
2803
|
+
refocusTapIndexesByPrimary.set(primary, []);
|
|
2804
|
+
});
|
|
2805
|
+
if (removed === 0)
|
|
2806
|
+
return 0;
|
|
2807
|
+
const kept = afterAdjacentDupes.filter((_, index) => !toDrop.has(index));
|
|
2808
|
+
steps.splice(0, steps.length, ...kept);
|
|
2809
|
+
renumberMobileSteps(steps);
|
|
2810
|
+
return removed;
|
|
2811
|
+
}
|
|
2624
2812
|
function generateMobileTestFromSession(input) {
|
|
2625
2813
|
if (input.medium !== "IOS_NATIVE" && input.medium !== "ANDROID_NATIVE") {
|
|
2626
2814
|
throw new Error(`Mobile test generation requires IOS_NATIVE or ANDROID_NATIVE medium (received ${input.medium})`);
|
|
@@ -2664,12 +2852,20 @@ function generateMobileTestFromSession(input) {
|
|
|
2664
2852
|
if (steps.length === 0) {
|
|
2665
2853
|
throw new Error("No mobile actions were recorded for this session");
|
|
2666
2854
|
}
|
|
2855
|
+
dropHybridNativeMirrorStepsInPlace(steps);
|
|
2856
|
+
const wanderTraceMessages = [];
|
|
2857
|
+
const wanderTraceSink = (message) => {
|
|
2858
|
+
wanderTraceMessages.push(message);
|
|
2859
|
+
};
|
|
2860
|
+
if (isMobileWanderSimplifyEnabled()) {
|
|
2861
|
+
simplifyMobileWanderingInPlace(steps, mobileWanderTraceEnabled() ? wanderTraceSink : void 0);
|
|
2862
|
+
}
|
|
2667
2863
|
const interactionSteps = steps.filter((step) => step.action !== "launchApp");
|
|
2668
2864
|
const runnableInteractions = interactionSteps.filter(isRunnableStep);
|
|
2669
2865
|
if (interactionSteps.length === 0 || runnableInteractions.length === 0) {
|
|
2670
2866
|
throw new Error("Generated mobile test has no runnable steps \u2014 re-record the flow so each interaction targets an on-screen element.");
|
|
2671
2867
|
}
|
|
2672
|
-
const warnings = [];
|
|
2868
|
+
const warnings = wanderTraceMessages.map((message) => `[wander] ${message}`);
|
|
2673
2869
|
const unrunnable = interactionSteps.length - runnableInteractions.length;
|
|
2674
2870
|
if (unrunnable > 0) {
|
|
2675
2871
|
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.`);
|
|
@@ -6226,6 +6422,7 @@ __export(dist_exports, {
|
|
|
6226
6422
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
6227
6423
|
isIdentityMobileXpath: () => isIdentityMobileXpath,
|
|
6228
6424
|
isMobilePlatform: () => isMobilePlatform,
|
|
6425
|
+
isMobileWanderSimplifyEnabled: () => isMobileWanderSimplifyEnabled,
|
|
6229
6426
|
isPricingPlan: () => isPricingPlan,
|
|
6230
6427
|
isRunnableStep: () => isRunnableStep,
|
|
6231
6428
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
@@ -6247,6 +6444,7 @@ __export(dist_exports, {
|
|
|
6247
6444
|
mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
|
|
6248
6445
|
mobileLocatorBundleHasIdentity: () => mobileLocatorBundleHasIdentity,
|
|
6249
6446
|
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
6447
|
+
mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
|
|
6250
6448
|
normalizeControlName: () => normalizeControlName,
|
|
6251
6449
|
normalizeTtsVoiceId: () => normalizeTtsVoiceId,
|
|
6252
6450
|
otherMobilePlatform: () => otherMobilePlatform,
|
|
@@ -6275,6 +6473,7 @@ __export(dist_exports, {
|
|
|
6275
6473
|
scrubUrlPropertyBag: () => scrubUrlPropertyBag,
|
|
6276
6474
|
selectorCandidatesForStep: () => selectorCandidatesForStep,
|
|
6277
6475
|
selectorForValue: () => selectorForValue,
|
|
6476
|
+
simplifyMobileWanderingInPlace: () => simplifyMobileWanderingInPlace,
|
|
6278
6477
|
stepAppliesToPlatform: () => stepAppliesToPlatform,
|
|
6279
6478
|
stepIsDivergentForPlatform: () => stepIsDivergentForPlatform,
|
|
6280
6479
|
stepsMissingPlatformLocators: () => stepsMissingPlatformLocators,
|
|
@@ -10561,7 +10760,7 @@ var require_sonic_boom = __commonJS({
|
|
|
10561
10760
|
if (!(this instanceof SonicBoom)) {
|
|
10562
10761
|
return new SonicBoom(opts);
|
|
10563
10762
|
}
|
|
10564
|
-
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir:
|
|
10763
|
+
let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir3, retryEAGAIN, fsync, contentMode, mode } = opts || {};
|
|
10565
10764
|
fd = fd || dest;
|
|
10566
10765
|
this._len = 0;
|
|
10567
10766
|
this.fd = -1;
|
|
@@ -10586,7 +10785,7 @@ var require_sonic_boom = __commonJS({
|
|
|
10586
10785
|
this.append = append || false;
|
|
10587
10786
|
this.mode = mode;
|
|
10588
10787
|
this.retryEAGAIN = retryEAGAIN || (() => true);
|
|
10589
|
-
this.mkdir =
|
|
10788
|
+
this.mkdir = mkdir3 || false;
|
|
10590
10789
|
let fsWriteSync;
|
|
10591
10790
|
let fsWrite;
|
|
10592
10791
|
if (contentMode === kContentModeBuffer) {
|
|
@@ -11297,8 +11496,8 @@ var require_thread_stream = __commonJS({
|
|
|
11297
11496
|
"use strict";
|
|
11298
11497
|
var { version } = require_package();
|
|
11299
11498
|
var { EventEmitter: EventEmitter2 } = __require("events");
|
|
11300
|
-
var { Worker } = __require("worker_threads");
|
|
11301
|
-
var { join:
|
|
11499
|
+
var { Worker: Worker2 } = __require("worker_threads");
|
|
11500
|
+
var { join: join8 } = __require("path");
|
|
11302
11501
|
var { pathToFileURL } = __require("url");
|
|
11303
11502
|
var { wait } = require_wait();
|
|
11304
11503
|
var {
|
|
@@ -11349,8 +11548,8 @@ var require_thread_stream = __commonJS({
|
|
|
11349
11548
|
function createWorker(stream, opts) {
|
|
11350
11549
|
const { filename, workerData } = opts;
|
|
11351
11550
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
11352
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
11353
|
-
const worker = new
|
|
11551
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11552
|
+
const worker = new Worker2(toExecute, {
|
|
11354
11553
|
...opts.workerOpts,
|
|
11355
11554
|
name: opts.workerOpts?.name || "thread-stream",
|
|
11356
11555
|
trackUnmanagedFds: false,
|
|
@@ -11817,7 +12016,7 @@ var require_transport = __commonJS({
|
|
|
11817
12016
|
var { createRequire } = __require("module");
|
|
11818
12017
|
var { existsSync: existsSync3 } = __require("fs");
|
|
11819
12018
|
var getCallers = require_caller();
|
|
11820
|
-
var { join:
|
|
12019
|
+
var { join: join8, isAbsolute: isAbsolute2, sep } = __require("path");
|
|
11821
12020
|
var { fileURLToPath } = __require("url");
|
|
11822
12021
|
var sleep = require_atomic_sleep();
|
|
11823
12022
|
var onExit = require_on_exit_leak_free();
|
|
@@ -11889,7 +12088,7 @@ var require_transport = __commonJS({
|
|
|
11889
12088
|
return false;
|
|
11890
12089
|
}
|
|
11891
12090
|
}
|
|
11892
|
-
return
|
|
12091
|
+
return isAbsolute2(path) && !existsSync3(path);
|
|
11893
12092
|
}
|
|
11894
12093
|
function stripQuotes(value) {
|
|
11895
12094
|
const first = value[0];
|
|
@@ -11970,7 +12169,7 @@ var require_transport = __commonJS({
|
|
|
11970
12169
|
throw new Error("only one of target or targets can be specified");
|
|
11971
12170
|
}
|
|
11972
12171
|
if (targets) {
|
|
11973
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
12172
|
+
target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
|
|
11974
12173
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
11975
12174
|
return {
|
|
11976
12175
|
...dest,
|
|
@@ -11988,7 +12187,7 @@ var require_transport = __commonJS({
|
|
|
11988
12187
|
});
|
|
11989
12188
|
});
|
|
11990
12189
|
} else if (pipeline) {
|
|
11991
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
12190
|
+
target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
|
|
11992
12191
|
options.pipelines = [pipeline.map((dest) => {
|
|
11993
12192
|
return {
|
|
11994
12193
|
...dest,
|
|
@@ -12007,11 +12206,11 @@ var require_transport = __commonJS({
|
|
|
12007
12206
|
return buildStream(fixTarget(target), options, worker, sync, name);
|
|
12008
12207
|
function fixTarget(origin) {
|
|
12009
12208
|
origin = bundlerOverrides[origin] || origin;
|
|
12010
|
-
if (
|
|
12209
|
+
if (isAbsolute2(origin) || origin.indexOf("file://") === 0) {
|
|
12011
12210
|
return origin;
|
|
12012
12211
|
}
|
|
12013
12212
|
if (origin === "pino/file") {
|
|
12014
|
-
return
|
|
12213
|
+
return join8(__dirname, "..", "file.js");
|
|
12015
12214
|
}
|
|
12016
12215
|
let fixTarget2;
|
|
12017
12216
|
for (const filePath of callers) {
|
|
@@ -12992,7 +13191,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
12992
13191
|
return circularValue;
|
|
12993
13192
|
}
|
|
12994
13193
|
let res = "";
|
|
12995
|
-
let
|
|
13194
|
+
let join8 = ",";
|
|
12996
13195
|
const originalIndentation = indentation;
|
|
12997
13196
|
if (Array.isArray(value)) {
|
|
12998
13197
|
if (value.length === 0) {
|
|
@@ -13006,7 +13205,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
13006
13205
|
indentation += spacer;
|
|
13007
13206
|
res += `
|
|
13008
13207
|
${indentation}`;
|
|
13009
|
-
|
|
13208
|
+
join8 = `,
|
|
13010
13209
|
${indentation}`;
|
|
13011
13210
|
}
|
|
13012
13211
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -13014,13 +13213,13 @@ ${indentation}`;
|
|
|
13014
13213
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
13015
13214
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
13016
13215
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
13017
|
-
res +=
|
|
13216
|
+
res += join8;
|
|
13018
13217
|
}
|
|
13019
13218
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
13020
13219
|
res += tmp !== void 0 ? tmp : "null";
|
|
13021
13220
|
if (value.length - 1 > maximumBreadth) {
|
|
13022
13221
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
13023
|
-
res += `${
|
|
13222
|
+
res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
13024
13223
|
}
|
|
13025
13224
|
if (spacer !== "") {
|
|
13026
13225
|
res += `
|
|
@@ -13041,7 +13240,7 @@ ${originalIndentation}`;
|
|
|
13041
13240
|
let separator = "";
|
|
13042
13241
|
if (spacer !== "") {
|
|
13043
13242
|
indentation += spacer;
|
|
13044
|
-
|
|
13243
|
+
join8 = `,
|
|
13045
13244
|
${indentation}`;
|
|
13046
13245
|
whitespace = " ";
|
|
13047
13246
|
}
|
|
@@ -13055,13 +13254,13 @@ ${indentation}`;
|
|
|
13055
13254
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
13056
13255
|
if (tmp !== void 0) {
|
|
13057
13256
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
13058
|
-
separator =
|
|
13257
|
+
separator = join8;
|
|
13059
13258
|
}
|
|
13060
13259
|
}
|
|
13061
13260
|
if (keyLength > maximumBreadth) {
|
|
13062
13261
|
const removedKeys = keyLength - maximumBreadth;
|
|
13063
13262
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
13064
|
-
separator =
|
|
13263
|
+
separator = join8;
|
|
13065
13264
|
}
|
|
13066
13265
|
if (spacer !== "" && separator.length > 1) {
|
|
13067
13266
|
res = `
|
|
@@ -13102,7 +13301,7 @@ ${originalIndentation}`;
|
|
|
13102
13301
|
}
|
|
13103
13302
|
const originalIndentation = indentation;
|
|
13104
13303
|
let res = "";
|
|
13105
|
-
let
|
|
13304
|
+
let join8 = ",";
|
|
13106
13305
|
if (Array.isArray(value)) {
|
|
13107
13306
|
if (value.length === 0) {
|
|
13108
13307
|
return "[]";
|
|
@@ -13115,7 +13314,7 @@ ${originalIndentation}`;
|
|
|
13115
13314
|
indentation += spacer;
|
|
13116
13315
|
res += `
|
|
13117
13316
|
${indentation}`;
|
|
13118
|
-
|
|
13317
|
+
join8 = `,
|
|
13119
13318
|
${indentation}`;
|
|
13120
13319
|
}
|
|
13121
13320
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -13123,13 +13322,13 @@ ${indentation}`;
|
|
|
13123
13322
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
13124
13323
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
13125
13324
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
13126
|
-
res +=
|
|
13325
|
+
res += join8;
|
|
13127
13326
|
}
|
|
13128
13327
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
13129
13328
|
res += tmp !== void 0 ? tmp : "null";
|
|
13130
13329
|
if (value.length - 1 > maximumBreadth) {
|
|
13131
13330
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
13132
|
-
res += `${
|
|
13331
|
+
res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
13133
13332
|
}
|
|
13134
13333
|
if (spacer !== "") {
|
|
13135
13334
|
res += `
|
|
@@ -13142,7 +13341,7 @@ ${originalIndentation}`;
|
|
|
13142
13341
|
let whitespace = "";
|
|
13143
13342
|
if (spacer !== "") {
|
|
13144
13343
|
indentation += spacer;
|
|
13145
|
-
|
|
13344
|
+
join8 = `,
|
|
13146
13345
|
${indentation}`;
|
|
13147
13346
|
whitespace = " ";
|
|
13148
13347
|
}
|
|
@@ -13151,7 +13350,7 @@ ${indentation}`;
|
|
|
13151
13350
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
13152
13351
|
if (tmp !== void 0) {
|
|
13153
13352
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
13154
|
-
separator =
|
|
13353
|
+
separator = join8;
|
|
13155
13354
|
}
|
|
13156
13355
|
}
|
|
13157
13356
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -13209,20 +13408,20 @@ ${originalIndentation}`;
|
|
|
13209
13408
|
indentation += spacer;
|
|
13210
13409
|
let res2 = `
|
|
13211
13410
|
${indentation}`;
|
|
13212
|
-
const
|
|
13411
|
+
const join9 = `,
|
|
13213
13412
|
${indentation}`;
|
|
13214
13413
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
13215
13414
|
let i = 0;
|
|
13216
13415
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
13217
13416
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
13218
13417
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
13219
|
-
res2 +=
|
|
13418
|
+
res2 += join9;
|
|
13220
13419
|
}
|
|
13221
13420
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
13222
13421
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
13223
13422
|
if (value.length - 1 > maximumBreadth) {
|
|
13224
13423
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
13225
|
-
res2 += `${
|
|
13424
|
+
res2 += `${join9}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
13226
13425
|
}
|
|
13227
13426
|
res2 += `
|
|
13228
13427
|
${originalIndentation}`;
|
|
@@ -13238,16 +13437,16 @@ ${originalIndentation}`;
|
|
|
13238
13437
|
return '"[Object]"';
|
|
13239
13438
|
}
|
|
13240
13439
|
indentation += spacer;
|
|
13241
|
-
const
|
|
13440
|
+
const join8 = `,
|
|
13242
13441
|
${indentation}`;
|
|
13243
13442
|
let res = "";
|
|
13244
13443
|
let separator = "";
|
|
13245
13444
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
13246
13445
|
if (isTypedArrayWithEntries(value)) {
|
|
13247
|
-
res += stringifyTypedArray(value,
|
|
13446
|
+
res += stringifyTypedArray(value, join8, maximumBreadth);
|
|
13248
13447
|
keys = keys.slice(value.length);
|
|
13249
13448
|
maximumPropertiesToStringify -= value.length;
|
|
13250
|
-
separator =
|
|
13449
|
+
separator = join8;
|
|
13251
13450
|
}
|
|
13252
13451
|
if (deterministic) {
|
|
13253
13452
|
keys = sort(keys, comparator);
|
|
@@ -13258,13 +13457,13 @@ ${indentation}`;
|
|
|
13258
13457
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
13259
13458
|
if (tmp !== void 0) {
|
|
13260
13459
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
13261
|
-
separator =
|
|
13460
|
+
separator = join8;
|
|
13262
13461
|
}
|
|
13263
13462
|
}
|
|
13264
13463
|
if (keyLength > maximumBreadth) {
|
|
13265
13464
|
const removedKeys = keyLength - maximumBreadth;
|
|
13266
13465
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
13267
|
-
separator =
|
|
13466
|
+
separator = join8;
|
|
13268
13467
|
}
|
|
13269
13468
|
if (separator !== "") {
|
|
13270
13469
|
res = `
|
|
@@ -61940,11 +62139,11 @@ var require_core = __commonJS({
|
|
|
61940
62139
|
function mapOuter(r, f) {
|
|
61941
62140
|
return r.matched ? f(r) : r;
|
|
61942
62141
|
}
|
|
61943
|
-
function ab(pa, pb,
|
|
61944
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) =>
|
|
62142
|
+
function ab(pa, pb, join8) {
|
|
62143
|
+
return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join8(ma.value, vb, data, i, j)));
|
|
61945
62144
|
}
|
|
61946
|
-
function abc(pa, pb, pc,
|
|
61947
|
-
return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) =>
|
|
62145
|
+
function abc(pa, pb, pc, join8) {
|
|
62146
|
+
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))));
|
|
61948
62147
|
}
|
|
61949
62148
|
function action(f) {
|
|
61950
62149
|
return (data, i) => {
|
|
@@ -74450,6 +74649,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74450
74649
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74451
74650
|
exports.computeMobileSurveyBreadthContinuation = computeMobileSurveyBreadthContinuation;
|
|
74452
74651
|
exports.computeMobileBreadthFinishRedirectDecision = computeMobileBreadthFinishRedirectDecision;
|
|
74652
|
+
exports.hybridScreenFingerprint = hybridScreenFingerprint;
|
|
74453
74653
|
exports.runMobileExplorePhase = runMobileExplorePhase;
|
|
74454
74654
|
var crypto_1 = __require("crypto");
|
|
74455
74655
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
@@ -74903,11 +75103,12 @@ var require_mobile_explorer = __commonJS({
|
|
|
74903
75103
|
const validSurfaceDigest = /^[0-9a-f]{16,64}$/.test(normalizedSurfaceDigest) ? normalizedSurfaceDigest : void 0;
|
|
74904
75104
|
const legacyElementDigests = [...new Set(elements.map((element) => element.pageIdentityDigest?.trim().toLowerCase() ?? "").filter((digest) => /^[0-9a-f]{16,64}$/.test(digest)))];
|
|
74905
75105
|
const pageIdentityDigest = validSurfaceDigest ?? (legacyElementDigests.length === 1 ? legacyElementDigests[0] : void 0);
|
|
75106
|
+
const hasStableControlIdentity = elements.some((element) => Boolean(safeHybridIdentity(element.id, credentials) || safeHybridIdentity(element.name, credentials)));
|
|
74906
75107
|
const descriptors = elements.map((element) => {
|
|
74907
75108
|
const editable = element.editable === true || /^(?:input|textarea|select)$/i.test(element.tag) || /(?:textbox|searchbox|combobox)/i.test(element.role);
|
|
74908
75109
|
const id = safeHybridIdentity(element.id, credentials)?.toLowerCase() ?? "";
|
|
74909
75110
|
const name = safeHybridIdentity(element.name, credentials)?.toLowerCase() ?? "";
|
|
74910
|
-
const semanticText = editable || id || name ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75111
|
+
const semanticText = editable || id || name || hasStableControlIdentity ? "" : stableHybridSemanticText(element.text, credentials);
|
|
74911
75112
|
return [
|
|
74912
75113
|
element.tag.trim().toLowerCase(),
|
|
74913
75114
|
element.role.trim().toLowerCase(),
|
|
@@ -81172,10 +81373,14 @@ var require_mobile_generator = __commonJS({
|
|
|
81172
81373
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
81173
81374
|
"use strict";
|
|
81174
81375
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81376
|
+
exports.structuralScreenKey = structuralScreenKey;
|
|
81175
81377
|
exports.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
81176
81378
|
exports.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
81177
81379
|
exports.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
81178
81380
|
exports.collapseRetryCycles = collapseRetryCycles;
|
|
81381
|
+
exports.coalesceSameFieldTypes = coalesceSameFieldTypes;
|
|
81382
|
+
exports.coalesceRepeatedTaps = coalesceRepeatedTaps;
|
|
81383
|
+
exports.elideNoOpDetours = elideNoOpDetours;
|
|
81179
81384
|
exports.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
|
|
81180
81385
|
exports.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
|
|
81181
81386
|
exports.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
|
|
@@ -81384,6 +81589,22 @@ var require_mobile_generator = __commonJS({
|
|
|
81384
81589
|
MOBILE_ASSERT_TEXT_TOOL,
|
|
81385
81590
|
FINISH_GENERATE_TOOL
|
|
81386
81591
|
];
|
|
81592
|
+
var STRUCTURAL_KEY_MAX_ELEMENTS = 16;
|
|
81593
|
+
function structuralScreenKey(resolved) {
|
|
81594
|
+
if (!resolved)
|
|
81595
|
+
return null;
|
|
81596
|
+
const nativeElements = resolved.screen.elements;
|
|
81597
|
+
const hybridElements = resolved.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81598
|
+
if (nativeElements.length + hybridElements.length < 2)
|
|
81599
|
+
return null;
|
|
81600
|
+
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 ?? ""}`);
|
|
81601
|
+
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 ?? ""}`);
|
|
81602
|
+
const activityOrBundle = resolved.signal.activity ?? resolved.signal.bundleId ?? "";
|
|
81603
|
+
const payload = `${activityOrBundle}
|
|
81604
|
+
${nativeSkeleton.join("\n")}
|
|
81605
|
+
${hybridSkeleton.join("\n")}`;
|
|
81606
|
+
return (0, crypto_1.createHash)("sha1").update(payload).digest("hex");
|
|
81607
|
+
}
|
|
81387
81608
|
function platformForMedium(medium) {
|
|
81388
81609
|
if (medium === "IOS_NATIVE")
|
|
81389
81610
|
return "IOS";
|
|
@@ -81545,18 +81766,39 @@ var require_mobile_generator = __commonJS({
|
|
|
81545
81766
|
}
|
|
81546
81767
|
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.`;
|
|
81547
81768
|
}
|
|
81548
|
-
function
|
|
81769
|
+
function alreadyFocusedWithEqualValue(element, value) {
|
|
81770
|
+
return element.focused === true && typeof element.text === "string" && value.length > 0 && element.text === value;
|
|
81771
|
+
}
|
|
81772
|
+
function nativeElementsOf(resolved) {
|
|
81773
|
+
return resolved?.screen.elements ?? [];
|
|
81774
|
+
}
|
|
81775
|
+
function hybridElementsOf(resolved) {
|
|
81776
|
+
return resolved?.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81777
|
+
}
|
|
81778
|
+
function metadataForElement(element, screenName, platform3, elements = []) {
|
|
81779
|
+
const match = (0, mobile_source_parser_js_2.identityMatchInfo)(element, elements);
|
|
81549
81780
|
return {
|
|
81550
81781
|
screenName,
|
|
81551
81782
|
targetLabel: labelForElement(element),
|
|
81552
81783
|
locatorBundle: (0, mobile_source_parser_js_2.buildLocatorBundle)(element, platform3),
|
|
81553
|
-
bounds: element.bounds
|
|
81784
|
+
bounds: element.bounds,
|
|
81785
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81554
81786
|
};
|
|
81555
81787
|
}
|
|
81556
|
-
function
|
|
81788
|
+
function hybridIdentityMatchInfo(element, elements) {
|
|
81789
|
+
const hybridIdentityValue = (candidate) => nonEmpty3(candidate.id) ?? (candidate.editable === true ? void 0 : nonEmpty3(candidate.text));
|
|
81790
|
+
const chosenValue = hybridIdentityValue(element);
|
|
81791
|
+
if (!chosenValue)
|
|
81792
|
+
return void 0;
|
|
81793
|
+
const sameValue = elements.filter((candidate) => hybridIdentityValue(candidate) === chosenValue);
|
|
81794
|
+
const matchIndex = sameValue.indexOf(element);
|
|
81795
|
+
return matchIndex >= 0 && sameValue.length > 1 ? { matchIndex, expectedMatchCount: sameValue.length } : void 0;
|
|
81796
|
+
}
|
|
81797
|
+
function metadataForHybridElement(element, screenName, elements = []) {
|
|
81557
81798
|
const editable = isEditableHybridElement(element);
|
|
81558
81799
|
const label = labelForHybridElement(element);
|
|
81559
81800
|
const safeText = editable ? void 0 : nonEmpty3(element.text);
|
|
81801
|
+
const match = hybridIdentityMatchInfo(element, elements);
|
|
81560
81802
|
return {
|
|
81561
81803
|
screenName,
|
|
81562
81804
|
targetLabel: label,
|
|
@@ -81565,11 +81807,16 @@ var require_mobile_generator = __commonJS({
|
|
|
81565
81807
|
webviewContext: element.webviewContext,
|
|
81566
81808
|
bounds: element.bounds,
|
|
81567
81809
|
locatorBundle: {
|
|
81568
|
-
|
|
81810
|
+
// WebView controls resolve via `webCss` (above) in the WEBVIEW context, not
|
|
81811
|
+
// by xpath. `LocatorBundle.xpath` is contractually an XPath, so never store a
|
|
81812
|
+
// `css=` selector here — an empty xpath keeps the fallback chain honest and
|
|
81813
|
+
// prevents the css selector being misread as an xpath at replay.
|
|
81814
|
+
xpath: "",
|
|
81569
81815
|
className: element.tag || "webview",
|
|
81570
81816
|
...element.id ? { accessibilityId: element.id } : {},
|
|
81571
81817
|
...safeText ? { text: safeText } : {}
|
|
81572
|
-
}
|
|
81818
|
+
},
|
|
81819
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81573
81820
|
};
|
|
81574
81821
|
}
|
|
81575
81822
|
function isEditableHybridElement(element) {
|
|
@@ -82287,7 +82534,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82287
82534
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
82288
82535
|
recordAction: (action) => {
|
|
82289
82536
|
const screenHash = getLatest()?.signal.screenHash;
|
|
82290
|
-
const
|
|
82537
|
+
const structuralKey = structuralScreenKey(getLatest());
|
|
82538
|
+
const stamped = screenHash && !action.metadata.screenHash || structuralKey && !action.metadata.structuralKey ? {
|
|
82539
|
+
...action,
|
|
82540
|
+
metadata: {
|
|
82541
|
+
...action.metadata,
|
|
82542
|
+
...screenHash && !action.metadata.screenHash ? { screenHash } : {},
|
|
82543
|
+
...structuralKey && !action.metadata.structuralKey ? { structuralKey } : {}
|
|
82544
|
+
}
|
|
82545
|
+
} : action;
|
|
82291
82546
|
actions.push((0, shared_1.placeholderizeRecordedMobileAction)(finalizeAction(stamped), credentials));
|
|
82292
82547
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
82293
82548
|
assertCount++;
|
|
@@ -82393,7 +82648,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82393
82648
|
const result2 = await driver.webTap(hybridTap.webviewContext, hybridTap.css);
|
|
82394
82649
|
const absorbed2 = await absorbActionResult(result2, "mobile_tap(webview)", opensAuthenticationSurface(hybridTap));
|
|
82395
82650
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82396
|
-
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2);
|
|
82651
|
+
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2, hybridElementsOf(before2));
|
|
82397
82652
|
const label = metadata.targetLabel ?? hybridTap.css;
|
|
82398
82653
|
const tapAction = {
|
|
82399
82654
|
type: "TAP",
|
|
@@ -82445,7 +82700,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82445
82700
|
const tapAction = {
|
|
82446
82701
|
type: "TAP",
|
|
82447
82702
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82448
|
-
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
82703
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before)),
|
|
82449
82704
|
innerText: null,
|
|
82450
82705
|
url: "",
|
|
82451
82706
|
value: null
|
|
@@ -82485,10 +82740,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82485
82740
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82486
82741
|
}
|
|
82487
82742
|
const beforeScreenName2 = screenName();
|
|
82743
|
+
const before2 = latest();
|
|
82488
82744
|
const result2 = await driver.webType(hybridType.webviewContext, hybridType.css, value);
|
|
82489
82745
|
const absorbed2 = await absorbActionResult(result2, "mobile_type(webview)");
|
|
82490
82746
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82491
|
-
const metadata = metadataForHybridElement(hybridType, beforeScreenName2);
|
|
82747
|
+
const metadata = metadataForHybridElement(hybridType, beforeScreenName2, hybridElementsOf(before2));
|
|
82492
82748
|
const label = metadata.targetLabel ?? hybridType.css;
|
|
82493
82749
|
recordAction({
|
|
82494
82750
|
type: "TYPE",
|
|
@@ -82503,7 +82759,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82503
82759
|
const element = resolveRef(toolArgs.ref);
|
|
82504
82760
|
if (!element)
|
|
82505
82761
|
return unknownRef(toolArgs.ref);
|
|
82762
|
+
if (alreadyFocusedWithEqualValue(element, value)) {
|
|
82763
|
+
return {
|
|
82764
|
+
ok: false,
|
|
82765
|
+
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.`
|
|
82766
|
+
};
|
|
82767
|
+
}
|
|
82506
82768
|
const beforeScreenName = screenName();
|
|
82769
|
+
const before = latest();
|
|
82507
82770
|
const result = await driver.type(value, element.bounds);
|
|
82508
82771
|
const absorbed = await absorbActionResult(result, "mobile_type");
|
|
82509
82772
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82511,7 +82774,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82511
82774
|
type: "TYPE",
|
|
82512
82775
|
value,
|
|
82513
82776
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82514
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82777
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82515
82778
|
});
|
|
82516
82779
|
}
|
|
82517
82780
|
const payload = actionPayload(result, absorbed);
|
|
@@ -82529,6 +82792,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82529
82792
|
return { ok: false, error: "This driver does not support non-clearing WEBVIEW keyboard submit." };
|
|
82530
82793
|
}
|
|
82531
82794
|
const beforeScreenName2 = screenName();
|
|
82795
|
+
const before2 = latest();
|
|
82532
82796
|
const result2 = await driver.webSubmitKeyboard(hybridField.webviewContext, hybridField.css);
|
|
82533
82797
|
const absorbed2 = await absorbActionResult(result2, "mobile_submit_keyboard(webview)");
|
|
82534
82798
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
@@ -82536,7 +82800,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82536
82800
|
type: "TYPE",
|
|
82537
82801
|
value: "\n",
|
|
82538
82802
|
selector: `css=${hybridField.css}`,
|
|
82539
|
-
metadata: metadataForHybridElement(hybridField, beforeScreenName2)
|
|
82803
|
+
metadata: metadataForHybridElement(hybridField, beforeScreenName2, hybridElementsOf(before2))
|
|
82540
82804
|
});
|
|
82541
82805
|
}
|
|
82542
82806
|
return actionPayload(result2, absorbed2);
|
|
@@ -82548,6 +82812,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82548
82812
|
return { ok: false, error: "This driver does not support non-clearing keyboard submit." };
|
|
82549
82813
|
}
|
|
82550
82814
|
const beforeScreenName = screenName();
|
|
82815
|
+
const before = latest();
|
|
82551
82816
|
const result = await driver.submitKeyboard(element.bounds);
|
|
82552
82817
|
const absorbed = await absorbActionResult(result, "mobile_submit_keyboard");
|
|
82553
82818
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82555,7 +82820,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82555
82820
|
type: "TYPE",
|
|
82556
82821
|
value: "\n",
|
|
82557
82822
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82558
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82823
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82559
82824
|
});
|
|
82560
82825
|
}
|
|
82561
82826
|
return actionPayload(result, absorbed);
|
|
@@ -82580,10 +82845,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82580
82845
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82581
82846
|
}
|
|
82582
82847
|
const beforeScreenName2 = screenName();
|
|
82848
|
+
const before2 = latest();
|
|
82583
82849
|
const result2 = await driver.webType(hybridFill.webviewContext, hybridFill.css, secret);
|
|
82584
82850
|
const absorbed2 = await absorbActionResult(result2, "mobile_fill_credential(webview)");
|
|
82585
82851
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82586
|
-
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2);
|
|
82852
|
+
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2, hybridElementsOf(before2));
|
|
82587
82853
|
const label = metadata.targetLabel ?? hybridFill.css;
|
|
82588
82854
|
recordAction({
|
|
82589
82855
|
type: "TYPE",
|
|
@@ -82598,7 +82864,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82598
82864
|
const element = resolveRef(toolArgs.ref);
|
|
82599
82865
|
if (!element)
|
|
82600
82866
|
return unknownRef(toolArgs.ref);
|
|
82867
|
+
if (alreadyFocusedWithEqualValue(element, secret)) {
|
|
82868
|
+
return {
|
|
82869
|
+
ok: false,
|
|
82870
|
+
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.`
|
|
82871
|
+
};
|
|
82872
|
+
}
|
|
82601
82873
|
const beforeScreenName = screenName();
|
|
82874
|
+
const before = latest();
|
|
82602
82875
|
const result = await driver.type(secret, element.bounds);
|
|
82603
82876
|
const absorbed = await absorbActionResult(result, "mobile_fill_credential");
|
|
82604
82877
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82606,7 +82879,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82606
82879
|
type: "TYPE",
|
|
82607
82880
|
value: (0, shared_1.mobileCredentialPlaceholder)(key),
|
|
82608
82881
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82609
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82882
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82610
82883
|
});
|
|
82611
82884
|
log2(` fill_credential: recorded {{${key}}} into ${labelForElement(element)}`);
|
|
82612
82885
|
}
|
|
@@ -82634,13 +82907,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82634
82907
|
if (!element)
|
|
82635
82908
|
return unknownRef(toolArgs.ref);
|
|
82636
82909
|
const beforeScreenName = screenName();
|
|
82910
|
+
const before = latest();
|
|
82637
82911
|
const result = await driver.clear(element.bounds);
|
|
82638
82912
|
const absorbed = await absorbActionResult(result, "mobile_clear");
|
|
82639
82913
|
if (shouldRecordAction(result, absorbed)) {
|
|
82640
82914
|
recordAction({
|
|
82641
82915
|
type: "CLEAR",
|
|
82642
82916
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82643
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82917
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82644
82918
|
});
|
|
82645
82919
|
}
|
|
82646
82920
|
return actionPayload(result, absorbed);
|
|
@@ -82703,7 +82977,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82703
82977
|
case "mobile_assert_visible": {
|
|
82704
82978
|
const hybrid = resolveHybridRef(toolArgs.ref);
|
|
82705
82979
|
if (hybrid) {
|
|
82706
|
-
const metadata = metadataForHybridElement(hybrid, screenName());
|
|
82980
|
+
const metadata = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82707
82981
|
recordAction({
|
|
82708
82982
|
type: "ASSERT_VISIBLE",
|
|
82709
82983
|
selector: `css=${hybrid.css}`,
|
|
@@ -82743,7 +83017,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82743
83017
|
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.`
|
|
82744
83018
|
};
|
|
82745
83019
|
}
|
|
82746
|
-
const metadata2 = metadataForHybridElement(hybrid, screenName());
|
|
83020
|
+
const metadata2 = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82747
83021
|
const stableWebIdentity = Boolean(nonEmpty3(hybrid.id) || nonEmpty3(hybrid.name));
|
|
82748
83022
|
if (stableWebIdentity) {
|
|
82749
83023
|
recordAction({
|
|
@@ -82796,7 +83070,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82796
83070
|
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.`
|
|
82797
83071
|
};
|
|
82798
83072
|
}
|
|
82799
|
-
const metadata = metadataForElement(element, screenName(), platform3);
|
|
83073
|
+
const metadata = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82800
83074
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82801
83075
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
82802
83076
|
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
@@ -82842,7 +83116,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82842
83116
|
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.`
|
|
82843
83117
|
};
|
|
82844
83118
|
}
|
|
82845
|
-
const metadata2 = metadataForElement(element, screenName(), platform3);
|
|
83119
|
+
const metadata2 = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82846
83120
|
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata: metadata2 });
|
|
82847
83121
|
return { ok: true, label: metadata2.targetLabel ?? "element" };
|
|
82848
83122
|
}
|
|
@@ -82892,7 +83166,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82892
83166
|
error: `The live element matching ${JSON.stringify(accessibilityId ?? text)} is not a durable assertion target (${weakReason}).`
|
|
82893
83167
|
};
|
|
82894
83168
|
}
|
|
82895
|
-
const metadata = metadataForElement(liveElement, screenName(), platform3);
|
|
83169
|
+
const metadata = metadataForElement(liveElement, screenName(), platform3, live.screen.elements);
|
|
82896
83170
|
recordAction({
|
|
82897
83171
|
type: "ASSERT_VISIBLE",
|
|
82898
83172
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(liveElement),
|
|
@@ -83020,7 +83294,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83020
83294
|
actions.push(finalizeAction({
|
|
83021
83295
|
type: "ASSERT_VISIBLE",
|
|
83022
83296
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(anchor),
|
|
83023
|
-
metadata: metadataForElement(anchor, screen, platform3)
|
|
83297
|
+
metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
|
|
83024
83298
|
}));
|
|
83025
83299
|
return true;
|
|
83026
83300
|
}
|
|
@@ -83363,6 +83637,168 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83363
83637
|
}
|
|
83364
83638
|
return out;
|
|
83365
83639
|
}
|
|
83640
|
+
function isDurableMutation(action) {
|
|
83641
|
+
if (action.type === "TYPE" || action.type === "CLEAR")
|
|
83642
|
+
return true;
|
|
83643
|
+
if (action.type === "TAP")
|
|
83644
|
+
return (0, shared_1.isStateValueMobileClassName)(action.metadata.locatorBundle?.className);
|
|
83645
|
+
return false;
|
|
83646
|
+
}
|
|
83647
|
+
function mobileWanderTraceEnabled2() {
|
|
83648
|
+
const v = (process.env.VALIDATEQA_MOBILE_WANDER_TRACE || "").trim().toLowerCase();
|
|
83649
|
+
return v === "true" || v === "1";
|
|
83650
|
+
}
|
|
83651
|
+
function wanderTraceLabel(action) {
|
|
83652
|
+
const identity = action.metadata.targetLabel || action.selector || action.metadata.locatorBundle?.accessibilityId || action.metadata.locatorBundle?.resourceId || "(no identity)";
|
|
83653
|
+
return `${action.type} ${identity}`;
|
|
83654
|
+
}
|
|
83655
|
+
function logWanderDiff(pass, before, after, log2) {
|
|
83656
|
+
if (after.length >= before.length)
|
|
83657
|
+
return;
|
|
83658
|
+
log2(` ${pass}: collapsed ${before.length - after.length} step(s) (${before.length} \u2192 ${after.length}).`);
|
|
83659
|
+
if (!mobileWanderTraceEnabled2())
|
|
83660
|
+
return;
|
|
83661
|
+
const survivors = new Set(after);
|
|
83662
|
+
for (const action of before) {
|
|
83663
|
+
if (!survivors.has(action)) {
|
|
83664
|
+
log2(` [wander] ${pass}: dropped ${wanderTraceLabel(action)}`);
|
|
83665
|
+
}
|
|
83666
|
+
}
|
|
83667
|
+
}
|
|
83668
|
+
function coalesceSameFieldTypes(actions) {
|
|
83669
|
+
if (actions.length <= 1)
|
|
83670
|
+
return actions;
|
|
83671
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83672
|
+
const lastTypeIndexBySelector = /* @__PURE__ */ new Map();
|
|
83673
|
+
const refocusTapIndexesBySelector = /* @__PURE__ */ new Map();
|
|
83674
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83675
|
+
const current = actions[i];
|
|
83676
|
+
if (current.type === "ASSERT_VISIBLE" || current.type === "ASSERT_TEXT") {
|
|
83677
|
+
lastTypeIndexBySelector.clear();
|
|
83678
|
+
refocusTapIndexesBySelector.clear();
|
|
83679
|
+
continue;
|
|
83680
|
+
}
|
|
83681
|
+
if (current.type === "CLEAR") {
|
|
83682
|
+
lastTypeIndexBySelector.delete(current.selector);
|
|
83683
|
+
refocusTapIndexesBySelector.delete(current.selector);
|
|
83684
|
+
continue;
|
|
83685
|
+
}
|
|
83686
|
+
if (isFormCommitAction(current)) {
|
|
83687
|
+
lastTypeIndexBySelector.clear();
|
|
83688
|
+
refocusTapIndexesBySelector.clear();
|
|
83689
|
+
continue;
|
|
83690
|
+
}
|
|
83691
|
+
if (current.type === "TAP") {
|
|
83692
|
+
if (current.selector) {
|
|
83693
|
+
const list = refocusTapIndexesBySelector.get(current.selector) ?? [];
|
|
83694
|
+
list.push(i);
|
|
83695
|
+
refocusTapIndexesBySelector.set(current.selector, list);
|
|
83696
|
+
}
|
|
83697
|
+
continue;
|
|
83698
|
+
}
|
|
83699
|
+
if (current.type !== "TYPE" || !current.selector)
|
|
83700
|
+
continue;
|
|
83701
|
+
const priorIndex = lastTypeIndexBySelector.get(current.selector);
|
|
83702
|
+
if (priorIndex !== void 0) {
|
|
83703
|
+
toDrop.add(priorIndex);
|
|
83704
|
+
for (const tapIndex of refocusTapIndexesBySelector.get(current.selector) ?? []) {
|
|
83705
|
+
if (tapIndex > priorIndex && tapIndex < i)
|
|
83706
|
+
toDrop.add(tapIndex);
|
|
83707
|
+
}
|
|
83708
|
+
}
|
|
83709
|
+
lastTypeIndexBySelector.set(current.selector, i);
|
|
83710
|
+
refocusTapIndexesBySelector.set(current.selector, []);
|
|
83711
|
+
}
|
|
83712
|
+
if (toDrop.size === 0)
|
|
83713
|
+
return actions;
|
|
83714
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83715
|
+
}
|
|
83716
|
+
function coalesceRepeatedTaps(actions) {
|
|
83717
|
+
if (actions.length <= 1)
|
|
83718
|
+
return actions;
|
|
83719
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83720
|
+
const lastTapIndexBySelector = /* @__PURE__ */ new Map();
|
|
83721
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83722
|
+
const action = actions[i];
|
|
83723
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT") {
|
|
83724
|
+
lastTapIndexBySelector.clear();
|
|
83725
|
+
continue;
|
|
83726
|
+
}
|
|
83727
|
+
if (isDurableMutation(action)) {
|
|
83728
|
+
lastTapIndexBySelector.clear();
|
|
83729
|
+
continue;
|
|
83730
|
+
}
|
|
83731
|
+
if (action.type !== "TAP" || !action.selector)
|
|
83732
|
+
continue;
|
|
83733
|
+
const key = action.metadata.structuralKey;
|
|
83734
|
+
const priorIndex = lastTapIndexBySelector.get(action.selector);
|
|
83735
|
+
if (priorIndex !== void 0 && key && actions[priorIndex].metadata.structuralKey === key) {
|
|
83736
|
+
const priorHash = actions[priorIndex].metadata.screenHash;
|
|
83737
|
+
const currentHash = action.metadata.screenHash;
|
|
83738
|
+
const screenAdvanced = !!priorHash && !!currentHash && priorHash !== currentHash;
|
|
83739
|
+
if (!screenAdvanced) {
|
|
83740
|
+
toDrop.add(priorIndex);
|
|
83741
|
+
}
|
|
83742
|
+
}
|
|
83743
|
+
lastTapIndexBySelector.set(action.selector, i);
|
|
83744
|
+
}
|
|
83745
|
+
if (toDrop.size === 0)
|
|
83746
|
+
return actions;
|
|
83747
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83748
|
+
}
|
|
83749
|
+
var MAX_DETOUR_LENGTH = 6;
|
|
83750
|
+
function isDetourClosingAction(action) {
|
|
83751
|
+
if (action.type === "BACK" || action.type === "HOME")
|
|
83752
|
+
return true;
|
|
83753
|
+
return isNavigationChromeTap(action);
|
|
83754
|
+
}
|
|
83755
|
+
function isEvidenceAction(action) {
|
|
83756
|
+
return action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "WAIT_FOR_ELEMENT";
|
|
83757
|
+
}
|
|
83758
|
+
function elideNoOpDetours(actions) {
|
|
83759
|
+
if (actions.length < 2)
|
|
83760
|
+
return actions;
|
|
83761
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83762
|
+
for (let j = 0; j < actions.length; j++) {
|
|
83763
|
+
if (toDrop.has(j))
|
|
83764
|
+
continue;
|
|
83765
|
+
if (!isDetourClosingAction(actions[j]))
|
|
83766
|
+
continue;
|
|
83767
|
+
const keyAfter = actions[j].metadata.structuralKey;
|
|
83768
|
+
if (!keyAfter)
|
|
83769
|
+
continue;
|
|
83770
|
+
const next = actions[j + 1];
|
|
83771
|
+
if (next && isEvidenceAction(next))
|
|
83772
|
+
continue;
|
|
83773
|
+
let bestStart = null;
|
|
83774
|
+
let leftScreen = false;
|
|
83775
|
+
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83776
|
+
for (let i = j; i >= earliestStart; i--) {
|
|
83777
|
+
if (i < j) {
|
|
83778
|
+
const inside = actions[i];
|
|
83779
|
+
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83780
|
+
break;
|
|
83781
|
+
}
|
|
83782
|
+
const insideKey = inside.metadata.structuralKey;
|
|
83783
|
+
if (insideKey && insideKey !== keyAfter)
|
|
83784
|
+
leftScreen = true;
|
|
83785
|
+
}
|
|
83786
|
+
if (i === 0)
|
|
83787
|
+
continue;
|
|
83788
|
+
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83789
|
+
if (i < j && leftScreen && keyBefore && keyBefore === keyAfter) {
|
|
83790
|
+
bestStart = i;
|
|
83791
|
+
}
|
|
83792
|
+
}
|
|
83793
|
+
if (bestStart !== null) {
|
|
83794
|
+
for (let k = bestStart; k <= j; k++)
|
|
83795
|
+
toDrop.add(k);
|
|
83796
|
+
}
|
|
83797
|
+
}
|
|
83798
|
+
if (toDrop.size === 0)
|
|
83799
|
+
return actions;
|
|
83800
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83801
|
+
}
|
|
83366
83802
|
function mobileActionsToPseudoPlaywright(actions) {
|
|
83367
83803
|
const escape = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
83368
83804
|
const lines = ["test('mobile', async ({ page }) => {"];
|
|
@@ -83450,6 +83886,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83450
83886
|
if (actions.length < beforeCycleCollapse) {
|
|
83451
83887
|
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
83452
83888
|
}
|
|
83889
|
+
const beforeTypeCoalesce = actions;
|
|
83890
|
+
actions = coalesceSameFieldTypes(actions);
|
|
83891
|
+
logWanderDiff("coalesceSameFieldTypes", beforeTypeCoalesce, actions, log2);
|
|
83892
|
+
const beforeTapCoalesce = actions;
|
|
83893
|
+
actions = coalesceRepeatedTaps(actions);
|
|
83894
|
+
logWanderDiff("coalesceRepeatedTaps", beforeTapCoalesce, actions, log2);
|
|
83895
|
+
const beforeDetourElision = actions;
|
|
83896
|
+
actions = elideNoOpDetours(actions);
|
|
83897
|
+
logWanderDiff("elideNoOpDetours", beforeDetourElision, actions, log2);
|
|
83453
83898
|
const beforeContainerDrop = actions.length;
|
|
83454
83899
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
83455
83900
|
if (actions.length < beforeContainerDrop) {
|
|
@@ -83634,48 +84079,29 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83634
84079
|
log2(" app data reset skipped by policy \u2014 scenario records after relaunch with preserved data.");
|
|
83635
84080
|
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: false, coldResetUnavailable: false, refuseGenerate: false, reason: "preserve_policy" }, log2);
|
|
83636
84081
|
}
|
|
83637
|
-
|
|
83638
|
-
|
|
83639
|
-
|
|
83640
|
-
|
|
83641
|
-
|
|
83642
|
-
|
|
83643
|
-
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
|
|
83644
|
-
}
|
|
83645
|
-
await driver.relaunch();
|
|
83646
|
-
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
83647
|
-
log2(" CLEAR_APP_DATA required but app-data reset was unavailable \u2014 refusing to generate from warm state.");
|
|
83648
|
-
return {
|
|
83649
|
-
appDataCleared: false,
|
|
83650
|
-
coldResetUnavailable: true,
|
|
83651
|
-
refuseGenerate: true,
|
|
83652
|
-
reason: "clear_app_data_unavailable"
|
|
83653
|
-
};
|
|
83654
|
-
}
|
|
83655
|
-
log2(" app-data reset unavailable \u2014 generating with @warm-state (relaunch only; may not match cold-boot replay).");
|
|
83656
|
-
return applyScenarioEntryDeepLink(driver, target, {
|
|
83657
|
-
appDataCleared: false,
|
|
83658
|
-
coldResetUnavailable: true,
|
|
83659
|
-
refuseGenerate: false,
|
|
83660
|
-
reason: platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
|
|
83661
|
-
}, log2);
|
|
84082
|
+
const appDataCleared = await driver.resetAppState?.().catch((err) => {
|
|
84083
|
+
log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
84084
|
+
return false;
|
|
84085
|
+
}) ?? false;
|
|
84086
|
+
if (appDataCleared) {
|
|
84087
|
+
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
|
|
83662
84088
|
}
|
|
83663
84089
|
await driver.relaunch();
|
|
83664
84090
|
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
83665
|
-
log2(" CLEAR_APP_DATA required but iOS
|
|
84091
|
+
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.");
|
|
83666
84092
|
return {
|
|
83667
84093
|
appDataCleared: false,
|
|
83668
84094
|
coldResetUnavailable: true,
|
|
83669
84095
|
refuseGenerate: true,
|
|
83670
|
-
reason: "ios_clear_app_data_unavailable"
|
|
84096
|
+
reason: platform3 === "IOS" ? "ios_clear_app_data_unavailable" : "clear_app_data_unavailable"
|
|
83671
84097
|
};
|
|
83672
84098
|
}
|
|
83673
|
-
log2(" iOS
|
|
84099
|
+
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).");
|
|
83674
84100
|
return applyScenarioEntryDeepLink(driver, target, {
|
|
83675
84101
|
appDataCleared: false,
|
|
83676
84102
|
coldResetUnavailable: true,
|
|
83677
84103
|
refuseGenerate: false,
|
|
83678
|
-
reason: "ios_no_clear_app"
|
|
84104
|
+
reason: platform3 === "IOS" ? "ios_no_clear_app" : platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
|
|
83679
84105
|
}, log2);
|
|
83680
84106
|
}
|
|
83681
84107
|
async function runMobileGeneratePhase(args) {
|
|
@@ -84278,6 +84704,8 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
84278
84704
|
"use strict";
|
|
84279
84705
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
84280
84706
|
exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = void 0;
|
|
84707
|
+
exports.timed = timed;
|
|
84708
|
+
exports.timedSync = timedSync;
|
|
84281
84709
|
exports.executeMobileScriptDrivenHeal = executeMobileScriptDrivenHeal;
|
|
84282
84710
|
exports.executeMobileTestWithHealing = executeMobileTestWithHealing2;
|
|
84283
84711
|
exports.executeMobileLiveHeal = executeMobileLiveHeal2;
|
|
@@ -84487,6 +84915,32 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84487
84915
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
84488
84916
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
84489
84917
|
}
|
|
84918
|
+
function formatHealTimingLine(category, label, ms, failed, errMessage) {
|
|
84919
|
+
const base2 = `\u{1FA7A}\u23F1 [${category}] ${label} \u2014 ${ms}ms`;
|
|
84920
|
+
return failed ? `${base2} (FAILED: ${errMessage ?? "unknown error"})` : base2;
|
|
84921
|
+
}
|
|
84922
|
+
async function timed(category, label, onProgress, fn) {
|
|
84923
|
+
const start = Date.now();
|
|
84924
|
+
try {
|
|
84925
|
+
const result = await fn();
|
|
84926
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84927
|
+
return result;
|
|
84928
|
+
} catch (err) {
|
|
84929
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84930
|
+
throw err;
|
|
84931
|
+
}
|
|
84932
|
+
}
|
|
84933
|
+
function timedSync(category, label, onProgress, fn) {
|
|
84934
|
+
const start = Date.now();
|
|
84935
|
+
try {
|
|
84936
|
+
const result = fn();
|
|
84937
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84938
|
+
return result;
|
|
84939
|
+
} catch (err) {
|
|
84940
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84941
|
+
throw err;
|
|
84942
|
+
}
|
|
84943
|
+
}
|
|
84490
84944
|
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
84491
84945
|
function isIdentityEqualityXpath(value) {
|
|
84492
84946
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
@@ -84704,21 +85158,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84704
85158
|
return JSON.stringify(base2?.target) !== JSON.stringify(healed.target);
|
|
84705
85159
|
}
|
|
84706
85160
|
function findMissingChangedLocators(baseSteps, healedSteps, screen) {
|
|
84707
|
-
const
|
|
85161
|
+
const results = [];
|
|
84708
85162
|
healedSteps.forEach((healed, index) => {
|
|
84709
85163
|
if (!stepTargetChanged(baseSteps[index], healed))
|
|
84710
85164
|
return;
|
|
84711
85165
|
const candidate = stepToLocatorCandidate(healed);
|
|
84712
85166
|
if (!candidate)
|
|
84713
85167
|
return;
|
|
84714
|
-
|
|
84715
|
-
|
|
84716
|
-
|
|
84717
|
-
|
|
84718
|
-
});
|
|
85168
|
+
const matchCount = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen).length;
|
|
85169
|
+
const label = healed.target?.primary ?? JSON.stringify(candidate);
|
|
85170
|
+
const expressions = stepIdentityExpressions(healed);
|
|
85171
|
+
if (matchCount === 0) {
|
|
85172
|
+
results.push({ label, expressions, kind: "missing" });
|
|
85173
|
+
} else if (matchCount > 1) {
|
|
85174
|
+
results.push({ label, expressions, kind: "ambiguous", matchCount });
|
|
84719
85175
|
}
|
|
84720
85176
|
});
|
|
84721
|
-
return
|
|
85177
|
+
return results;
|
|
84722
85178
|
}
|
|
84723
85179
|
function changedStepExpressions(baseSteps, healedSteps) {
|
|
84724
85180
|
const out = [];
|
|
@@ -84947,10 +85403,10 @@ ${args.directive}`);
|
|
|
84947
85403
|
let content;
|
|
84948
85404
|
let finishReason;
|
|
84949
85405
|
try {
|
|
84950
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
85406
|
+
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)(() => {
|
|
84951
85407
|
arm();
|
|
84952
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
84953
|
-
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
85408
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85409
|
+
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true }));
|
|
84954
85410
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84955
85411
|
finishReason = response?.choices?.[0]?.finish_reason ?? void 0;
|
|
84956
85412
|
} catch {
|
|
@@ -84993,14 +85449,18 @@ ${args.directive}`);
|
|
|
84993
85449
|
continue;
|
|
84994
85450
|
}
|
|
84995
85451
|
if (activeFailingSource && isLocatorNotFound(activeError) && !isDrift(activeError)) {
|
|
84996
|
-
const
|
|
84997
|
-
const missing =
|
|
85452
|
+
const failingSourceSnapshot = activeFailingSource;
|
|
85453
|
+
const missing = timedSync("device-op", `phase1-attempt-${attempt}-static-verify`, options.onProgress, () => {
|
|
85454
|
+
const screen = (0, mobile_source_parser_js_1.parseMobileSource)(failingSourceSnapshot, platform3);
|
|
85455
|
+
return findMissingChangedLocators(baseSteps, healedSteps, screen);
|
|
85456
|
+
});
|
|
84998
85457
|
if (missing.length > 0) {
|
|
84999
85458
|
for (const entry of missing)
|
|
85000
85459
|
pushUnique(triedLocators, entry.expressions);
|
|
85001
85460
|
markRecoverableTurn();
|
|
85002
85461
|
lastVerdict = "fix";
|
|
85003
|
-
|
|
85462
|
+
const first = missing[0];
|
|
85463
|
+
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.`;
|
|
85004
85464
|
continue;
|
|
85005
85465
|
}
|
|
85006
85466
|
}
|
|
@@ -85014,9 +85474,10 @@ ${args.directive}`);
|
|
|
85014
85474
|
pendingDirective = "The proposed fix could not be device-verified here. If it is not clearly correct, revise it.";
|
|
85015
85475
|
continue;
|
|
85016
85476
|
}
|
|
85477
|
+
const runNativeTest = options.runNativeTest;
|
|
85017
85478
|
let verify;
|
|
85018
85479
|
try {
|
|
85019
|
-
verify = await options.runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables });
|
|
85480
|
+
verify = await timed("reexec", `phase1-attempt-${attempt}-device-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables }));
|
|
85020
85481
|
} catch {
|
|
85021
85482
|
return {
|
|
85022
85483
|
passed: false,
|
|
@@ -85123,6 +85584,9 @@ ${args.directive}`);
|
|
|
85123
85584
|
}
|
|
85124
85585
|
}
|
|
85125
85586
|
async function executeMobileTestWithHealing2(steps, target, ctx, options) {
|
|
85587
|
+
return timed("total", "heal-total", options.onProgress, () => executeMobileTestWithHealingImpl(steps, target, ctx, options));
|
|
85588
|
+
}
|
|
85589
|
+
async function executeMobileTestWithHealingImpl(steps, target, ctx, options) {
|
|
85126
85590
|
const invalidAuthSetup = detectInvalidMobileAuthSetup(steps, ctx.error);
|
|
85127
85591
|
if (invalidAuthSetup) {
|
|
85128
85592
|
return { passed: false, verdict: "test_invalid", healReason: invalidAuthSetup };
|
|
@@ -85249,9 +85713,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85249
85713
|
const candidate = partsToCandidate(parts);
|
|
85250
85714
|
if (!hasIdentitySignal(candidate))
|
|
85251
85715
|
return;
|
|
85252
|
-
const
|
|
85253
|
-
|
|
85254
|
-
|
|
85716
|
+
const matches = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen);
|
|
85717
|
+
const target = matches.find((el) => el.bounds.width > 0 && el.bounds.height > 0) ?? matches[0];
|
|
85718
|
+
if (target)
|
|
85719
|
+
await driver.tap(target.bounds);
|
|
85255
85720
|
}
|
|
85256
85721
|
function buildLiveUserContent(args) {
|
|
85257
85722
|
const parts = [];
|
|
@@ -85316,7 +85781,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85316
85781
|
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
85317
85782
|
let snap;
|
|
85318
85783
|
try {
|
|
85319
|
-
snap = await liveDriver.snapshot();
|
|
85784
|
+
snap = await timed("device-op", `phase2-pass-${pass}-snapshot`, options.onProgress, () => liveDriver.snapshot());
|
|
85320
85785
|
} catch {
|
|
85321
85786
|
return {
|
|
85322
85787
|
passed: false,
|
|
@@ -85342,10 +85807,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85342
85807
|
];
|
|
85343
85808
|
let content;
|
|
85344
85809
|
try {
|
|
85345
|
-
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)(() => {
|
|
85810
|
+
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)(() => {
|
|
85346
85811
|
arm();
|
|
85347
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
85348
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
85812
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85813
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true })));
|
|
85349
85814
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
85350
85815
|
} catch {
|
|
85351
85816
|
continue;
|
|
@@ -85377,7 +85842,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85377
85842
|
const healedSteps = parsed.steps;
|
|
85378
85843
|
let verify;
|
|
85379
85844
|
try {
|
|
85380
|
-
verify = await runNativeTest({ steps: healedSteps, target, testVariables });
|
|
85845
|
+
verify = await timed("reexec", `phase2-pass-${pass}-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables }));
|
|
85381
85846
|
} catch {
|
|
85382
85847
|
return {
|
|
85383
85848
|
passed: false,
|
|
@@ -89231,7 +89696,7 @@ var require_kill_tree = __commonJS({
|
|
|
89231
89696
|
var node_child_process_1 = __require("child_process");
|
|
89232
89697
|
var node_util_1 = __require("util");
|
|
89233
89698
|
var node_fs_1 = __require("fs");
|
|
89234
|
-
var
|
|
89699
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
89235
89700
|
var PS_TIMEOUT_MS = 2e3;
|
|
89236
89701
|
var PS_BUFFER = 1024 * 1024;
|
|
89237
89702
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -89285,7 +89750,7 @@ var require_kill_tree = __commonJS({
|
|
|
89285
89750
|
timer.unref?.();
|
|
89286
89751
|
let stdout;
|
|
89287
89752
|
try {
|
|
89288
|
-
const result = await
|
|
89753
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89289
89754
|
maxBuffer: PS_BUFFER,
|
|
89290
89755
|
signal: controller.signal
|
|
89291
89756
|
});
|
|
@@ -89323,7 +89788,7 @@ var require_kill_tree = __commonJS({
|
|
|
89323
89788
|
timer.unref?.();
|
|
89324
89789
|
let stdout;
|
|
89325
89790
|
try {
|
|
89326
|
-
const result = await
|
|
89791
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89327
89792
|
maxBuffer: PS_BUFFER,
|
|
89328
89793
|
signal: controller.signal
|
|
89329
89794
|
});
|
|
@@ -89400,7 +89865,7 @@ var require_kill_tree = __commonJS({
|
|
|
89400
89865
|
timer.unref?.();
|
|
89401
89866
|
let stdout;
|
|
89402
89867
|
try {
|
|
89403
|
-
const result = await
|
|
89868
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid="], {
|
|
89404
89869
|
maxBuffer: PS_BUFFER,
|
|
89405
89870
|
signal: controller.signal
|
|
89406
89871
|
});
|
|
@@ -99279,8 +99744,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99279
99744
|
onRetry: (attempt, reason, delayMs) => logs2.push(` \u26A0 scenario-gen AI call retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
99280
99745
|
});
|
|
99281
99746
|
} catch (aiErr) {
|
|
99282
|
-
const
|
|
99283
|
-
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${
|
|
99747
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
99748
|
+
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg3}`);
|
|
99284
99749
|
throw (0, ai_retry_js_1.markAIProviderError)(aiErr);
|
|
99285
99750
|
}
|
|
99286
99751
|
const iterDuration = Date.now() - iterStart;
|
|
@@ -99323,8 +99788,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99323
99788
|
try {
|
|
99324
99789
|
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
99325
99790
|
} catch (parseErr) {
|
|
99326
|
-
const
|
|
99327
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content:
|
|
99791
|
+
const errMsg3 = `Invalid tool arguments: ${parseErr.message}`;
|
|
99792
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg3 });
|
|
99328
99793
|
continue;
|
|
99329
99794
|
}
|
|
99330
99795
|
if (toolName === "finish_generate") {
|
|
@@ -100582,7 +101047,7 @@ var require_util4 = __commonJS({
|
|
|
100582
101047
|
}
|
|
100583
101048
|
path = url.path;
|
|
100584
101049
|
}
|
|
100585
|
-
var
|
|
101050
|
+
var isAbsolute2 = exports.isAbsolute(path);
|
|
100586
101051
|
var parts = path.split(/\/+/);
|
|
100587
101052
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
100588
101053
|
part = parts[i];
|
|
@@ -100602,7 +101067,7 @@ var require_util4 = __commonJS({
|
|
|
100602
101067
|
}
|
|
100603
101068
|
path = parts.join("/");
|
|
100604
101069
|
if (path === "") {
|
|
100605
|
-
path =
|
|
101070
|
+
path = isAbsolute2 ? "/" : ".";
|
|
100606
101071
|
}
|
|
100607
101072
|
if (url) {
|
|
100608
101073
|
url.path = path;
|
|
@@ -100611,7 +101076,7 @@ var require_util4 = __commonJS({
|
|
|
100611
101076
|
return path;
|
|
100612
101077
|
}
|
|
100613
101078
|
exports.normalize = normalize;
|
|
100614
|
-
function
|
|
101079
|
+
function join8(aRoot, aPath) {
|
|
100615
101080
|
if (aRoot === "") {
|
|
100616
101081
|
aRoot = ".";
|
|
100617
101082
|
}
|
|
@@ -100643,7 +101108,7 @@ var require_util4 = __commonJS({
|
|
|
100643
101108
|
}
|
|
100644
101109
|
return joined;
|
|
100645
101110
|
}
|
|
100646
|
-
exports.join =
|
|
101111
|
+
exports.join = join8;
|
|
100647
101112
|
exports.isAbsolute = function(aPath) {
|
|
100648
101113
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
100649
101114
|
};
|
|
@@ -100816,7 +101281,7 @@ var require_util4 = __commonJS({
|
|
|
100816
101281
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
100817
101282
|
}
|
|
100818
101283
|
}
|
|
100819
|
-
sourceURL =
|
|
101284
|
+
sourceURL = join8(urlGenerate(parsed), sourceURL);
|
|
100820
101285
|
}
|
|
100821
101286
|
return normalize(sourceURL);
|
|
100822
101287
|
}
|
|
@@ -314529,8 +314994,8 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314529
314994
|
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel)
|
|
314530
314995
|
})), { label: `script-heal-${attempt}` });
|
|
314531
314996
|
} catch (aiErr) {
|
|
314532
|
-
const
|
|
314533
|
-
logs2.push(`\u26A0 AI call failed: ${
|
|
314997
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
314998
|
+
logs2.push(`\u26A0 AI call failed: ${errMsg3}`);
|
|
314534
314999
|
attemptHistory.push({
|
|
314535
315000
|
phase: "script",
|
|
314536
315001
|
attempt,
|
|
@@ -314538,13 +315003,13 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314538
315003
|
passed: false,
|
|
314539
315004
|
changedCode: false,
|
|
314540
315005
|
summary: "AI call failed before returning a heal result",
|
|
314541
|
-
errorSummary: truncateForPrompt(
|
|
315006
|
+
errorSummary: truncateForPrompt(errMsg3),
|
|
314542
315007
|
failedStep: summarizeFailedStep(currentStepResults)
|
|
314543
315008
|
});
|
|
314544
315009
|
if (feedbackEnabled) {
|
|
314545
|
-
lastTransportFailure = truncateForPrompt(
|
|
315010
|
+
lastTransportFailure = truncateForPrompt(errMsg3, 200) ?? "the AI request failed";
|
|
314546
315011
|
} else {
|
|
314547
|
-
currentError = `AI call failed on attempt ${attempt}: ${
|
|
315012
|
+
currentError = `AI call failed on attempt ${attempt}: ${errMsg3}`;
|
|
314548
315013
|
}
|
|
314549
315014
|
markRecoverableTurn();
|
|
314550
315015
|
continue;
|
|
@@ -314828,8 +315293,8 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314828
315293
|
// item 5b
|
|
314829
315294
|
});
|
|
314830
315295
|
} catch (runErr) {
|
|
314831
|
-
const
|
|
314832
|
-
logs2.push(`\u{1F4A5} Native execution threw: ${
|
|
315296
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
315297
|
+
logs2.push(`\u{1F4A5} Native execution threw: ${errMsg3}`);
|
|
314833
315298
|
attemptHistory.push({
|
|
314834
315299
|
phase: "script",
|
|
314835
315300
|
attempt,
|
|
@@ -314837,9 +315302,9 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314837
315302
|
passed: false,
|
|
314838
315303
|
changedCode: true,
|
|
314839
315304
|
summary: truncateForPrompt(changeDesc) ?? "Applied code fix, but native execution threw before verification completed",
|
|
314840
|
-
errorSummary: truncateForPrompt(
|
|
315305
|
+
errorSummary: truncateForPrompt(errMsg3)
|
|
314841
315306
|
});
|
|
314842
|
-
currentError = `Native execution threw: ${
|
|
315307
|
+
currentError = `Native execution threw: ${errMsg3}`;
|
|
314843
315308
|
currentStepResults = [];
|
|
314844
315309
|
continue;
|
|
314845
315310
|
}
|
|
@@ -315595,9 +316060,9 @@ Last error (after heal attempts): ${lastErrorSnippet}` : `Heal aborted: same err
|
|
|
315595
316060
|
previousErrorSignature = errorSig;
|
|
315596
316061
|
}
|
|
315597
316062
|
} catch (runErr) {
|
|
315598
|
-
const
|
|
315599
|
-
logs2.push(`\u{1F4A5} Native run threw: ${
|
|
315600
|
-
currentError =
|
|
316063
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
316064
|
+
logs2.push(`\u{1F4A5} Native run threw: ${errMsg3}`);
|
|
316065
|
+
currentError = errMsg3;
|
|
315601
316066
|
currentStepResults = [];
|
|
315602
316067
|
}
|
|
315603
316068
|
} else {
|
|
@@ -316159,8 +316624,8 @@ When you call finish_fix, preserve locators only when they are stable structural
|
|
|
316159
316624
|
}
|
|
316160
316625
|
}
|
|
316161
316626
|
} catch (toolErr) {
|
|
316162
|
-
const
|
|
316163
|
-
let errorContent = `Error: ${
|
|
316627
|
+
const errMsg3 = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
316628
|
+
let errorContent = `Error: ${errMsg3}`;
|
|
316164
316629
|
try {
|
|
316165
316630
|
const consoleResult = await mcpClient.callTool({ name: "browser_console_messages", arguments: {} });
|
|
316166
316631
|
const consoleText = Array.isArray(consoleResult.content) ? consoleResult.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n") : String(consoleResult.content ?? "");
|
|
@@ -316181,8 +316646,8 @@ ${errorContent}`;
|
|
|
316181
316646
|
}
|
|
316182
316647
|
}
|
|
316183
316648
|
} catch (mcpErr) {
|
|
316184
|
-
const
|
|
316185
|
-
logs2.push(`\u{1F4A5} MCP exploration failed: ${
|
|
316649
|
+
const errMsg3 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
|
|
316650
|
+
logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg3}`);
|
|
316186
316651
|
}
|
|
316187
316652
|
if (verdict === "app_bug") {
|
|
316188
316653
|
phase2AttemptHistory.push({
|
|
@@ -316776,9 +317241,9 @@ ${testBlocks}
|
|
|
316776
317241
|
}
|
|
316777
317242
|
emit(` \u2713 ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
316778
317243
|
} catch (err) {
|
|
316779
|
-
const
|
|
316780
|
-
toolResultContent = `ERROR: ${
|
|
316781
|
-
emit(` \u2717 ${
|
|
317244
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
317245
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
317246
|
+
emit(` \u2717 ${errMsg3}`);
|
|
316782
317247
|
}
|
|
316783
317248
|
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
|
|
316784
317249
|
}
|
|
@@ -323062,8 +323527,8 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
323062
323527
|
const snapItems = Array.isArray(snapResult.content) ? snapResult.content : [];
|
|
323063
323528
|
snapshotText = extractInlineSnapshotYaml(snapItems.find((c) => c.type === "text")?.text || null);
|
|
323064
323529
|
} catch (err) {
|
|
323065
|
-
const
|
|
323066
|
-
const isTimeout = /timeout|timed out|TimeoutError/i.test(
|
|
323530
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
323531
|
+
const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg3);
|
|
323067
323532
|
return {
|
|
323068
323533
|
snapshotText: null,
|
|
323069
323534
|
meta: {
|
|
@@ -327139,9 +327604,9 @@ ${currentSummary}`;
|
|
|
327139
327604
|
});
|
|
327140
327605
|
}
|
|
327141
327606
|
} catch (err) {
|
|
327142
|
-
const
|
|
327143
|
-
logs2.push(` Error persisting page: ${
|
|
327144
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${
|
|
327607
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327608
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327609
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg3}` });
|
|
327145
327610
|
}
|
|
327146
327611
|
continue;
|
|
327147
327612
|
}
|
|
@@ -327343,12 +327808,12 @@ ${currentSummary}`;
|
|
|
327343
327808
|
})
|
|
327344
327809
|
});
|
|
327345
327810
|
} catch (err) {
|
|
327346
|
-
const
|
|
327347
|
-
logs2.push(` Error persisting page: ${
|
|
327811
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327812
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327348
327813
|
toolResults.push({
|
|
327349
327814
|
role: "tool",
|
|
327350
327815
|
tool_call_id: toolCall.id,
|
|
327351
|
-
content: `ERROR persisting page: ${
|
|
327816
|
+
content: `ERROR persisting page: ${errMsg3}`
|
|
327352
327817
|
});
|
|
327353
327818
|
}
|
|
327354
327819
|
continue;
|
|
@@ -327383,12 +327848,12 @@ ${currentSummary}`;
|
|
|
327383
327848
|
content: JSON.stringify({ acknowledged: true })
|
|
327384
327849
|
});
|
|
327385
327850
|
} catch (err) {
|
|
327386
|
-
const
|
|
327387
|
-
logs2.push(` Error persisting transition: ${
|
|
327851
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327852
|
+
logs2.push(` Error persisting transition: ${errMsg3}`);
|
|
327388
327853
|
toolResults.push({
|
|
327389
327854
|
role: "tool",
|
|
327390
327855
|
tool_call_id: toolCall.id,
|
|
327391
|
-
content: `ERROR persisting transition: ${
|
|
327856
|
+
content: `ERROR persisting transition: ${errMsg3}`
|
|
327392
327857
|
});
|
|
327393
327858
|
}
|
|
327394
327859
|
continue;
|
|
@@ -328084,15 +328549,15 @@ Exploration complete: ${summary}`);
|
|
|
328084
328549
|
toolResultContent = (0, snapshot_context_compactor_js_1.formatMCPToolResult)(toolResultContent, MAX_TOOL_RESULT_CHARS);
|
|
328085
328550
|
logs2.push(` OK: ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
328086
328551
|
} catch (err) {
|
|
328087
|
-
const
|
|
328088
|
-
toolResultContent = `ERROR: ${
|
|
328089
|
-
logs2.push(` Error: ${
|
|
328552
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
328553
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
328554
|
+
logs2.push(` Error: ${errMsg3}`);
|
|
328090
328555
|
if (useV2) {
|
|
328091
328556
|
(0, exploration_state_js_1.reduceEvent)(state2, {
|
|
328092
328557
|
type: "ACTION_FAILED",
|
|
328093
328558
|
route: state2.currentRoute,
|
|
328094
328559
|
action: `${toolName}(${JSON.stringify(toolArgs).slice(0, 100)})`,
|
|
328095
|
-
error:
|
|
328560
|
+
error: errMsg3
|
|
328096
328561
|
});
|
|
328097
328562
|
}
|
|
328098
328563
|
}
|
|
@@ -330941,10 +331406,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
330941
331406
|
onLog: options.onLog,
|
|
330942
331407
|
onProvenCommands: recordProvenCommands
|
|
330943
331408
|
});
|
|
330944
|
-
const
|
|
331409
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
330945
331410
|
const fixPrompt = [
|
|
330946
331411
|
"Your previous attempt did not produce a valid result file.",
|
|
330947
|
-
`Error: ${
|
|
331412
|
+
`Error: ${errMsg3}`,
|
|
330948
331413
|
`Re-read BRIEF.md and write a valid SurveyorOutput JSON to ${resultFilePath}.`,
|
|
330949
331414
|
"Single JSON object. No prose. Then exit."
|
|
330950
331415
|
].join("\n");
|
|
@@ -333085,7 +333550,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333085
333550
|
log2(`First-pass discovery-result invalid or missing: ${firstError.message}`);
|
|
333086
333551
|
}
|
|
333087
333552
|
if (!parsedResult) {
|
|
333088
|
-
const
|
|
333553
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
333089
333554
|
if (planDebounceTimerRef.current) {
|
|
333090
333555
|
clearTimeout(planDebounceTimerRef.current);
|
|
333091
333556
|
planDebounceTimerRef.current = void 0;
|
|
@@ -333110,7 +333575,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333110
333575
|
""
|
|
333111
333576
|
].join("\n");
|
|
333112
333577
|
const fixPrompt = [
|
|
333113
|
-
`Your previous attempt did not produce a valid result file. Error: ${
|
|
333578
|
+
`Your previous attempt did not produce a valid result file. Error: ${errMsg3}`,
|
|
333114
333579
|
"Re-read BRIEF.md.",
|
|
333115
333580
|
phaseContext,
|
|
333116
333581
|
`Continue from where you left off \u2014 already verified tests are in ${testsDirPath}/`,
|
|
@@ -334739,10 +335204,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
334739
335204
|
onLog: parserLog,
|
|
334740
335205
|
onProvenCommands: recordProvenCommands
|
|
334741
335206
|
});
|
|
334742
|
-
const
|
|
335207
|
+
const errMsg3 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
|
|
334743
335208
|
const fixPrompt = [
|
|
334744
335209
|
"Your previous attempt did not leave a valid result file.",
|
|
334745
|
-
`Error: ${
|
|
335210
|
+
`Error: ${errMsg3}`,
|
|
334746
335211
|
`Writing a valid SurveyorOutput JSON object to ${resultFilePath} is your ONLY priority now.`,
|
|
334747
335212
|
"Procedure: navigate to at most 6-8 of the main pages, then IMMEDIATELY use",
|
|
334748
335213
|
`the Write tool to save the SurveyorOutput JSON to ${resultFilePath}. Do this`,
|
|
@@ -338623,8 +339088,8 @@ var require_resource_governor = __commonJS({
|
|
|
338623
339088
|
function isDiscoveryLike(mode) {
|
|
338624
339089
|
return mode === "discovery";
|
|
338625
339090
|
}
|
|
338626
|
-
function evaluateRunAdmission2(options, request) {
|
|
338627
|
-
const snapshot = getResourceSnapshot2();
|
|
339091
|
+
function evaluateRunAdmission2(options, request, snapshotOverride) {
|
|
339092
|
+
const snapshot = snapshotOverride ?? getResourceSnapshot2();
|
|
338628
339093
|
const candidateWeight = modeWeight(request.mode, options.weights);
|
|
338629
339094
|
const currentUnits = activeUnits(request.activeByType, options.weights);
|
|
338630
339095
|
const memory = pressureFactor(snapshot.memoryPct, options.memorySoftPct, options.memoryHardPct);
|
|
@@ -341122,8 +341587,8 @@ var require_adm_zip = __commonJS({
|
|
|
341122
341587
|
return null;
|
|
341123
341588
|
}
|
|
341124
341589
|
function fixPath(zipPath) {
|
|
341125
|
-
const { join:
|
|
341126
|
-
return
|
|
341590
|
+
const { join: join8, normalize, sep } = pth.posix;
|
|
341591
|
+
return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
|
|
341127
341592
|
}
|
|
341128
341593
|
function filenameFilter(filterfn) {
|
|
341129
341594
|
if (filterfn instanceof RegExp) {
|
|
@@ -343958,11 +344423,11 @@ var require_winchars = __commonJS({
|
|
|
343958
344423
|
var require_strip_absolute_path = __commonJS({
|
|
343959
344424
|
"../../node_modules/tar/lib/strip-absolute-path.js"(exports, module) {
|
|
343960
344425
|
"use strict";
|
|
343961
|
-
var { isAbsolute, parse } = __require("path").win32;
|
|
344426
|
+
var { isAbsolute: isAbsolute2, parse } = __require("path").win32;
|
|
343962
344427
|
module.exports = (path) => {
|
|
343963
344428
|
let r = "";
|
|
343964
344429
|
let parsed = parse(path);
|
|
343965
|
-
while (
|
|
344430
|
+
while (isAbsolute2(path) || parsed.root) {
|
|
343966
344431
|
const root = path.charAt(0) === "/" && path.slice(0, 4) !== "//?/" ? "/" : parsed.root;
|
|
343967
344432
|
path = path.slice(root.length);
|
|
343968
344433
|
r += root;
|
|
@@ -346474,7 +346939,7 @@ var require_update = __commonJS({
|
|
|
346474
346939
|
var require_opts_arg = __commonJS({
|
|
346475
346940
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports, module) {
|
|
346476
346941
|
"use strict";
|
|
346477
|
-
var { promisify:
|
|
346942
|
+
var { promisify: promisify4 } = __require("util");
|
|
346478
346943
|
var fs = __require("fs");
|
|
346479
346944
|
var optsArg = (opts) => {
|
|
346480
346945
|
if (!opts)
|
|
@@ -346488,9 +346953,9 @@ var require_opts_arg = __commonJS({
|
|
|
346488
346953
|
else
|
|
346489
346954
|
throw new TypeError("invalid options argument");
|
|
346490
346955
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
346491
|
-
opts.mkdirAsync =
|
|
346956
|
+
opts.mkdirAsync = promisify4(opts.mkdir);
|
|
346492
346957
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
346493
|
-
opts.statAsync =
|
|
346958
|
+
opts.statAsync = promisify4(opts.stat);
|
|
346494
346959
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
346495
346960
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
346496
346961
|
return opts;
|
|
@@ -346743,10 +347208,10 @@ var require_chownr = __commonJS({
|
|
|
346743
347208
|
}
|
|
346744
347209
|
} : (path2, uid, gid) => lchownSync(path2, uid, gid);
|
|
346745
347210
|
var nodeVersion = process.version;
|
|
346746
|
-
var
|
|
347211
|
+
var readdir3 = (path2, options, cb) => fs.readdir(path2, options, cb);
|
|
346747
347212
|
var readdirSync = (path2, options) => fs.readdirSync(path2, options);
|
|
346748
347213
|
if (/^v4\./.test(nodeVersion))
|
|
346749
|
-
|
|
347214
|
+
readdir3 = (path2, options, cb) => fs.readdir(path2, cb);
|
|
346750
347215
|
var chown = (cpath, uid, gid, cb) => {
|
|
346751
347216
|
fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, (er) => {
|
|
346752
347217
|
cb(er && er.code !== "ENOENT" ? er : null);
|
|
@@ -346773,7 +347238,7 @@ var require_chownr = __commonJS({
|
|
|
346773
347238
|
}
|
|
346774
347239
|
};
|
|
346775
347240
|
var chownr = (p, uid, gid, cb) => {
|
|
346776
|
-
|
|
347241
|
+
readdir3(p, { withFileTypes: true }, (er, children) => {
|
|
346777
347242
|
if (er) {
|
|
346778
347243
|
if (er.code === "ENOENT")
|
|
346779
347244
|
return cb();
|
|
@@ -347047,7 +347512,7 @@ var require_path_reservations = __commonJS({
|
|
|
347047
347512
|
var assert = __require("assert");
|
|
347048
347513
|
var normalize = require_normalize_unicode();
|
|
347049
347514
|
var stripSlashes = require_strip_trailing_slashes();
|
|
347050
|
-
var { join:
|
|
347515
|
+
var { join: join8 } = __require("path");
|
|
347051
347516
|
var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
|
|
347052
347517
|
var isWindows = platform3 === "win32";
|
|
347053
347518
|
module.exports = () => {
|
|
@@ -347056,7 +347521,7 @@ var require_path_reservations = __commonJS({
|
|
|
347056
347521
|
const getDirs = (path) => {
|
|
347057
347522
|
const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
|
|
347058
347523
|
if (set.length) {
|
|
347059
|
-
path2 =
|
|
347524
|
+
path2 = join8(set[set.length - 1], path2);
|
|
347060
347525
|
}
|
|
347061
347526
|
set.push(path2 || "/");
|
|
347062
347527
|
return set;
|
|
@@ -347124,7 +347589,7 @@ var require_path_reservations = __commonJS({
|
|
|
347124
347589
|
};
|
|
347125
347590
|
const reserve = (paths, fn) => {
|
|
347126
347591
|
paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
|
|
347127
|
-
return stripSlashes(
|
|
347592
|
+
return stripSlashes(join8(normalize(p))).toLowerCase();
|
|
347128
347593
|
});
|
|
347129
347594
|
const dirs = new Set(
|
|
347130
347595
|
paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
|
|
@@ -347179,7 +347644,7 @@ var require_unpack = __commonJS({
|
|
|
347179
347644
|
var fs = __require("fs");
|
|
347180
347645
|
var fsm = require_fs_minipass();
|
|
347181
347646
|
var path = __require("path");
|
|
347182
|
-
var
|
|
347647
|
+
var mkdir3 = require_mkdir();
|
|
347183
347648
|
var wc = require_winchars();
|
|
347184
347649
|
var pathReservations = require_path_reservations();
|
|
347185
347650
|
var stripAbsolutePath = require_strip_absolute_path();
|
|
@@ -347430,7 +347895,7 @@ var require_unpack = __commonJS({
|
|
|
347430
347895
|
}
|
|
347431
347896
|
}
|
|
347432
347897
|
[MKDIR](dir, mode, cb) {
|
|
347433
|
-
|
|
347898
|
+
mkdir3(normPath(dir), {
|
|
347434
347899
|
uid: this.uid,
|
|
347435
347900
|
gid: this.gid,
|
|
347436
347901
|
processUid: this.processUid,
|
|
@@ -347833,7 +348298,7 @@ var require_unpack = __commonJS({
|
|
|
347833
348298
|
}
|
|
347834
348299
|
[MKDIR](dir, mode) {
|
|
347835
348300
|
try {
|
|
347836
|
-
return
|
|
348301
|
+
return mkdir3.sync(normPath(dir), {
|
|
347837
348302
|
uid: this.uid,
|
|
347838
348303
|
gid: this.gid,
|
|
347839
348304
|
processUid: this.processUid,
|
|
@@ -351339,7 +351804,7 @@ var require_process_inventory = __commonJS({
|
|
|
351339
351804
|
var node_child_process_1 = __require("child_process");
|
|
351340
351805
|
var node_os_1 = __importDefault(__require("os"));
|
|
351341
351806
|
var node_util_1 = __require("util");
|
|
351342
|
-
var
|
|
351807
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
351343
351808
|
var DEFAULT_LIMIT = 60;
|
|
351344
351809
|
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
351345
351810
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -351401,7 +351866,7 @@ var require_process_inventory = __commonJS({
|
|
|
351401
351866
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
351402
351867
|
timer.unref?.();
|
|
351403
351868
|
try {
|
|
351404
|
-
const { stdout } = await
|
|
351869
|
+
const { stdout } = await execFileAsync4(command, args, {
|
|
351405
351870
|
maxBuffer: 1024 * 1024,
|
|
351406
351871
|
signal: controller.signal
|
|
351407
351872
|
});
|
|
@@ -352547,7 +353012,7 @@ var require_package4 = __commonJS({
|
|
|
352547
353012
|
"package.json"(exports, module) {
|
|
352548
353013
|
module.exports = {
|
|
352549
353014
|
name: "@validate.qa/runner",
|
|
352550
|
-
version: "1.0.
|
|
353015
|
+
version: "1.0.72",
|
|
352551
353016
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
352552
353017
|
bin: {
|
|
352553
353018
|
"validate-runner": "dist/cli.js"
|
|
@@ -352556,7 +353021,7 @@ var require_package4 = __commonJS({
|
|
|
352556
353021
|
build: "tsup",
|
|
352557
353022
|
dev: "tsx src/cli.ts",
|
|
352558
353023
|
test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
|
|
352559
|
-
"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",
|
|
353024
|
+
"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",
|
|
352560
353025
|
prepublishOnly: "npm run build"
|
|
352561
353026
|
},
|
|
352562
353027
|
files: [
|
|
@@ -352634,7 +353099,7 @@ function getConfigDir() {
|
|
|
352634
353099
|
}
|
|
352635
353100
|
|
|
352636
353101
|
// src/runner.ts
|
|
352637
|
-
import { join as
|
|
353102
|
+
import { join as join7 } from "path";
|
|
352638
353103
|
import { homedir as homedir3 } from "os";
|
|
352639
353104
|
import { createHash } from "crypto";
|
|
352640
353105
|
|
|
@@ -353229,6 +353694,69 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
353229
353694
|
};
|
|
353230
353695
|
}
|
|
353231
353696
|
|
|
353697
|
+
// src/services/mobile/ios-simulator-reset.ts
|
|
353698
|
+
import { execFile } from "child_process";
|
|
353699
|
+
import { promisify } from "util";
|
|
353700
|
+
import { mkdir, readdir, rm } from "fs/promises";
|
|
353701
|
+
import { join as join3, isAbsolute } from "path";
|
|
353702
|
+
var execFileAsync = promisify(execFile);
|
|
353703
|
+
var SIMCTL_TIMEOUT_MS = 15e3;
|
|
353704
|
+
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;
|
|
353705
|
+
function isIosSimulatorUdid(udid) {
|
|
353706
|
+
return typeof udid === "string" && SIMULATOR_UDID_RE.test(udid.trim());
|
|
353707
|
+
}
|
|
353708
|
+
function errMsg(err) {
|
|
353709
|
+
return err instanceof Error ? err.message : String(err);
|
|
353710
|
+
}
|
|
353711
|
+
async function simctl(args) {
|
|
353712
|
+
const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
|
|
353713
|
+
timeout: SIMCTL_TIMEOUT_MS,
|
|
353714
|
+
encoding: "utf-8"
|
|
353715
|
+
});
|
|
353716
|
+
return stdout.trim();
|
|
353717
|
+
}
|
|
353718
|
+
async function clearIosSimulatorAppData(udid, bundleId, env = process.env) {
|
|
353719
|
+
if (!isIosSimulatorUdid(udid)) {
|
|
353720
|
+
return { ok: false, detail: `'${udid}' is not a simulator UDID \u2014 physical iOS devices cannot be cleared` };
|
|
353721
|
+
}
|
|
353722
|
+
let container;
|
|
353723
|
+
try {
|
|
353724
|
+
container = await simctl(["get_app_container", udid, bundleId, "data"]);
|
|
353725
|
+
} catch (err) {
|
|
353726
|
+
return { ok: false, detail: `simctl get_app_container failed (app not installed?): ${errMsg(err)}` };
|
|
353727
|
+
}
|
|
353728
|
+
if (!isAbsolute(container) || !container.includes("/Containers/Data/Application/")) {
|
|
353729
|
+
return { ok: false, detail: `unexpected app container path: ${container}` };
|
|
353730
|
+
}
|
|
353731
|
+
try {
|
|
353732
|
+
await simctl(["spawn", udid, "defaults", "delete", bundleId]);
|
|
353733
|
+
} catch {
|
|
353734
|
+
}
|
|
353735
|
+
try {
|
|
353736
|
+
const entries = await readdir(container);
|
|
353737
|
+
for (const entry of entries) {
|
|
353738
|
+
await rm(join3(container, entry), { recursive: true, force: true });
|
|
353739
|
+
}
|
|
353740
|
+
for (const dir of ["Documents", "Library/Caches", "Library/Preferences", "tmp"]) {
|
|
353741
|
+
await mkdir(join3(container, dir), { recursive: true });
|
|
353742
|
+
}
|
|
353743
|
+
} catch (err) {
|
|
353744
|
+
return { ok: false, detail: `app container wipe failed: ${errMsg(err)}` };
|
|
353745
|
+
}
|
|
353746
|
+
const keychainPolicy = (env.VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET ?? "").trim().toLowerCase();
|
|
353747
|
+
if (keychainPolicy !== "never") {
|
|
353748
|
+
try {
|
|
353749
|
+
await simctl(["keychain", udid, "reset"]);
|
|
353750
|
+
} catch (err) {
|
|
353751
|
+
return {
|
|
353752
|
+
ok: false,
|
|
353753
|
+
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.`
|
|
353754
|
+
};
|
|
353755
|
+
}
|
|
353756
|
+
}
|
|
353757
|
+
return { ok: true };
|
|
353758
|
+
}
|
|
353759
|
+
|
|
353232
353760
|
// src/services/mobile/appium-actions.ts
|
|
353233
353761
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
353234
353762
|
var APPIUM_WEBVIEW_CONTEXT_TIMEOUT_MS = 12e4;
|
|
@@ -353411,9 +353939,12 @@ async function tapAt(sessionPath, bounds) {
|
|
|
353411
353939
|
}
|
|
353412
353940
|
}
|
|
353413
353941
|
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
353414
|
-
if (platform3 !== "ANDROID") return false;
|
|
353415
353942
|
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
353416
353943
|
if (policy === "never") return false;
|
|
353944
|
+
if (platform3 === "IOS") {
|
|
353945
|
+
return isIosSimulatorUdid(deviceId);
|
|
353946
|
+
}
|
|
353947
|
+
if (platform3 !== "ANDROID") return false;
|
|
353417
353948
|
if (policy === "always") return true;
|
|
353418
353949
|
return !!deviceId && deviceId.startsWith("emulator-");
|
|
353419
353950
|
}
|
|
@@ -354292,12 +354823,51 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354292
354823
|
});
|
|
354293
354824
|
}
|
|
354294
354825
|
|
|
354826
|
+
// src/services/device-quarantine.ts
|
|
354827
|
+
var QUARANTINE_MAX_MS = (() => {
|
|
354828
|
+
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
354829
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354830
|
+
})();
|
|
354831
|
+
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354832
|
+
var DeviceQuarantinedError = class extends Error {
|
|
354833
|
+
constructor(deviceId) {
|
|
354834
|
+
super(
|
|
354835
|
+
`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.`
|
|
354836
|
+
);
|
|
354837
|
+
this.name = "DeviceQuarantinedError";
|
|
354838
|
+
}
|
|
354839
|
+
};
|
|
354840
|
+
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354841
|
+
quarantinedAt.set(deviceId, now);
|
|
354842
|
+
}
|
|
354843
|
+
function unquarantineDevice(deviceId) {
|
|
354844
|
+
quarantinedAt.delete(deviceId);
|
|
354845
|
+
}
|
|
354846
|
+
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354847
|
+
if (typeof deviceId !== "string") return false;
|
|
354848
|
+
const since = quarantinedAt.get(deviceId);
|
|
354849
|
+
if (since === void 0) return false;
|
|
354850
|
+
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354851
|
+
quarantinedAt.delete(deviceId);
|
|
354852
|
+
console.warn(
|
|
354853
|
+
`[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).`
|
|
354854
|
+
);
|
|
354855
|
+
return false;
|
|
354856
|
+
}
|
|
354857
|
+
return true;
|
|
354858
|
+
}
|
|
354859
|
+
function assertDeviceNotQuarantined(deviceId, now = Date.now()) {
|
|
354860
|
+
if (isDeviceQuarantined(deviceId, now)) {
|
|
354861
|
+
throw new DeviceQuarantinedError(deviceId);
|
|
354862
|
+
}
|
|
354863
|
+
}
|
|
354864
|
+
|
|
354295
354865
|
// src/services/appium-executor.ts
|
|
354296
354866
|
var import_runner_core8 = __toESM(require_dist2());
|
|
354297
|
-
import { execFile as
|
|
354298
|
-
import { promisify as
|
|
354867
|
+
import { execFile as execFile3 } from "child_process";
|
|
354868
|
+
import { promisify as promisify3 } from "util";
|
|
354299
354869
|
import { tmpdir as tmpdir3 } from "os";
|
|
354300
|
-
import { join as
|
|
354870
|
+
import { join as join5 } from "path";
|
|
354301
354871
|
|
|
354302
354872
|
// src/services/device-discovery.ts
|
|
354303
354873
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -354604,9 +355174,6 @@ function countReadyDevicesByKind(platform3, snapshot = getCachedDeviceSnapshot()
|
|
|
354604
355174
|
function selectFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(), requirements = {}) {
|
|
354605
355175
|
const ready = snapshot.filter((d) => d.platform === platform3 && d.isAvailable && d.state === "BOOTED");
|
|
354606
355176
|
if (ready.length === 0) return { kind: "busy", reason: `no booted ${platform3} device` };
|
|
354607
|
-
if (requirements.resetPolicy === "CLEAR_APP_DATA" && platform3 !== "ANDROID") {
|
|
354608
|
-
return { kind: "unsatisfiable", reason: "CLEAR_APP_DATA is unsupported on iOS" };
|
|
354609
|
-
}
|
|
354610
355177
|
let candidates = requirements.resetPolicy === "CLEAR_APP_DATA" ? ready.filter((d) => !d.isPhysical) : [...ready];
|
|
354611
355178
|
const appId = requirements.appId?.trim();
|
|
354612
355179
|
if (appId) {
|
|
@@ -354655,23 +355222,23 @@ function pickFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(),
|
|
|
354655
355222
|
|
|
354656
355223
|
// src/services/mobile/mobile-network-capture.ts
|
|
354657
355224
|
var import_runner_core7 = __toESM(require_dist2());
|
|
354658
|
-
import { execFile, execFileSync as execFileSync3 } from "child_process";
|
|
355225
|
+
import { execFile as execFile2, execFileSync as execFileSync3 } from "child_process";
|
|
354659
355226
|
import { randomUUID } from "crypto";
|
|
354660
|
-
import { mkdtemp, rm, writeFile, readFile, readdir, mkdir, unlink } from "fs/promises";
|
|
355227
|
+
import { mkdtemp, rm as rm2, writeFile, readFile, readdir as readdir2, mkdir as mkdir2, unlink } from "fs/promises";
|
|
354661
355228
|
import { tmpdir as tmpdir2, networkInterfaces } from "os";
|
|
354662
|
-
import { join as
|
|
354663
|
-
import { promisify } from "util";
|
|
355229
|
+
import { join as join4 } from "path";
|
|
355230
|
+
import { promisify as promisify2 } from "util";
|
|
354664
355231
|
import {
|
|
354665
355232
|
getLocal,
|
|
354666
355233
|
generateCACertificate
|
|
354667
355234
|
} from "mockttp";
|
|
354668
|
-
var
|
|
355235
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
354669
355236
|
var MAX_CALLS = 1e3;
|
|
354670
355237
|
var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
|
|
354671
355238
|
var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
354672
355239
|
var CA_KEY_BITS = 2048;
|
|
354673
355240
|
var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
|
|
354674
|
-
var PENDING_CLEANUP_DIR =
|
|
355241
|
+
var PENDING_CLEANUP_DIR = join4(tmpdir2(), "validateqa-mobile-capture-pending");
|
|
354675
355242
|
var IOS_SIM_HOST_PROXY_ENV = "VALIDATEQA_MOBILE_IOS_SIM_HOST_PROXY";
|
|
354676
355243
|
var STALE_MARKER_RECONCILE_MS = 6 * 60 * 60 * 1e3;
|
|
354677
355244
|
function envFlag(name) {
|
|
@@ -354853,7 +355420,7 @@ var MitmProxy = class {
|
|
|
354853
355420
|
try {
|
|
354854
355421
|
await server3.stop();
|
|
354855
355422
|
} catch (err) {
|
|
354856
|
-
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${
|
|
355423
|
+
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg2(err)}`);
|
|
354857
355424
|
}
|
|
354858
355425
|
this.onLog?.(
|
|
354859
355426
|
`[mobile-net] proxy closed: ${this.calls.length} calls captured` + (this.dropped > 0 ? ` (${this.dropped} dropped over cap)` : "")
|
|
@@ -354878,7 +355445,7 @@ var MitmProxy = class {
|
|
|
354878
355445
|
isHttps
|
|
354879
355446
|
});
|
|
354880
355447
|
} catch (err) {
|
|
354881
|
-
this.onLog?.(`[mobile-net] request capture error (ignored): ${
|
|
355448
|
+
this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg2(err)}`);
|
|
354882
355449
|
}
|
|
354883
355450
|
}
|
|
354884
355451
|
// ── response → finalize the matched request ───────────────────────────────
|
|
@@ -354921,7 +355488,7 @@ var MitmProxy = class {
|
|
|
354921
355488
|
intercepted: true
|
|
354922
355489
|
});
|
|
354923
355490
|
} catch (err) {
|
|
354924
|
-
this.onLog?.(`[mobile-net] response capture error (ignored): ${
|
|
355491
|
+
this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg2(err)}`);
|
|
354925
355492
|
}
|
|
354926
355493
|
}
|
|
354927
355494
|
// ── tls-client-error → record WHY interception failed ─────────────────────
|
|
@@ -354998,17 +355565,17 @@ async function generateCaCert(dir, onLog) {
|
|
|
354998
355565
|
// rely on stop() to remove the cert from the device promptly after the run
|
|
354999
355566
|
// rather than on a short expiry.
|
|
355000
355567
|
});
|
|
355001
|
-
const caCertPath =
|
|
355568
|
+
const caCertPath = join4(dir, "validateqa-mobile-ca.pem");
|
|
355002
355569
|
await writeFile(caCertPath, cert, "utf-8");
|
|
355003
355570
|
return { caCertPath, caKeyPem: key, caCertPem: cert };
|
|
355004
355571
|
} catch (err) {
|
|
355005
|
-
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${
|
|
355572
|
+
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg2(err)}`);
|
|
355006
355573
|
return null;
|
|
355007
355574
|
}
|
|
355008
355575
|
}
|
|
355009
355576
|
async function androidReadProxy(deviceId) {
|
|
355010
355577
|
try {
|
|
355011
|
-
const { stdout } = await
|
|
355578
|
+
const { stdout } = await execFileAsync2(
|
|
355012
355579
|
"adb",
|
|
355013
355580
|
["-s", deviceId, "shell", "settings", "get", "global", "http_proxy"],
|
|
355014
355581
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
@@ -355021,33 +355588,33 @@ async function androidReadProxy(deviceId) {
|
|
|
355021
355588
|
}
|
|
355022
355589
|
async function androidSetProxy(deviceId, hostPort, onLog) {
|
|
355023
355590
|
try {
|
|
355024
|
-
await
|
|
355591
|
+
await execFileAsync2(
|
|
355025
355592
|
"adb",
|
|
355026
355593
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", hostPort],
|
|
355027
355594
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355028
355595
|
);
|
|
355029
355596
|
return true;
|
|
355030
355597
|
} catch (err) {
|
|
355031
|
-
onLog?.(`[mobile-net] adb set http_proxy failed: ${
|
|
355598
|
+
onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg2(err)}`);
|
|
355032
355599
|
return false;
|
|
355033
355600
|
}
|
|
355034
355601
|
}
|
|
355035
355602
|
async function androidClearProxy(deviceId, originalProxy, onLog) {
|
|
355036
355603
|
const restoreValue = originalProxy && originalProxy.length > 0 ? originalProxy : ":0";
|
|
355037
355604
|
try {
|
|
355038
|
-
await
|
|
355605
|
+
await execFileAsync2(
|
|
355039
355606
|
"adb",
|
|
355040
355607
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", restoreValue],
|
|
355041
355608
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355042
355609
|
);
|
|
355043
355610
|
} catch (err) {
|
|
355044
|
-
onLog?.(`[mobile-net] adb restore http_proxy failed: ${
|
|
355611
|
+
onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg2(err)}`);
|
|
355045
355612
|
}
|
|
355046
355613
|
}
|
|
355047
355614
|
async function androidInstallCa(deviceId, caCertPath) {
|
|
355048
355615
|
const devicePath = "/sdcard/validateqa-mobile-ca.pem";
|
|
355049
355616
|
try {
|
|
355050
|
-
await
|
|
355617
|
+
await execFileAsync2("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
|
|
355051
355618
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355052
355619
|
});
|
|
355053
355620
|
return {
|
|
@@ -355055,23 +355622,23 @@ async function androidInstallCa(deviceId, caCertPath) {
|
|
|
355055
355622
|
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.`
|
|
355056
355623
|
};
|
|
355057
355624
|
} catch (err) {
|
|
355058
|
-
return { installed: false, note: `CA push failed: ${
|
|
355625
|
+
return { installed: false, note: `CA push failed: ${errMsg2(err)}` };
|
|
355059
355626
|
}
|
|
355060
355627
|
}
|
|
355061
355628
|
async function androidRemoveCa(deviceId, onLog) {
|
|
355062
355629
|
try {
|
|
355063
|
-
await
|
|
355630
|
+
await execFileAsync2(
|
|
355064
355631
|
"adb",
|
|
355065
355632
|
["-s", deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"],
|
|
355066
355633
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355067
355634
|
);
|
|
355068
355635
|
} catch (err) {
|
|
355069
|
-
onLog?.(`[mobile-net] adb remove CA failed: ${
|
|
355636
|
+
onLog?.(`[mobile-net] adb remove CA failed: ${errMsg2(err)}`);
|
|
355070
355637
|
}
|
|
355071
355638
|
}
|
|
355072
355639
|
async function iosSimTrustCa(deviceId, caCertPath) {
|
|
355073
355640
|
try {
|
|
355074
|
-
await
|
|
355641
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
|
|
355075
355642
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355076
355643
|
});
|
|
355077
355644
|
return {
|
|
@@ -355079,18 +355646,18 @@ async function iosSimTrustCa(deviceId, caCertPath) {
|
|
|
355079
355646
|
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."
|
|
355080
355647
|
};
|
|
355081
355648
|
} catch (err) {
|
|
355082
|
-
return { trusted: false, note: `simctl add-root-cert failed: ${
|
|
355649
|
+
return { trusted: false, note: `simctl add-root-cert failed: ${errMsg2(err)}` };
|
|
355083
355650
|
}
|
|
355084
355651
|
}
|
|
355085
355652
|
async function iosSimRemoveCa(deviceId, onLog) {
|
|
355086
355653
|
if (process.env.VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET === "1") {
|
|
355087
355654
|
try {
|
|
355088
|
-
await
|
|
355655
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "reset"], {
|
|
355089
355656
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355090
355657
|
});
|
|
355091
355658
|
onLog?.(`[mobile-net] simctl keychain reset run for ${deviceId} (opt-in; cleared ALL sim certs).`);
|
|
355092
355659
|
} catch (err) {
|
|
355093
|
-
onLog?.(`[mobile-net] simctl keychain reset failed: ${
|
|
355660
|
+
onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg2(err)}`);
|
|
355094
355661
|
}
|
|
355095
355662
|
return;
|
|
355096
355663
|
}
|
|
@@ -355098,7 +355665,7 @@ async function iosSimRemoveCa(deviceId, onLog) {
|
|
|
355098
355665
|
`[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.`
|
|
355099
355666
|
);
|
|
355100
355667
|
}
|
|
355101
|
-
function
|
|
355668
|
+
function errMsg2(err) {
|
|
355102
355669
|
return err instanceof Error ? err.message : String(err);
|
|
355103
355670
|
}
|
|
355104
355671
|
function isProcessAlive(pid) {
|
|
@@ -355189,7 +355756,7 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355189
355756
|
let service;
|
|
355190
355757
|
let prior;
|
|
355191
355758
|
try {
|
|
355192
|
-
const { stdout: order } = await
|
|
355759
|
+
const { stdout: order } = await execFileAsync2("networksetup", ["-listnetworkserviceorder"], {
|
|
355193
355760
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355194
355761
|
});
|
|
355195
355762
|
const svc = parsePrimaryNetworkService(order);
|
|
@@ -355198,26 +355765,26 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355198
355765
|
}
|
|
355199
355766
|
service = svc;
|
|
355200
355767
|
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
355201
|
-
|
|
355202
|
-
|
|
355768
|
+
execFileAsync2("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
355769
|
+
execFileAsync2("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
355203
355770
|
]);
|
|
355204
355771
|
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
355205
355772
|
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
355206
|
-
await
|
|
355773
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355207
355774
|
}
|
|
355208
355775
|
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
355209
355776
|
return { configured: true, service, prior };
|
|
355210
355777
|
} catch (err) {
|
|
355211
355778
|
if (service && prior) {
|
|
355212
|
-
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${
|
|
355779
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg2(err)}`);
|
|
355213
355780
|
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
355214
355781
|
try {
|
|
355215
|
-
await
|
|
355782
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355216
355783
|
} catch {
|
|
355217
355784
|
}
|
|
355218
355785
|
}
|
|
355219
355786
|
}
|
|
355220
|
-
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${
|
|
355787
|
+
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg2(err)}` };
|
|
355221
355788
|
}
|
|
355222
355789
|
}
|
|
355223
355790
|
async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
@@ -355227,9 +355794,9 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
|
355227
355794
|
};
|
|
355228
355795
|
for (const args of buildHostProxyRestoreCommands(service, restore)) {
|
|
355229
355796
|
try {
|
|
355230
|
-
await
|
|
355797
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355231
355798
|
} catch (err) {
|
|
355232
|
-
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${
|
|
355799
|
+
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg2(err)}`);
|
|
355233
355800
|
}
|
|
355234
355801
|
}
|
|
355235
355802
|
onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
|
|
@@ -355237,11 +355804,11 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
|
355237
355804
|
var activeCleanups = /* @__PURE__ */ new Map();
|
|
355238
355805
|
var exitHandlersInstalled = false;
|
|
355239
355806
|
function markerPath(sessionKey) {
|
|
355240
|
-
return
|
|
355807
|
+
return join4(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
|
|
355241
355808
|
}
|
|
355242
355809
|
async function writePendingCleanup(sessionKey, marker) {
|
|
355243
355810
|
try {
|
|
355244
|
-
await
|
|
355811
|
+
await mkdir2(PENDING_CLEANUP_DIR, { recursive: true });
|
|
355245
355812
|
await writeFile(markerPath(sessionKey), JSON.stringify(marker), "utf-8");
|
|
355246
355813
|
} catch {
|
|
355247
355814
|
}
|
|
@@ -355281,7 +355848,7 @@ function restoreDeviceSync(marker, onLog) {
|
|
|
355281
355848
|
async function reconcilePendingCleanups(onLog) {
|
|
355282
355849
|
let files;
|
|
355283
355850
|
try {
|
|
355284
|
-
files = await
|
|
355851
|
+
files = await readdir2(PENDING_CLEANUP_DIR);
|
|
355285
355852
|
} catch {
|
|
355286
355853
|
return;
|
|
355287
355854
|
}
|
|
@@ -355289,7 +355856,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
355289
355856
|
if (!file.endsWith(".json")) continue;
|
|
355290
355857
|
const sessionKey = file.slice(0, -".json".length);
|
|
355291
355858
|
if (activeCleanups.has(sessionKey)) continue;
|
|
355292
|
-
const full =
|
|
355859
|
+
const full = join4(PENDING_CLEANUP_DIR, file);
|
|
355293
355860
|
try {
|
|
355294
355861
|
const raw = await readFile(full, "utf-8");
|
|
355295
355862
|
const marker = JSON.parse(raw);
|
|
@@ -355342,7 +355909,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355342
355909
|
let caKeyPem = null;
|
|
355343
355910
|
let caCertPem = null;
|
|
355344
355911
|
try {
|
|
355345
|
-
tmpDir = await mkdtemp(
|
|
355912
|
+
tmpDir = await mkdtemp(join4(tmpdir2(), "validateqa-mobile-ca-"));
|
|
355346
355913
|
const ca = await generateCaCert(tmpDir, onLog);
|
|
355347
355914
|
if (ca) {
|
|
355348
355915
|
caCertPath = ca.caCertPath;
|
|
@@ -355350,7 +355917,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355350
355917
|
caCertPem = ca.caCertPem;
|
|
355351
355918
|
}
|
|
355352
355919
|
} catch (err) {
|
|
355353
|
-
onLog?.(`[mobile-net] CA setup degraded: ${
|
|
355920
|
+
onLog?.(`[mobile-net] CA setup degraded: ${errMsg2(err)}`);
|
|
355354
355921
|
}
|
|
355355
355922
|
if (!caKeyPem || !caCertPem) {
|
|
355356
355923
|
try {
|
|
@@ -355358,9 +355925,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355358
355925
|
caKeyPem = fallback.key;
|
|
355359
355926
|
caCertPem = fallback.cert;
|
|
355360
355927
|
} catch (err) {
|
|
355361
|
-
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${
|
|
355928
|
+
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg2(err)}`);
|
|
355362
355929
|
if (tmpDir) {
|
|
355363
|
-
await
|
|
355930
|
+
await rm2(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
355364
355931
|
}
|
|
355365
355932
|
throw err instanceof Error ? err : new Error(String(err));
|
|
355366
355933
|
}
|
|
@@ -355415,7 +355982,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355415
355982
|
}
|
|
355416
355983
|
}
|
|
355417
355984
|
} catch (err) {
|
|
355418
|
-
wiringReason = `device wiring degraded: ${
|
|
355985
|
+
wiringReason = `device wiring degraded: ${errMsg2(err)}`;
|
|
355419
355986
|
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
355420
355987
|
}
|
|
355421
355988
|
if (proxySet || caInstalledOnDevice || hostProxySet) {
|
|
@@ -355504,9 +356071,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355504
356071
|
await proxy.close();
|
|
355505
356072
|
if (capturedTmpDir) {
|
|
355506
356073
|
try {
|
|
355507
|
-
await
|
|
356074
|
+
await rm2(capturedTmpDir, { recursive: true, force: true });
|
|
355508
356075
|
} catch (err) {
|
|
355509
|
-
onLog?.(`[mobile-net] temp CA cleanup failed: ${
|
|
356076
|
+
onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg2(err)}`);
|
|
355510
356077
|
}
|
|
355511
356078
|
}
|
|
355512
356079
|
const apiCount = finalCalls.filter((c) => c.isApi).length;
|
|
@@ -355539,11 +356106,11 @@ var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
|
355539
356106
|
})();
|
|
355540
356107
|
var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
355541
356108
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
355542
|
-
var WD_SESSION_CREATE_TIMEOUT_MS =
|
|
356109
|
+
var WD_SESSION_CREATE_TIMEOUT_MS = Number(process.env.VALIDATEQA_WD_SESSION_CREATE_TIMEOUT_MS) || 3e5;
|
|
355543
356110
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
355544
356111
|
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
355545
356112
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
355546
|
-
var
|
|
356113
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
355547
356114
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
355548
356115
|
var WebDriverTransportError = class extends Error {
|
|
355549
356116
|
isTransport = true;
|
|
@@ -355555,6 +356122,13 @@ var WebDriverTransportError = class extends Error {
|
|
|
355555
356122
|
function isTransportError(err) {
|
|
355556
356123
|
return err instanceof WebDriverTransportError || typeof err === "object" && err !== null && err.isTransport === true;
|
|
355557
356124
|
}
|
|
356125
|
+
var MobileAmbiguousMatchError = class extends Error {
|
|
356126
|
+
isAmbiguousMatch = true;
|
|
356127
|
+
constructor(message) {
|
|
356128
|
+
super(message);
|
|
356129
|
+
this.name = "MobileAmbiguousMatchError";
|
|
356130
|
+
}
|
|
356131
|
+
};
|
|
355558
356132
|
function crossPlatformRunReadinessError(steps, authoringPlatform, runPlatform) {
|
|
355559
356133
|
if (!authoringPlatform || authoringPlatform === runPlatform) return null;
|
|
355560
356134
|
const missing = stepsMissingPlatformLocators(steps, runPlatform);
|
|
@@ -355672,11 +356246,11 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
355672
356246
|
return { "appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200) };
|
|
355673
356247
|
}
|
|
355674
356248
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
355675
|
-
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() ||
|
|
356249
|
+
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join5(tmpdir3(), "validateqa-wda");
|
|
355676
356250
|
return {
|
|
355677
356251
|
"appium:wdaLocalPort": readPort("APPIUM_WDA_LOCAL_PORT", 8100),
|
|
355678
356252
|
"appium:mjpegServerPort": readPort("APPIUM_MJPEG_SERVER_PORT", 9100),
|
|
355679
|
-
"appium:derivedDataPath":
|
|
356253
|
+
"appium:derivedDataPath": join5(derivedDataRoot, safeDevice)
|
|
355680
356254
|
};
|
|
355681
356255
|
}
|
|
355682
356256
|
function isAndroidSystemPortBusyError(error2) {
|
|
@@ -355700,6 +356274,7 @@ function androidSystemPortForwardRemovalArgs(target, error2, env = process.env)
|
|
|
355700
356274
|
return ["-s", deviceId, "forward", "--remove", `tcp:${systemPort}`];
|
|
355701
356275
|
}
|
|
355702
356276
|
async function createSession(target) {
|
|
356277
|
+
assertDeviceNotQuarantined(target.deviceId);
|
|
355703
356278
|
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform) : {};
|
|
355704
356279
|
const parallelCaps = mobileParallelSessionCapabilities(
|
|
355705
356280
|
target.platform,
|
|
@@ -355721,6 +356296,13 @@ async function createSession(target) {
|
|
|
355721
356296
|
// whole survey. The runner owns session teardown explicitly, so a long
|
|
355722
356297
|
// idle window is safe. Env-overridable.
|
|
355723
356298
|
"appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
|
|
356299
|
+
// Xcode 27+ relocates Simulator.app out of <DEVELOPER_DIR>/Applications, so
|
|
356300
|
+
// XCUITest's default "open Simulator UI" step fails ("Simulator.app does not
|
|
356301
|
+
// exist") on this toolchain. The simulator is already booted and driven
|
|
356302
|
+
// headlessly via CoreSimulator, so tell XCUITest to skip opening the GUI.
|
|
356303
|
+
// Scoped to simulator UDIDs (a physical iOS device has no such step);
|
|
356304
|
+
// override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
|
|
356305
|
+
...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
355724
356306
|
...parallelCaps,
|
|
355725
356307
|
...realCaps
|
|
355726
356308
|
} : {
|
|
@@ -355760,7 +356342,7 @@ async function removeStaleAndroidSystemPortForward(target, error2) {
|
|
|
355760
356342
|
const args = androidSystemPortForwardRemovalArgs(target, error2);
|
|
355761
356343
|
if (!args) return false;
|
|
355762
356344
|
try {
|
|
355763
|
-
await
|
|
356345
|
+
await execFileAsync3("adb", args, { timeout: 1e4, windowsHide: true });
|
|
355764
356346
|
return true;
|
|
355765
356347
|
} catch {
|
|
355766
356348
|
return false;
|
|
@@ -355927,6 +356509,9 @@ async function deleteSession(sessionId) {
|
|
|
355927
356509
|
} catch {
|
|
355928
356510
|
}
|
|
355929
356511
|
}
|
|
356512
|
+
async function deleteAppiumSession(sessionId) {
|
|
356513
|
+
await deleteSession(sessionId);
|
|
356514
|
+
}
|
|
355930
356515
|
async function takeScreenshot(sessionId) {
|
|
355931
356516
|
try {
|
|
355932
356517
|
const result = await wdRequest(`/session/${sessionId}/screenshot`);
|
|
@@ -355998,10 +356583,9 @@ async function stopScreenRecording(sessionId, log2) {
|
|
|
355998
356583
|
return void 0;
|
|
355999
356584
|
}
|
|
356000
356585
|
}
|
|
356001
|
-
function
|
|
356002
|
-
|
|
356003
|
-
|
|
356004
|
-
const parsed = bounds;
|
|
356586
|
+
function parseMobileBoundsLike(value) {
|
|
356587
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.x === "number" && typeof value.y === "number" && typeof value.width === "number" && typeof value.height === "number") {
|
|
356588
|
+
const parsed = value;
|
|
356005
356589
|
if ([parsed.x, parsed.y, parsed.width, parsed.height].every(Number.isFinite)) {
|
|
356006
356590
|
return {
|
|
356007
356591
|
x: parsed.x,
|
|
@@ -356013,6 +356597,9 @@ function boundsFromStep(step) {
|
|
|
356013
356597
|
}
|
|
356014
356598
|
return null;
|
|
356015
356599
|
}
|
|
356600
|
+
function boundsFromStep(step) {
|
|
356601
|
+
return parseMobileBoundsLike(step.metadata?.bounds);
|
|
356602
|
+
}
|
|
356016
356603
|
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
356017
356604
|
if (isCrossPlatform) return null;
|
|
356018
356605
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
@@ -356164,6 +356751,131 @@ async function revealLikelyOffscreenElement(sessionId) {
|
|
|
356164
356751
|
});
|
|
356165
356752
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
356166
356753
|
}
|
|
356754
|
+
function strictMatchMode() {
|
|
356755
|
+
const raw = (process.env.VALIDATEQA_MOBILE_STRICT_MATCH ?? "").trim().toLowerCase();
|
|
356756
|
+
if (raw === "strict") return "strict";
|
|
356757
|
+
if (raw === "off") return "off";
|
|
356758
|
+
return "lenient";
|
|
356759
|
+
}
|
|
356760
|
+
var MOBILE_MAX_DISAMBIG_CANDIDATES = 20;
|
|
356761
|
+
var COINCIDENT_CENTER_EPSILON_PX = 2;
|
|
356762
|
+
function isRealElementRect(rect) {
|
|
356763
|
+
return rect.width > 1 && rect.height > 1;
|
|
356764
|
+
}
|
|
356765
|
+
function rectContainsPoint(rect, x, y) {
|
|
356766
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
356767
|
+
}
|
|
356768
|
+
function rectCenter(rect) {
|
|
356769
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
356770
|
+
}
|
|
356771
|
+
function centerDistance(rect, point) {
|
|
356772
|
+
const center = rectCenter(rect);
|
|
356773
|
+
return Math.hypot(center.x - point.x, center.y - point.y);
|
|
356774
|
+
}
|
|
356775
|
+
function chooseRecordedMatch(candidates, opts) {
|
|
356776
|
+
if (candidates.length === 0) return { kind: "ambiguous", count: 0 };
|
|
356777
|
+
if (candidates.length === 1) {
|
|
356778
|
+
return { kind: "matched", elementId: candidates[0].elementId, reason: "single" };
|
|
356779
|
+
}
|
|
356780
|
+
if (typeof opts.matchIndex === "number" && Number.isInteger(opts.matchIndex) && opts.matchIndex >= 0 && opts.matchIndex < candidates.length && typeof opts.expectedMatchCount === "number" && opts.expectedMatchCount === candidates.length) {
|
|
356781
|
+
return { kind: "matched", elementId: candidates[opts.matchIndex].elementId, reason: "matchIndex" };
|
|
356782
|
+
}
|
|
356783
|
+
{
|
|
356784
|
+
const withRect = candidates.filter(
|
|
356785
|
+
(candidate) => candidate.rect != null
|
|
356786
|
+
);
|
|
356787
|
+
if (withRect.length === candidates.length) {
|
|
356788
|
+
const real = withRect.filter((candidate) => isRealElementRect(candidate.rect));
|
|
356789
|
+
if (real.length === 1) {
|
|
356790
|
+
return { kind: "matched", elementId: real[0].elementId, reason: "visible" };
|
|
356791
|
+
}
|
|
356792
|
+
const centers = withRect.map((candidate) => rectCenter(candidate.rect));
|
|
356793
|
+
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);
|
|
356794
|
+
if (coincident) {
|
|
356795
|
+
return { kind: "matched", elementId: withRect[0].elementId, reason: "coincident" };
|
|
356796
|
+
}
|
|
356797
|
+
}
|
|
356798
|
+
}
|
|
356799
|
+
if (opts.recordedBounds) {
|
|
356800
|
+
const bounds = opts.recordedBounds;
|
|
356801
|
+
const center = rectCenter(bounds);
|
|
356802
|
+
const withRect = candidates.filter(
|
|
356803
|
+
(candidate) => candidate.rect != null
|
|
356804
|
+
);
|
|
356805
|
+
const containing = withRect.filter((candidate) => rectContainsPoint(candidate.rect, center.x, center.y));
|
|
356806
|
+
if (containing.length > 0) {
|
|
356807
|
+
const best = [...containing].sort(
|
|
356808
|
+
(a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height || centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356809
|
+
)[0];
|
|
356810
|
+
return { kind: "matched", elementId: best.elementId, reason: "bounds" };
|
|
356811
|
+
}
|
|
356812
|
+
if (withRect.length > 0) {
|
|
356813
|
+
const tolerance = Math.max(bounds.height, 1);
|
|
356814
|
+
const nearest = [...withRect].sort(
|
|
356815
|
+
(a, b) => centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356816
|
+
)[0];
|
|
356817
|
+
if (centerDistance(nearest.rect, center) <= tolerance) {
|
|
356818
|
+
return { kind: "matched", elementId: nearest.elementId, reason: "bounds" };
|
|
356819
|
+
}
|
|
356820
|
+
}
|
|
356821
|
+
}
|
|
356822
|
+
return opts.strict === "strict" ? { kind: "ambiguous", count: candidates.length } : { kind: "fallback", elementId: candidates[0].elementId, count: candidates.length };
|
|
356823
|
+
}
|
|
356824
|
+
function extractElementIdsFromPluralResponse(result) {
|
|
356825
|
+
const rawList = result?.value;
|
|
356826
|
+
if (!Array.isArray(rawList)) return [];
|
|
356827
|
+
const ids = [];
|
|
356828
|
+
for (const entry of rawList) {
|
|
356829
|
+
const record = entry;
|
|
356830
|
+
const id = validateElementId(record?.ELEMENT ?? record?.["element-6066-11e4-a52e-4f735466cecf"]);
|
|
356831
|
+
if (id) ids.push(id);
|
|
356832
|
+
}
|
|
356833
|
+
return ids;
|
|
356834
|
+
}
|
|
356835
|
+
async function findElements(sessionId, strategy) {
|
|
356836
|
+
const result = await wdRequest(`/session/${sessionId}/elements`, {
|
|
356837
|
+
method: "POST",
|
|
356838
|
+
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
356839
|
+
});
|
|
356840
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356841
|
+
}
|
|
356842
|
+
async function findWebViewElements(sessionPath, css) {
|
|
356843
|
+
const result = await wdRequest(`${sessionPath}/elements`, {
|
|
356844
|
+
method: "POST",
|
|
356845
|
+
body: JSON.stringify({ using: "css selector", value: css })
|
|
356846
|
+
});
|
|
356847
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356848
|
+
}
|
|
356849
|
+
async function readElementRect(sessionId, elementId) {
|
|
356850
|
+
try {
|
|
356851
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/rect`);
|
|
356852
|
+
return parseMobileBoundsLike(result?.value);
|
|
356853
|
+
} catch (err) {
|
|
356854
|
+
if (isTransportError(err)) throw err;
|
|
356855
|
+
return null;
|
|
356856
|
+
}
|
|
356857
|
+
}
|
|
356858
|
+
async function disambiguateElementIds(sessionId, candidateIds, opts) {
|
|
356859
|
+
const indexOnly = candidateIds.map((elementId) => ({ elementId }));
|
|
356860
|
+
const indexResult = chooseRecordedMatch(indexOnly, {
|
|
356861
|
+
recordedBounds: null,
|
|
356862
|
+
matchIndex: opts.matchIndex,
|
|
356863
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356864
|
+
strict: opts.strict
|
|
356865
|
+
});
|
|
356866
|
+
if (indexResult.kind === "matched" || candidateIds.length > MOBILE_MAX_DISAMBIG_CANDIDATES) {
|
|
356867
|
+
return indexResult;
|
|
356868
|
+
}
|
|
356869
|
+
const withRects = await Promise.all(
|
|
356870
|
+
candidateIds.map(async (elementId) => ({ elementId, rect: await readElementRect(sessionId, elementId) }))
|
|
356871
|
+
);
|
|
356872
|
+
return chooseRecordedMatch(withRects, {
|
|
356873
|
+
recordedBounds: opts.recordedBounds ?? null,
|
|
356874
|
+
matchIndex: opts.matchIndex,
|
|
356875
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356876
|
+
strict: opts.strict
|
|
356877
|
+
});
|
|
356878
|
+
}
|
|
356167
356879
|
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
356168
356880
|
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
356169
356881
|
if (strategies.length === 0) {
|
|
@@ -356171,22 +356883,45 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356171
356883
|
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"
|
|
356172
356884
|
);
|
|
356173
356885
|
}
|
|
356886
|
+
const mode = strictMatchMode();
|
|
356887
|
+
const matchIndex = typeof target?.matchIndex === "number" ? target.matchIndex : void 0;
|
|
356888
|
+
const expectedMatchCount = typeof target?.expectedMatchCount === "number" ? target.expectedMatchCount : void 0;
|
|
356174
356889
|
const startTime = Date.now();
|
|
356175
356890
|
let lastError = "";
|
|
356891
|
+
let lastAmbiguity = null;
|
|
356176
356892
|
let revealAttempted = false;
|
|
356177
356893
|
while (Date.now() - startTime < timeoutMs) {
|
|
356178
356894
|
for (const strategy of strategies) {
|
|
356179
356895
|
try {
|
|
356180
|
-
const
|
|
356181
|
-
|
|
356182
|
-
|
|
356896
|
+
const ids = await findElements(sessionId, strategy);
|
|
356897
|
+
if (ids.length === 0) continue;
|
|
356898
|
+
if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
356899
|
+
return ids[0];
|
|
356900
|
+
}
|
|
356901
|
+
const result = await disambiguateElementIds(sessionId, ids, {
|
|
356902
|
+
recordedBounds: opts?.recordedBounds,
|
|
356903
|
+
matchIndex,
|
|
356904
|
+
expectedMatchCount,
|
|
356905
|
+
strict: mode
|
|
356183
356906
|
});
|
|
356184
|
-
|
|
356185
|
-
|
|
356907
|
+
if (result.kind === "matched") return result.elementId;
|
|
356908
|
+
const shouldFailClosed = opts?.failClosedOnAmbiguity || result.kind === "ambiguous" && !opts?.tolerateAmbiguity;
|
|
356909
|
+
if (shouldFailClosed) {
|
|
356910
|
+
throw new MobileAmbiguousMatchError(
|
|
356911
|
+
`ambiguous locator: ${result.count} matches, cannot identify the recorded instance (strategy=${strategy.using} value=${strategy.value})`
|
|
356912
|
+
);
|
|
356913
|
+
}
|
|
356914
|
+
opts?.log?.(
|
|
356915
|
+
`\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.`
|
|
356186
356916
|
);
|
|
356187
|
-
|
|
356917
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
356188
356918
|
} catch (err) {
|
|
356189
356919
|
if (isTransportError(err)) throw err;
|
|
356920
|
+
if (err instanceof MobileAmbiguousMatchError) {
|
|
356921
|
+
lastAmbiguity = err;
|
|
356922
|
+
lastError = err.message;
|
|
356923
|
+
continue;
|
|
356924
|
+
}
|
|
356190
356925
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356191
356926
|
}
|
|
356192
356927
|
}
|
|
@@ -356201,6 +356936,7 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356201
356936
|
}
|
|
356202
356937
|
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
356203
356938
|
}
|
|
356939
|
+
if (lastAmbiguity) throw lastAmbiguity;
|
|
356204
356940
|
throw new Error(`Element not found after ${timeoutMs}ms. Tried ${strategies.length} strategies${revealAttempted ? " plus one reveal scroll" : ""}. Last error: ${lastError}`);
|
|
356205
356941
|
}
|
|
356206
356942
|
async function tapCoordinates(sessionId, bounds) {
|
|
@@ -356234,7 +356970,7 @@ async function prepareAndroidInputEnvironment(deviceId) {
|
|
|
356234
356970
|
];
|
|
356235
356971
|
for (const [namespace, key, value] of settingsToDisable) {
|
|
356236
356972
|
try {
|
|
356237
|
-
await
|
|
356973
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
356238
356974
|
timeout: 8e3,
|
|
356239
356975
|
windowsHide: true
|
|
356240
356976
|
});
|
|
@@ -356246,14 +356982,14 @@ async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
|
356246
356982
|
if (!deviceId) {
|
|
356247
356983
|
throw new Error("Android keyboard fallback requires a device id");
|
|
356248
356984
|
}
|
|
356249
|
-
await
|
|
356985
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
|
|
356250
356986
|
timeout: 1e4,
|
|
356251
356987
|
windowsHide: true
|
|
356252
356988
|
});
|
|
356253
356989
|
}
|
|
356254
356990
|
async function pressAndroidKeyboardEnter(deviceId) {
|
|
356255
356991
|
if (!deviceId) throw new Error("Android keyboard Enter fallback requires a device id");
|
|
356256
|
-
await
|
|
356992
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
|
|
356257
356993
|
timeout: 1e4,
|
|
356258
356994
|
windowsHide: true
|
|
356259
356995
|
});
|
|
@@ -356341,21 +357077,40 @@ async function executeInWebViewContext(sessionId, step, css, body) {
|
|
|
356341
357077
|
const cleanCss = css.startsWith("css=") ? css.slice(4) : css;
|
|
356342
357078
|
await withWebViewContext(sessionPath, webview, () => body(sessionPath, cleanCss));
|
|
356343
357079
|
}
|
|
356344
|
-
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS) {
|
|
357080
|
+
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
357081
|
+
const mode = strictMatchMode();
|
|
356345
357082
|
const startedAt = Date.now();
|
|
356346
357083
|
let lastError = "";
|
|
356347
357084
|
while (Date.now() - startedAt < timeoutMs) {
|
|
356348
357085
|
try {
|
|
356349
|
-
const
|
|
356350
|
-
|
|
356351
|
-
|
|
356352
|
-
})
|
|
356353
|
-
|
|
356354
|
-
|
|
356355
|
-
|
|
356356
|
-
|
|
357086
|
+
const ids = await findWebViewElements(sessionPath, css);
|
|
357087
|
+
if (ids.length === 0) {
|
|
357088
|
+
lastError = `no match for css=${css}`;
|
|
357089
|
+
} else if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
357090
|
+
return ids[0];
|
|
357091
|
+
} else {
|
|
357092
|
+
const candidates = ids.map((elementId) => ({ elementId }));
|
|
357093
|
+
const result = chooseRecordedMatch(candidates, {
|
|
357094
|
+
recordedBounds: null,
|
|
357095
|
+
matchIndex: opts?.matchIndex,
|
|
357096
|
+
expectedMatchCount: opts?.expectedMatchCount,
|
|
357097
|
+
strict: mode
|
|
357098
|
+
});
|
|
357099
|
+
if (result.kind === "matched") {
|
|
357100
|
+
return result.elementId;
|
|
357101
|
+
}
|
|
357102
|
+
if (result.kind === "ambiguous" && !opts?.tolerateAmbiguity) {
|
|
357103
|
+
throw new MobileAmbiguousMatchError(
|
|
357104
|
+
`ambiguous WEBVIEW locator: ${result.count} matches for css=${css}, cannot identify the recorded instance`
|
|
357105
|
+
);
|
|
357106
|
+
}
|
|
357107
|
+
opts?.log?.(
|
|
357108
|
+
`\u26A0 ambiguous WEBVIEW locator (${result.count} matches for css=${css}) \u2014 using the first match. Set VALIDATEQA_MOBILE_STRICT_MATCH=strict to fail instead.`
|
|
357109
|
+
);
|
|
357110
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
357111
|
+
}
|
|
356357
357112
|
} catch (err) {
|
|
356358
|
-
if (isTransportError(err)) throw err;
|
|
357113
|
+
if (isTransportError(err) || err instanceof MobileAmbiguousMatchError) throw err;
|
|
356359
357114
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356360
357115
|
}
|
|
356361
357116
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
@@ -356364,21 +357119,42 @@ async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIM
|
|
|
356364
357119
|
`WEBVIEW element not found for css=${css} after ${timeoutMs}ms` + (lastError ? `. Last error: ${lastError}` : "")
|
|
356365
357120
|
);
|
|
356366
357121
|
}
|
|
357122
|
+
function isWebViewHydrationTransient(err) {
|
|
357123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357124
|
+
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);
|
|
357125
|
+
}
|
|
357126
|
+
var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
|
|
357127
|
+
async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
|
|
357128
|
+
const deadline = Date.now() + Math.max(1e3, timeoutMs);
|
|
357129
|
+
for (; ; ) {
|
|
357130
|
+
try {
|
|
357131
|
+
await executeInWebViewContext(sessionId, step, css, body);
|
|
357132
|
+
return;
|
|
357133
|
+
} catch (err) {
|
|
357134
|
+
if (!isWebViewHydrationTransient(err) || Date.now() >= deadline) throw err;
|
|
357135
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
357136
|
+
}
|
|
357137
|
+
}
|
|
357138
|
+
}
|
|
356367
357139
|
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356368
357140
|
const webCss = stepWebViewCss(step);
|
|
356369
357141
|
if (webCss) {
|
|
356370
|
-
await
|
|
357142
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356371
357143
|
sessionId,
|
|
356372
357144
|
step,
|
|
356373
357145
|
webCss,
|
|
356374
|
-
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css)
|
|
357146
|
+
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css),
|
|
357147
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356375
357148
|
);
|
|
356376
357149
|
return;
|
|
356377
357150
|
}
|
|
356378
357151
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356379
357152
|
if (step.target) {
|
|
356380
357153
|
try {
|
|
356381
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357154
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357155
|
+
platform: platform3,
|
|
357156
|
+
recordedBounds: bounds
|
|
357157
|
+
});
|
|
356382
357158
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356383
357159
|
method: "POST",
|
|
356384
357160
|
body: "{}"
|
|
@@ -356400,11 +357176,12 @@ async function executeType(sessionId, step, context) {
|
|
|
356400
357176
|
const keysValue = keyboardSubmitOnly ? WEBDRIVER_ENTER_KEY : value;
|
|
356401
357177
|
const webCss = stepWebViewCss(step);
|
|
356402
357178
|
if (webCss) {
|
|
356403
|
-
await
|
|
357179
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356404
357180
|
sessionId,
|
|
356405
357181
|
step,
|
|
356406
357182
|
webCss,
|
|
356407
|
-
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value)
|
|
357183
|
+
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value),
|
|
357184
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356408
357185
|
);
|
|
356409
357186
|
return;
|
|
356410
357187
|
}
|
|
@@ -356415,7 +357192,9 @@ async function executeType(sessionId, step, context) {
|
|
|
356415
357192
|
const locatorStrategies = buildLocatorStrategies(step.target, context.platform);
|
|
356416
357193
|
if (step.target && locatorStrategies.length > 0) {
|
|
356417
357194
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
356418
|
-
platform: context.platform
|
|
357195
|
+
platform: context.platform,
|
|
357196
|
+
recordedBounds: bounds,
|
|
357197
|
+
log: context.log
|
|
356419
357198
|
});
|
|
356420
357199
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356421
357200
|
method: "POST",
|
|
@@ -356456,22 +357235,28 @@ async function executeType(sessionId, step, context) {
|
|
|
356456
357235
|
}
|
|
356457
357236
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356458
357237
|
const webCss = stepWebViewCss(step);
|
|
357238
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356459
357239
|
if (webCss) {
|
|
356460
357240
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356461
|
-
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357241
|
+
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357242
|
+
matchIndex: step.target?.matchIndex,
|
|
357243
|
+
expectedMatchCount: step.target?.expectedMatchCount
|
|
357244
|
+
});
|
|
356462
357245
|
await wdRequest(`${sessionPath}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356463
357246
|
});
|
|
356464
357247
|
return;
|
|
356465
357248
|
}
|
|
356466
357249
|
const locatorStrategies = buildLocatorStrategies(step.target, platform3);
|
|
356467
357250
|
if (step.target && locatorStrategies.length > 0) {
|
|
356468
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357251
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357252
|
+
platform: platform3,
|
|
357253
|
+
recordedBounds: bounds
|
|
357254
|
+
});
|
|
356469
357255
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
356470
357256
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
356471
357257
|
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356472
357258
|
return;
|
|
356473
357259
|
}
|
|
356474
|
-
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356475
357260
|
await clearField(`/session/${sessionId}`, bounds ?? void 0);
|
|
356476
357261
|
}
|
|
356477
357262
|
async function executeSwipe(sessionId, step) {
|
|
@@ -356532,34 +357317,47 @@ async function executeWaitForElement(sessionId, step, platform3) {
|
|
|
356532
357317
|
sessionId,
|
|
356533
357318
|
step,
|
|
356534
357319
|
webCss,
|
|
356535
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout).then(() => void 0)
|
|
357320
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout, { disambiguate: false }).then(() => void 0)
|
|
356536
357321
|
);
|
|
356537
357322
|
return;
|
|
356538
357323
|
}
|
|
356539
|
-
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
357324
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3, disambiguate: false });
|
|
356540
357325
|
}
|
|
356541
|
-
async function executeAssertVisible(sessionId, step, platform3) {
|
|
357326
|
+
async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356542
357327
|
const webCss = stepWebViewCss(step);
|
|
356543
357328
|
if (webCss) {
|
|
356544
357329
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356545
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357330
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357331
|
+
matchIndex: step.target?.matchIndex,
|
|
357332
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357333
|
+
tolerateAmbiguity: true
|
|
357334
|
+
});
|
|
356546
357335
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/displayed`);
|
|
356547
357336
|
if (!result2?.value) throw new Error(`WEBVIEW element found but not visible: css=${css}`);
|
|
356548
357337
|
});
|
|
356549
357338
|
return;
|
|
356550
357339
|
}
|
|
356551
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357340
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357341
|
+
identityOnly: true,
|
|
357342
|
+
platform: platform3,
|
|
357343
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357344
|
+
tolerateAmbiguity: true
|
|
357345
|
+
});
|
|
356552
357346
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
356553
357347
|
if (!result?.value) {
|
|
356554
357348
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
356555
357349
|
}
|
|
356556
357350
|
}
|
|
356557
|
-
async function executeAssertText(sessionId, step, platform3) {
|
|
357351
|
+
async function executeAssertText(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356558
357352
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
356559
357353
|
const webCss = stepWebViewCss(step);
|
|
356560
357354
|
if (webCss) {
|
|
356561
357355
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356562
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357356
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357357
|
+
matchIndex: step.target?.matchIndex,
|
|
357358
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357359
|
+
tolerateAmbiguity: true
|
|
357360
|
+
});
|
|
356563
357361
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/text`);
|
|
356564
357362
|
const actualText2 = typeof result2?.value === "string" ? result2.value : "";
|
|
356565
357363
|
const textCandidates2 = await readElementTextCandidates(sessionId, elementId2, actualText2);
|
|
@@ -356571,7 +357369,12 @@ async function executeAssertText(sessionId, step, platform3) {
|
|
|
356571
357369
|
});
|
|
356572
357370
|
return;
|
|
356573
357371
|
}
|
|
356574
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357372
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357373
|
+
identityOnly: true,
|
|
357374
|
+
platform: platform3,
|
|
357375
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357376
|
+
tolerateAmbiguity: true
|
|
357377
|
+
});
|
|
356575
357378
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
356576
357379
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
356577
357380
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -356611,6 +357414,8 @@ function acquireControlPortSlot(deviceId) {
|
|
|
356611
357414
|
function releaseControlPortSlot(deviceId) {
|
|
356612
357415
|
controlPortSlotsByDevice.delete(deviceId);
|
|
356613
357416
|
}
|
|
357417
|
+
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357418
|
+
var pinnedDeviceSnapshot = null;
|
|
356614
357419
|
async function executeMobileTest(options) {
|
|
356615
357420
|
const logLines = [];
|
|
356616
357421
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -356618,6 +357423,7 @@ async function executeMobileTest(options) {
|
|
|
356618
357423
|
const { steps: runSteps, missingKeys: missingVariableKeys } = resolveMobileStepValuePlaceholders(options.steps, options.testVariables);
|
|
356619
357424
|
const runPlatform = options.target.platform;
|
|
356620
357425
|
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
357426
|
+
const captureStepScreenshots = options.captureStepScreenshots !== false;
|
|
356621
357427
|
if (missingVariableKeys.length > 0) {
|
|
356622
357428
|
log2(`\u26A0 Unresolved step-value placeholder(s): ${missingVariableKeys.map((k) => `{{${k}}}`).join(", ")} \u2014 no matching test variable configured for this project/run.`);
|
|
356623
357429
|
}
|
|
@@ -356682,32 +357488,42 @@ async function executeMobileTest(options) {
|
|
|
356682
357488
|
await startAppiumServer();
|
|
356683
357489
|
log2("Appium server started");
|
|
356684
357490
|
}
|
|
356685
|
-
log2("Discovering devices...");
|
|
356686
|
-
const devices = discoverAllDevices({ includeApps: true });
|
|
356687
357491
|
const platformFilter = options.target.platform;
|
|
356688
|
-
const
|
|
356689
|
-
|
|
356690
|
-
);
|
|
356691
|
-
if (
|
|
356692
|
-
|
|
356693
|
-
|
|
357492
|
+
const pinnedDeviceId = options.target.assignedDeviceId;
|
|
357493
|
+
const candidateSnapshot = pinnedDeviceSnapshot;
|
|
357494
|
+
const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && candidateSnapshot.deviceId === pinnedDeviceId && Date.now() - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
|
|
357495
|
+
if (reusablePinnedSnapshot) {
|
|
357496
|
+
assertDeviceNotQuarantined(reusablePinnedSnapshot.device.id);
|
|
357497
|
+
selectedDevice = reusablePinnedSnapshot.device;
|
|
357498
|
+
log2(`Reusing known device (skipped device re-scan): ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357499
|
+
} else {
|
|
357500
|
+
log2("Discovering devices...");
|
|
357501
|
+
const devices = discoverAllDevices({ includeApps: true });
|
|
357502
|
+
const matchingDevices = devices.filter(
|
|
357503
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
356694
357504
|
);
|
|
356695
|
-
|
|
356696
|
-
if (options.target.assignedDeviceId) {
|
|
356697
|
-
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
356698
|
-
if (!selectedDevice) {
|
|
357505
|
+
if (matchingDevices.length === 0) {
|
|
356699
357506
|
throw new Error(
|
|
356700
|
-
`
|
|
357507
|
+
`No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
|
|
356701
357508
|
);
|
|
356702
357509
|
}
|
|
357510
|
+
if (options.target.assignedDeviceId) {
|
|
357511
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
357512
|
+
if (!selectedDevice) {
|
|
357513
|
+
throw new Error(
|
|
357514
|
+
`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.`
|
|
357515
|
+
);
|
|
357516
|
+
}
|
|
357517
|
+
}
|
|
357518
|
+
if (!selectedDevice && options.target.recordedDeviceId) {
|
|
357519
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
|
|
357520
|
+
}
|
|
357521
|
+
if (!selectedDevice) {
|
|
357522
|
+
selectedDevice = matchingDevices[0];
|
|
357523
|
+
}
|
|
357524
|
+
log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357525
|
+
pinnedDeviceSnapshot = options.target.assignedDeviceId ? { deviceId: options.target.assignedDeviceId, device: selectedDevice, at: Date.now() } : null;
|
|
356703
357526
|
}
|
|
356704
|
-
if (!selectedDevice && options.target.recordedDeviceId) {
|
|
356705
|
-
selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
|
|
356706
|
-
}
|
|
356707
|
-
if (!selectedDevice) {
|
|
356708
|
-
selectedDevice = matchingDevices[0];
|
|
356709
|
-
}
|
|
356710
|
-
log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
|
|
356711
357527
|
try {
|
|
356712
357528
|
capture = await startMobileNetworkCapture({
|
|
356713
357529
|
platform: options.target.platform,
|
|
@@ -356774,7 +357590,14 @@ async function executeMobileTest(options) {
|
|
|
356774
357590
|
const resetAllowed = shouldResetMobileAppData(options.target.platform, selectedDevice.id);
|
|
356775
357591
|
if (resetAllowed) {
|
|
356776
357592
|
try {
|
|
356777
|
-
|
|
357593
|
+
if (options.target.platform === "IOS") {
|
|
357594
|
+
await terminateTargetApp(sessionId, "IOS", options.target.appId).catch(() => {
|
|
357595
|
+
});
|
|
357596
|
+
const wiped = await clearIosSimulatorAppData(selectedDevice.id, options.target.appId);
|
|
357597
|
+
if (!wiped.ok) throw new Error(wiped.detail);
|
|
357598
|
+
} else {
|
|
357599
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
357600
|
+
}
|
|
356778
357601
|
clearedAppDataBeforeLaunch = true;
|
|
356779
357602
|
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).");
|
|
356780
357603
|
} catch (err) {
|
|
@@ -356827,17 +357650,21 @@ async function executeMobileTest(options) {
|
|
|
356827
357650
|
const entryStep = selectEntryDriftGateStep(runSteps, runPlatform);
|
|
356828
357651
|
if (entryStep) {
|
|
356829
357652
|
try {
|
|
356830
|
-
const entryTimeout =
|
|
357653
|
+
const entryTimeout = Math.max(DEFAULT_STEP_TIMEOUT_MS, COLD_BOOT_ENTRY_TIMEOUT_MS);
|
|
356831
357654
|
const entryWebCss = stepWebViewCss(entryStep);
|
|
356832
357655
|
if (entryWebCss) {
|
|
356833
357656
|
await executeInWebViewContext(
|
|
356834
357657
|
sessionId,
|
|
356835
357658
|
entryStep,
|
|
356836
357659
|
entryWebCss,
|
|
356837
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout).then(() => void 0)
|
|
357660
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout, { disambiguate: false }).then(() => void 0)
|
|
356838
357661
|
);
|
|
356839
357662
|
} else {
|
|
356840
|
-
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357663
|
+
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357664
|
+
identityOnly: true,
|
|
357665
|
+
platform: runPlatform,
|
|
357666
|
+
disambiguate: false
|
|
357667
|
+
});
|
|
356841
357668
|
}
|
|
356842
357669
|
} catch (err) {
|
|
356843
357670
|
if (isTransportError(err)) throw err;
|
|
@@ -356897,10 +357724,10 @@ async function executeMobileTest(options) {
|
|
|
356897
357724
|
await executeWaitForElement(sessionId, step, runPlatform);
|
|
356898
357725
|
break;
|
|
356899
357726
|
case "assertVisible":
|
|
356900
|
-
await executeAssertVisible(sessionId, step, runPlatform);
|
|
357727
|
+
await executeAssertVisible(sessionId, step, runPlatform, isCrossPlatform);
|
|
356901
357728
|
break;
|
|
356902
357729
|
case "assertText":
|
|
356903
|
-
await executeAssertText(sessionId, step, runPlatform);
|
|
357730
|
+
await executeAssertText(sessionId, step, runPlatform, isCrossPlatform);
|
|
356904
357731
|
break;
|
|
356905
357732
|
default: {
|
|
356906
357733
|
const _exhaustive = step.action;
|
|
@@ -356919,7 +357746,7 @@ async function executeMobileTest(options) {
|
|
|
356919
357746
|
environmental = stepError;
|
|
356920
357747
|
}
|
|
356921
357748
|
}
|
|
356922
|
-
const screenshot = await takeScreenshot(sessionId).catch(() => null);
|
|
357749
|
+
const screenshot = captureStepScreenshots ? await takeScreenshot(sessionId).catch(() => null) : null;
|
|
356923
357750
|
screenshots.push(screenshot ?? "");
|
|
356924
357751
|
if (status === "failed") {
|
|
356925
357752
|
await recordFirstFailingSource();
|
|
@@ -357181,10 +358008,10 @@ async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
|
357181
358008
|
await executeWaitForElement(sessionId, step, platform3);
|
|
357182
358009
|
return true;
|
|
357183
358010
|
case "assertVisible":
|
|
357184
|
-
await executeAssertVisible(sessionId, step, platform3);
|
|
358011
|
+
await executeAssertVisible(sessionId, step, platform3, true);
|
|
357185
358012
|
return true;
|
|
357186
358013
|
case "assertText":
|
|
357187
|
-
await executeAssertText(sessionId, step, platform3);
|
|
358014
|
+
await executeAssertText(sessionId, step, platform3, true);
|
|
357188
358015
|
return true;
|
|
357189
358016
|
default:
|
|
357190
358017
|
break;
|
|
@@ -357266,7 +358093,15 @@ async function harvestPlatformLocators(options) {
|
|
|
357266
358093
|
const identityStrategies = webViewCss ? [] : buildIdentityLocatorStrategies(step.target);
|
|
357267
358094
|
if (!webViewCss && identityStrategies.length > 0 && step.target) {
|
|
357268
358095
|
try {
|
|
357269
|
-
elementId = await findElement(
|
|
358096
|
+
elementId = await findElement(
|
|
358097
|
+
session.sessionId,
|
|
358098
|
+
step.target,
|
|
358099
|
+
Math.max(DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS),
|
|
358100
|
+
{
|
|
358101
|
+
identityOnly: true,
|
|
358102
|
+
failClosedOnAmbiguity: true
|
|
358103
|
+
}
|
|
358104
|
+
);
|
|
357270
358105
|
} catch (err) {
|
|
357271
358106
|
if (isTransportError(err)) throw err;
|
|
357272
358107
|
elementId = null;
|
|
@@ -357362,6 +358197,86 @@ function validateRunnerMobileTarget(value) {
|
|
|
357362
358197
|
};
|
|
357363
358198
|
}
|
|
357364
358199
|
|
|
358200
|
+
// src/services/heartbeat-keepalive.ts
|
|
358201
|
+
import { Worker } from "worker_threads";
|
|
358202
|
+
var KEEPALIVE_INTERVAL_MS = (() => {
|
|
358203
|
+
const raw = Number(process.env.VALIDATEQA_HEARTBEAT_KEEPALIVE_MS);
|
|
358204
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2e4;
|
|
358205
|
+
})();
|
|
358206
|
+
function isHeartbeatKeepaliveEnabled(env = process.env) {
|
|
358207
|
+
return (env.VALIDATEQA_HEARTBEAT_KEEPALIVE ?? "").trim().toLowerCase() !== "off";
|
|
358208
|
+
}
|
|
358209
|
+
var WORKER_SOURCE = `
|
|
358210
|
+
const { parentPort } = require('node:worker_threads');
|
|
358211
|
+
let url = null;
|
|
358212
|
+
let headers = null;
|
|
358213
|
+
let body = null;
|
|
358214
|
+
let intervalMs = 20000;
|
|
358215
|
+
let timer = null;
|
|
358216
|
+
|
|
358217
|
+
async function post() {
|
|
358218
|
+
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358219
|
+
try {
|
|
358220
|
+
await fetch(url, {
|
|
358221
|
+
method: 'POST',
|
|
358222
|
+
headers,
|
|
358223
|
+
body,
|
|
358224
|
+
signal: AbortSignal.timeout(10000),
|
|
358225
|
+
});
|
|
358226
|
+
} catch {
|
|
358227
|
+
// Best-effort by design: the next tick retries; the main loop's own
|
|
358228
|
+
// heartbeat error handling owns logging/upgrade signals.
|
|
358229
|
+
}
|
|
358230
|
+
}
|
|
358231
|
+
|
|
358232
|
+
parentPort.on('message', (msg) => {
|
|
358233
|
+
if (!msg || typeof msg !== 'object') return;
|
|
358234
|
+
if (msg.type === 'update') {
|
|
358235
|
+
url = msg.url;
|
|
358236
|
+
headers = msg.headers;
|
|
358237
|
+
body = msg.body;
|
|
358238
|
+
intervalMs = msg.intervalMs || intervalMs;
|
|
358239
|
+
if (!timer) {
|
|
358240
|
+
timer = setInterval(post, intervalMs);
|
|
358241
|
+
if (timer.unref) timer.unref();
|
|
358242
|
+
}
|
|
358243
|
+
} else if (msg.type === 'stop') {
|
|
358244
|
+
if (timer) clearInterval(timer);
|
|
358245
|
+
timer = null;
|
|
358246
|
+
parentPort.close();
|
|
358247
|
+
}
|
|
358248
|
+
});
|
|
358249
|
+
`;
|
|
358250
|
+
var NOOP_KEEPALIVE = { update: () => {
|
|
358251
|
+
}, stop: () => {
|
|
358252
|
+
} };
|
|
358253
|
+
function startHeartbeatKeepalive(options = {}) {
|
|
358254
|
+
if (!isHeartbeatKeepaliveEnabled(options.env)) return NOOP_KEEPALIVE;
|
|
358255
|
+
const intervalMs = options.intervalMs ?? KEEPALIVE_INTERVAL_MS;
|
|
358256
|
+
let worker;
|
|
358257
|
+
try {
|
|
358258
|
+
worker = new Worker(WORKER_SOURCE, { eval: true });
|
|
358259
|
+
worker.unref();
|
|
358260
|
+
worker.on("error", () => {
|
|
358261
|
+
worker = null;
|
|
358262
|
+
});
|
|
358263
|
+
} catch {
|
|
358264
|
+
return NOOP_KEEPALIVE;
|
|
358265
|
+
}
|
|
358266
|
+
return {
|
|
358267
|
+
update(url, headers, body) {
|
|
358268
|
+
worker?.postMessage({ type: "update", url, headers, body, intervalMs });
|
|
358269
|
+
},
|
|
358270
|
+
stop() {
|
|
358271
|
+
try {
|
|
358272
|
+
worker?.postMessage({ type: "stop" });
|
|
358273
|
+
} catch {
|
|
358274
|
+
}
|
|
358275
|
+
worker = null;
|
|
358276
|
+
}
|
|
358277
|
+
};
|
|
358278
|
+
}
|
|
358279
|
+
|
|
357365
358280
|
// src/mobile-device-queue.ts
|
|
357366
358281
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
357367
358282
|
if (!ctx) return false;
|
|
@@ -357435,7 +358350,7 @@ import { createServer } from "http";
|
|
|
357435
358350
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
357436
358351
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
357437
358352
|
import { homedir as homedir2 } from "os";
|
|
357438
|
-
import { join as
|
|
358353
|
+
import { join as join6, dirname } from "path";
|
|
357439
358354
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
357440
358355
|
init_dist();
|
|
357441
358356
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
@@ -357542,7 +358457,7 @@ async function getWindowSize3() {
|
|
|
357542
358457
|
}
|
|
357543
358458
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
357544
358459
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
357545
|
-
var BRIDGE_PID_FILE =
|
|
358460
|
+
var BRIDGE_PID_FILE = join6(homedir2(), ".validate.qa", "mobile-bridge.pid");
|
|
357546
358461
|
function readBridgePidFile() {
|
|
357547
358462
|
try {
|
|
357548
358463
|
const raw = readFileSync2(BRIDGE_PID_FILE, "utf-8").trim();
|
|
@@ -357854,6 +358769,13 @@ async function startSession(validated, res) {
|
|
|
357854
358769
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
357855
358770
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
357856
358771
|
"appium:noReset": true,
|
|
358772
|
+
// Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
|
|
358773
|
+
// Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
|
|
358774
|
+
// "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
|
|
358775
|
+
// skip the GUI open. Scoped to simulator UDIDs; APPIUM_IOS_HEADLESS=false
|
|
358776
|
+
// overrides. Verified live: without this, recorder/mirror sessions on iOS
|
|
358777
|
+
// die with "Simulator.app does not exist" while executor sessions work.
|
|
358778
|
+
...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
357857
358779
|
...realCaps
|
|
357858
358780
|
} : {
|
|
357859
358781
|
platformName: "Android",
|
|
@@ -357872,7 +358794,12 @@ async function startSession(validated, res) {
|
|
|
357872
358794
|
capabilities: {
|
|
357873
358795
|
alwaysMatch: capabilities
|
|
357874
358796
|
}
|
|
357875
|
-
})
|
|
358797
|
+
}),
|
|
358798
|
+
// Session creation is the one legitimately slow WebDriver call (iOS WDA
|
|
358799
|
+
// build/launch can take minutes on a cold toolchain) — same ceiling as
|
|
358800
|
+
// the executor's wdRequest session-create path. Without this, the shared
|
|
358801
|
+
// appiumRequest 10s default aborts every iOS recorder session.
|
|
358802
|
+
signal: AbortSignal.timeout(WD_SESSION_CREATE_TIMEOUT_MS)
|
|
357876
358803
|
});
|
|
357877
358804
|
state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
|
|
357878
358805
|
if (!state.appiumSessionId) {
|
|
@@ -358650,6 +359577,9 @@ function detachMirrorSession(deviceId) {
|
|
|
358650
359577
|
stopAllMirrorClients();
|
|
358651
359578
|
stopSessionReaper();
|
|
358652
359579
|
}
|
|
359580
|
+
function getMirrorSessionId(deviceId) {
|
|
359581
|
+
return runnerChannels.get(deviceId)?.sessionId;
|
|
359582
|
+
}
|
|
358653
359583
|
|
|
358654
359584
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
358655
359585
|
var import_runner_core9 = __toESM(require_dist2());
|
|
@@ -358680,6 +359610,7 @@ var MobileBridgeDriver = class {
|
|
|
358680
359610
|
sessionId;
|
|
358681
359611
|
platform;
|
|
358682
359612
|
appId;
|
|
359613
|
+
deviceId;
|
|
358683
359614
|
allowAppReset;
|
|
358684
359615
|
recoverSession;
|
|
358685
359616
|
maxSessionRecoveries;
|
|
@@ -358700,6 +359631,7 @@ var MobileBridgeDriver = class {
|
|
|
358700
359631
|
this.sessionId = config.sessionId;
|
|
358701
359632
|
this.platform = config.platform;
|
|
358702
359633
|
this.appId = config.appId;
|
|
359634
|
+
this.deviceId = config.deviceId;
|
|
358703
359635
|
this.allowAppReset = config.allowAppReset === true;
|
|
358704
359636
|
this.recoverSession = config.recoverSession;
|
|
358705
359637
|
this.maxSessionRecoveries = Math.max(0, Math.floor(config.maxSessionRecoveries ?? 1));
|
|
@@ -358997,17 +359929,34 @@ var MobileBridgeDriver = class {
|
|
|
358997
359929
|
});
|
|
358998
359930
|
}
|
|
358999
359931
|
/**
|
|
359000
|
-
* Deterministic state reset: clear app data + relaunch
|
|
359001
|
-
*
|
|
359002
|
-
*
|
|
359003
|
-
*
|
|
359004
|
-
*
|
|
359932
|
+
* Deterministic state reset: clear app data + relaunch, per the
|
|
359933
|
+
* runner-computed allowAppReset policy (Android emulators / iOS simulators
|
|
359934
|
+
* only — a personal physical device is never wiped). Android clears via
|
|
359935
|
+
* `mobile: clearApp`; iOS terminates the app and wipes its simulator data
|
|
359936
|
+
* container + keychain through `simctl` (see ios-simulator-reset.ts).
|
|
359937
|
+
* Returns false when unsupported/disallowed or when the clear fails —
|
|
359938
|
+
* callers fall back to relaunch(), preserving today's behavior. On success,
|
|
359939
|
+
* waits for cold-boot content so recording and replay agree on the true
|
|
359940
|
+
* entry screen.
|
|
359005
359941
|
*/
|
|
359006
359942
|
async resetAppState() {
|
|
359007
|
-
if (!this.allowAppReset
|
|
359943
|
+
if (!this.allowAppReset) return false;
|
|
359944
|
+
if (this.platform === "IOS" && !isIosSimulatorUdid(this.deviceId)) return false;
|
|
359008
359945
|
return this.withSessionRecovery("app data reset", async () => {
|
|
359009
359946
|
try {
|
|
359010
|
-
|
|
359947
|
+
if (this.platform === "IOS") {
|
|
359948
|
+
try {
|
|
359949
|
+
await this.executeScript("mobile: terminateApp", [{ ...this.appLifecycleArgs(), timeout: 5e3 }]);
|
|
359950
|
+
} catch (err) {
|
|
359951
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
359952
|
+
}
|
|
359953
|
+
const wiped = await clearIosSimulatorAppData(this.deviceId ?? "", this.appId);
|
|
359954
|
+
if (!wiped.ok) {
|
|
359955
|
+
throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
|
|
359956
|
+
}
|
|
359957
|
+
} else {
|
|
359958
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
359959
|
+
}
|
|
359011
359960
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
359012
359961
|
try {
|
|
359013
359962
|
await waitForColdBootSettle(this.sessionPath);
|
|
@@ -359598,6 +360547,113 @@ function envInt(name, fallback) {
|
|
|
359598
360547
|
}
|
|
359599
360548
|
var MOBILE_DEVICE_CEILING = Math.max(0, envInt("MOBILE_DEVICE_CEILING", 0));
|
|
359600
360549
|
var MOBILE_HEAL_HOLD_MS = Math.max(0, envInt("MOBILE_HEAL_HOLD_MS", 2e4));
|
|
360550
|
+
var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 30 * 6e4));
|
|
360551
|
+
var MOBILE_RUN_ABORT_GRACE_MS = Math.max(0, envInt("MOBILE_RUN_ABORT_GRACE_MS", 2 * 6e4));
|
|
360552
|
+
var MOBILE_CANCEL_POLL_MS = Math.max(0, envInt("MOBILE_CANCEL_POLL_MS", 15e3));
|
|
360553
|
+
var MobileRunWatchdog = class {
|
|
360554
|
+
constructor(options) {
|
|
360555
|
+
this.options = options;
|
|
360556
|
+
}
|
|
360557
|
+
phase = "idle";
|
|
360558
|
+
released = false;
|
|
360559
|
+
quarantined = false;
|
|
360560
|
+
cancelAbortTimer = null;
|
|
360561
|
+
cancelGraceTimer = null;
|
|
360562
|
+
/** Current phase (exposed for tests / observability). */
|
|
360563
|
+
getPhase() {
|
|
360564
|
+
return this.phase;
|
|
360565
|
+
}
|
|
360566
|
+
/** Arm the wall-clock abort timer. Call once, immediately after dispatch. */
|
|
360567
|
+
start() {
|
|
360568
|
+
if (this.phase !== "idle") return;
|
|
360569
|
+
this.phase = "active";
|
|
360570
|
+
if (this.options.abortMs > 0) {
|
|
360571
|
+
this.cancelAbortTimer = this.options.schedule(
|
|
360572
|
+
() => this.triggerAbort(`exceeded the ${Math.round(this.options.abortMs / 6e4)}min wall-clock ceiling`),
|
|
360573
|
+
this.options.abortMs
|
|
360574
|
+
);
|
|
360575
|
+
}
|
|
360576
|
+
}
|
|
360577
|
+
/** Stage 1. Idempotent (a no-op unless still ACTIVE). Also called by the
|
|
360578
|
+
* cancellation observer. Quarantines the device (blocking the run's own
|
|
360579
|
+
* session-recovery from resurrecting the killed session — observed live),
|
|
360580
|
+
* kills the run's session (best-effort), and starts the grace clock;
|
|
360581
|
+
* deliberately does NOT release the lease. */
|
|
360582
|
+
triggerAbort(reason) {
|
|
360583
|
+
if (this.phase !== "active") return;
|
|
360584
|
+
this.phase = "aborting";
|
|
360585
|
+
this.clearAbortTimer();
|
|
360586
|
+
if (this.options.deviceId) {
|
|
360587
|
+
this.quarantined = true;
|
|
360588
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360589
|
+
this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
|
|
360590
|
+
} else {
|
|
360591
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
|
|
360592
|
+
}
|
|
360593
|
+
if (this.options.graceMs > 0) {
|
|
360594
|
+
this.cancelGraceTimer = this.options.schedule(() => this.forceRelease(), this.options.graceMs);
|
|
360595
|
+
} else {
|
|
360596
|
+
this.forceRelease();
|
|
360597
|
+
}
|
|
360598
|
+
}
|
|
360599
|
+
/** The run's promise settled (normal completion, or after an abort). The single
|
|
360600
|
+
* release owner: releases at most once and lifts any quarantine. Idempotent. */
|
|
360601
|
+
settle() {
|
|
360602
|
+
if (this.phase === "settled") return;
|
|
360603
|
+
const wasForceReleased = this.released;
|
|
360604
|
+
this.phase = "settled";
|
|
360605
|
+
this.clearAbortTimer();
|
|
360606
|
+
this.clearGraceTimer();
|
|
360607
|
+
if (!this.released) {
|
|
360608
|
+
this.released = true;
|
|
360609
|
+
this.options.callbacks.release();
|
|
360610
|
+
}
|
|
360611
|
+
if (this.quarantined && this.options.deviceId) {
|
|
360612
|
+
this.quarantined = false;
|
|
360613
|
+
this.options.callbacks.unquarantine(this.options.deviceId);
|
|
360614
|
+
if (wasForceReleased) {
|
|
360615
|
+
this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
|
|
360616
|
+
}
|
|
360617
|
+
}
|
|
360618
|
+
}
|
|
360619
|
+
/** Stage 2. The run did not settle within the grace window after the abort. Force-
|
|
360620
|
+
* release the slot to unwedge capacity; the device stays quarantined (since the
|
|
360621
|
+
* stage-1 abort) until the promise eventually settles. Idempotent and guarded
|
|
360622
|
+
* against a late settle. */
|
|
360623
|
+
forceRelease() {
|
|
360624
|
+
if (this.released || this.phase === "settled") return;
|
|
360625
|
+
this.clearGraceTimer();
|
|
360626
|
+
this.released = true;
|
|
360627
|
+
this.options.callbacks.release();
|
|
360628
|
+
if (this.options.deviceId) {
|
|
360629
|
+
if (!this.quarantined) {
|
|
360630
|
+
this.quarantined = true;
|
|
360631
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360632
|
+
}
|
|
360633
|
+
this.options.callbacks.log(
|
|
360634
|
+
"warn",
|
|
360635
|
+
`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.`
|
|
360636
|
+
);
|
|
360637
|
+
} else {
|
|
360638
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} did not settle after abort \u2014 force-released its slot to unwedge mobile capacity.`);
|
|
360639
|
+
}
|
|
360640
|
+
}
|
|
360641
|
+
clearAbortTimer() {
|
|
360642
|
+
if (this.cancelAbortTimer) {
|
|
360643
|
+
this.cancelAbortTimer();
|
|
360644
|
+
this.cancelAbortTimer = null;
|
|
360645
|
+
}
|
|
360646
|
+
}
|
|
360647
|
+
clearGraceTimer() {
|
|
360648
|
+
if (this.cancelGraceTimer) {
|
|
360649
|
+
this.cancelGraceTimer();
|
|
360650
|
+
this.cancelGraceTimer = null;
|
|
360651
|
+
}
|
|
360652
|
+
}
|
|
360653
|
+
};
|
|
360654
|
+
function isServerCancelledRunStatus(status) {
|
|
360655
|
+
return status === "ERROR" || status === "CANCELLED" || status === "CANCELLING";
|
|
360656
|
+
}
|
|
359601
360657
|
function scrubAuditMessages(messages, testVariables) {
|
|
359602
360658
|
if (messages === void 0 || messages === null) return messages;
|
|
359603
360659
|
const compacted = compactForAudit(messages);
|
|
@@ -359673,7 +360729,7 @@ var log = {
|
|
|
359673
360729
|
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
359674
360730
|
};
|
|
359675
360731
|
function getRunsDir() {
|
|
359676
|
-
const dir = process.env.RUNS_DIR ||
|
|
360732
|
+
const dir = process.env.RUNS_DIR || join7(homedir3(), ".validate.qa", "runs");
|
|
359677
360733
|
(0, import_runner_core12.ensurePrivateDirectory)(dir);
|
|
359678
360734
|
return dir;
|
|
359679
360735
|
}
|
|
@@ -361159,9 +362215,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361159
362215
|
// flip the test to needs_manual_authoring), so an environmentally
|
|
361160
362216
|
// aborted pass — wrong screen, system dialog, transport flake —
|
|
361161
362217
|
// must not persist step divergences it could not fairly judge.
|
|
361162
|
-
//
|
|
361163
|
-
//
|
|
361164
|
-
|
|
362218
|
+
// The same applies to a harvest that ADVANCED THROUGH NOTHING
|
|
362219
|
+
// (reachedOrder null): it never got past the entry screen, so its
|
|
362220
|
+
// step judgments are cold-boot/entry noise, not app knowledge —
|
|
362221
|
+
// observed live: a slow WKWebView hydration marked step 1
|
|
362222
|
+
// divergent and permanently dead-ended the test in
|
|
362223
|
+
// needs_manual_authoring with no retry path. Harvested bundles
|
|
362224
|
+
// are still posted: they were positively resolved and the merge
|
|
362225
|
+
// is idempotent.
|
|
362226
|
+
divergentOrders: harvest.environmentalError || harvest.reachedOrder == null ? [] : harvest.divergentOrders,
|
|
361165
362227
|
reachedOrder: harvest.reachedOrder,
|
|
361166
362228
|
logs: harvest.logs,
|
|
361167
362229
|
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
@@ -361605,9 +362667,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361605
362667
|
sessionId: session.sessionId,
|
|
361606
362668
|
platform: platform3,
|
|
361607
362669
|
appId: session.appId,
|
|
361608
|
-
|
|
361609
|
-
//
|
|
361610
|
-
//
|
|
362670
|
+
deviceId: session.deviceId,
|
|
362671
|
+
// Deterministic-state authorization: Android emulators / iOS
|
|
362672
|
+
// simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
|
|
362673
|
+
// Android default) — a personal physical device must never have
|
|
362674
|
+
// its app data wiped.
|
|
361611
362675
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
361612
362676
|
maxSessionRecoveries: 1,
|
|
361613
362677
|
recoverSession: async (cause) => {
|
|
@@ -362123,6 +363187,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362123
363187
|
sessionId: opened.sessionId,
|
|
362124
363188
|
platform: target.platform,
|
|
362125
363189
|
appId: opened.appId,
|
|
363190
|
+
deviceId: opened.deviceId,
|
|
362126
363191
|
allowAppReset: target.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(target.platform, opened.deviceId)
|
|
362127
363192
|
});
|
|
362128
363193
|
};
|
|
@@ -362735,6 +363800,7 @@ async function startRunner(opts) {
|
|
|
362735
363800
|
else activeMobileOpsByPlatform.set(p, next);
|
|
362736
363801
|
};
|
|
362737
363802
|
const leasedDeviceIds = /* @__PURE__ */ new Map();
|
|
363803
|
+
const activeMobileRuns = /* @__PURE__ */ new Map();
|
|
362738
363804
|
let activeMobileDiscoveries = 0;
|
|
362739
363805
|
const mobileHealHold = /* @__PURE__ */ new Map();
|
|
362740
363806
|
const pruneMobileHealHolds = () => {
|
|
@@ -362815,6 +363881,7 @@ async function startRunner(opts) {
|
|
|
362815
363881
|
}
|
|
362816
363882
|
}
|
|
362817
363883
|
};
|
|
363884
|
+
const heartbeatKeepalive = startHeartbeatKeepalive();
|
|
362818
363885
|
const sendHeartbeat = async () => {
|
|
362819
363886
|
try {
|
|
362820
363887
|
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
@@ -362829,41 +363896,44 @@ async function startRunner(opts) {
|
|
|
362829
363896
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
362830
363897
|
lastInventoryKinds.clear();
|
|
362831
363898
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
362832
|
-
const
|
|
363899
|
+
const heartbeatUrl = `${serverUrl}/api/runner/heartbeat`;
|
|
363900
|
+
const heartbeatBody = JSON.stringify({
|
|
363901
|
+
runnerName: RUNNER_NAME,
|
|
363902
|
+
runnerVersion: RUNNER_VERSION,
|
|
363903
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
363904
|
+
runsCompleted,
|
|
363905
|
+
activeRuns,
|
|
363906
|
+
maxConcurrent: MAX_CONCURRENT,
|
|
363907
|
+
uptimeMs: Date.now() - startedAt,
|
|
363908
|
+
capabilities,
|
|
363909
|
+
devices,
|
|
363910
|
+
mobileBridge: bridge ? {
|
|
363911
|
+
online: bridge.running,
|
|
363912
|
+
port: bridge.port,
|
|
363913
|
+
tunnelUrl: bridge.tunnelUrl,
|
|
363914
|
+
tunnelToken: bridge.tunnelToken
|
|
363915
|
+
} : void 0,
|
|
363916
|
+
activeByType: Object.fromEntries(activeByType),
|
|
363917
|
+
activeByProject: Object.fromEntries(activeByProject),
|
|
363918
|
+
typeLimits: TYPE_LIMITS,
|
|
363919
|
+
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
363920
|
+
resourceSnapshot: resourceStatus.snapshot,
|
|
363921
|
+
resourceGovernor: resourceStatus.governor,
|
|
363922
|
+
processInventory,
|
|
363923
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
363924
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
363925
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
363926
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
363927
|
+
doctor: getCachedDoctor()
|
|
363928
|
+
});
|
|
363929
|
+
const response = await fetch(heartbeatUrl, {
|
|
362833
363930
|
method: "POST",
|
|
362834
363931
|
headers,
|
|
362835
|
-
body:
|
|
362836
|
-
runnerName: RUNNER_NAME,
|
|
362837
|
-
runnerVersion: RUNNER_VERSION,
|
|
362838
|
-
runnerBuildId: RUNNER_BUILD_ID,
|
|
362839
|
-
runsCompleted,
|
|
362840
|
-
activeRuns,
|
|
362841
|
-
maxConcurrent: MAX_CONCURRENT,
|
|
362842
|
-
uptimeMs: Date.now() - startedAt,
|
|
362843
|
-
capabilities,
|
|
362844
|
-
devices,
|
|
362845
|
-
mobileBridge: bridge ? {
|
|
362846
|
-
online: bridge.running,
|
|
362847
|
-
port: bridge.port,
|
|
362848
|
-
tunnelUrl: bridge.tunnelUrl,
|
|
362849
|
-
tunnelToken: bridge.tunnelToken
|
|
362850
|
-
} : void 0,
|
|
362851
|
-
activeByType: Object.fromEntries(activeByType),
|
|
362852
|
-
activeByProject: Object.fromEntries(activeByProject),
|
|
362853
|
-
typeLimits: TYPE_LIMITS,
|
|
362854
|
-
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
362855
|
-
resourceSnapshot: resourceStatus.snapshot,
|
|
362856
|
-
resourceGovernor: resourceStatus.governor,
|
|
362857
|
-
processInventory,
|
|
362858
|
-
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
362859
|
-
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
362860
|
-
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
362861
|
-
// Cached & cheap — see getCachedDoctor above.
|
|
362862
|
-
doctor: getCachedDoctor()
|
|
362863
|
-
}),
|
|
363932
|
+
body: heartbeatBody,
|
|
362864
363933
|
signal: AbortSignal.timeout(1e4)
|
|
362865
363934
|
});
|
|
362866
363935
|
if (response.ok) {
|
|
363936
|
+
heartbeatKeepalive.update(heartbeatUrl, headers, heartbeatBody);
|
|
362867
363937
|
const body = await response.json().catch(() => null);
|
|
362868
363938
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
362869
363939
|
void handleControlCommands(body.commands);
|
|
@@ -362934,6 +364004,7 @@ async function startRunner(opts) {
|
|
|
362934
364004
|
process.exit(1);
|
|
362935
364005
|
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
362936
364006
|
clearInterval(heartbeat);
|
|
364007
|
+
heartbeatKeepalive.stop();
|
|
362937
364008
|
if (liveBrowserHeartbeatTimer) {
|
|
362938
364009
|
clearTimeout(liveBrowserHeartbeatTimer);
|
|
362939
364010
|
liveBrowserHeartbeatTimer = null;
|
|
@@ -362985,6 +364056,32 @@ async function startRunner(opts) {
|
|
|
362985
364056
|
}
|
|
362986
364057
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
362987
364058
|
pidsWatchdog.unref();
|
|
364059
|
+
const pollMobileRunCancellations = async () => {
|
|
364060
|
+
if (shuttingDown || activeMobileRuns.size === 0) return;
|
|
364061
|
+
const runIds = [...activeMobileRuns.keys()];
|
|
364062
|
+
try {
|
|
364063
|
+
const res = await fetch(`${serverUrl}/api/runner/runs/status`, {
|
|
364064
|
+
method: "POST",
|
|
364065
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
364066
|
+
body: JSON.stringify({ runIds }),
|
|
364067
|
+
signal: AbortSignal.timeout(1e4)
|
|
364068
|
+
});
|
|
364069
|
+
if (!res.ok) return;
|
|
364070
|
+
const body = await res.json().catch(() => null);
|
|
364071
|
+
if (!body || !Array.isArray(body.runs)) return;
|
|
364072
|
+
for (const row of body.runs) {
|
|
364073
|
+
if (!row || typeof row.id !== "string" || typeof row.status !== "string") continue;
|
|
364074
|
+
if (!isServerCancelledRunStatus(row.status)) continue;
|
|
364075
|
+
const watchdog = activeMobileRuns.get(row.id);
|
|
364076
|
+
if (watchdog) watchdog.triggerAbort(`was cancelled server-side (status ${row.status})`);
|
|
364077
|
+
}
|
|
364078
|
+
} catch {
|
|
364079
|
+
}
|
|
364080
|
+
};
|
|
364081
|
+
const mobileCancelPoll = enableMobile && MOBILE_CANCEL_POLL_MS > 0 ? setInterval(() => {
|
|
364082
|
+
void pollMobileRunCancellations();
|
|
364083
|
+
}, MOBILE_CANCEL_POLL_MS) : null;
|
|
364084
|
+
mobileCancelPoll?.unref?.();
|
|
362988
364085
|
const deferralLogged = /* @__PURE__ */ new Map();
|
|
362989
364086
|
const logDeferredOnce = (runId, line) => {
|
|
362990
364087
|
if (deferralLogged.get(runId) === line) return;
|
|
@@ -363067,14 +364164,17 @@ async function startRunner(opts) {
|
|
|
363067
364164
|
});
|
|
363068
364165
|
const capacity = MOBILE_DEVICE_CEILING > 0 ? Math.min(derived, MOBILE_DEVICE_CEILING) : derived;
|
|
363069
364166
|
const leaseTarget = run2.target ?? run2.deviceRequirements;
|
|
363070
|
-
const
|
|
364167
|
+
const leaseExclusions = {
|
|
364168
|
+
has: (id) => leasedDeviceIds.has(id) || isDeviceQuarantined(id)
|
|
364169
|
+
};
|
|
364170
|
+
const leaseSelection = selectFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot, {
|
|
363071
364171
|
appId: typeof leaseTarget?.appId === "string" ? leaseTarget.appId : void 0,
|
|
363072
364172
|
recordedDeviceId: typeof leaseTarget?.recordedDeviceId === "string" ? leaseTarget.recordedDeviceId : void 0,
|
|
363073
364173
|
platformVersion: typeof leaseTarget?.platformVersion === "string" ? leaseTarget.platformVersion : void 0,
|
|
363074
364174
|
deviceName: typeof run2.deviceRequirements?.deviceName === "string" ? run2.deviceRequirements.deviceName : void 0,
|
|
363075
364175
|
resetPolicy: leaseTarget?.resetPolicy
|
|
363076
364176
|
});
|
|
363077
|
-
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform,
|
|
364177
|
+
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot) : null;
|
|
363078
364178
|
if (leaseSelection.kind === "unsatisfiable") {
|
|
363079
364179
|
mobileLeaseError = leaseSelection.reason;
|
|
363080
364180
|
log.warn(` Mobile requirements unsatisfiable for ${run2.runId}: ${leaseSelection.reason}; claiming once to report the configuration error`);
|
|
@@ -363132,6 +364232,54 @@ async function startRunner(opts) {
|
|
|
363132
364232
|
await new Promise((r) => setTimeout(r, staggerMs));
|
|
363133
364233
|
}
|
|
363134
364234
|
claimedThisPoll++;
|
|
364235
|
+
const applyRelease = () => {
|
|
364236
|
+
activeRuns--;
|
|
364237
|
+
decrementType(runMode);
|
|
364238
|
+
decrementProject(runProject);
|
|
364239
|
+
if (isMobileOp) {
|
|
364240
|
+
if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
|
|
364241
|
+
if (isDiscovery) activeMobileDiscoveries--;
|
|
364242
|
+
if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
|
|
364243
|
+
activeMobileRuns.delete(run2.runId);
|
|
364244
|
+
}
|
|
364245
|
+
};
|
|
364246
|
+
const runWatchdog = isMobileOp ? new MobileRunWatchdog({
|
|
364247
|
+
runId: run2.runId,
|
|
364248
|
+
deviceId: leasedDeviceId ?? void 0,
|
|
364249
|
+
abortMs: MOBILE_RUN_WATCHDOG_MS,
|
|
364250
|
+
graceMs: MOBILE_RUN_ABORT_GRACE_MS,
|
|
364251
|
+
// Real timers, unref'd so a pending watchdog never keeps the process
|
|
364252
|
+
// alive on shutdown.
|
|
364253
|
+
schedule: (fn, ms) => {
|
|
364254
|
+
const t = setTimeout(fn, ms);
|
|
364255
|
+
if (typeof t.unref === "function") t.unref();
|
|
364256
|
+
return () => clearTimeout(t);
|
|
364257
|
+
},
|
|
364258
|
+
callbacks: {
|
|
364259
|
+
abort: (deviceId, rid, reason) => {
|
|
364260
|
+
const sessionId = getMirrorSessionId(deviceId);
|
|
364261
|
+
log.warn(
|
|
364262
|
+
`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.`
|
|
364263
|
+
);
|
|
364264
|
+
if (sessionId) void deleteAppiumSession(sessionId);
|
|
364265
|
+
},
|
|
364266
|
+
release: applyRelease,
|
|
364267
|
+
quarantine: (deviceId) => {
|
|
364268
|
+
quarantineDevice(deviceId);
|
|
364269
|
+
},
|
|
364270
|
+
unquarantine: (deviceId) => {
|
|
364271
|
+
unquarantineDevice(deviceId);
|
|
364272
|
+
},
|
|
364273
|
+
log: (level, message) => {
|
|
364274
|
+
if (level === "warn") log.warn(message);
|
|
364275
|
+
else log.info(message);
|
|
364276
|
+
}
|
|
364277
|
+
}
|
|
364278
|
+
}) : null;
|
|
364279
|
+
if (runWatchdog) {
|
|
364280
|
+
activeMobileRuns.set(run2.runId, runWatchdog);
|
|
364281
|
+
runWatchdog.start();
|
|
364282
|
+
}
|
|
363135
364283
|
executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
|
|
363136
364284
|
if (mobilePlatform) {
|
|
363137
364285
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
@@ -363142,14 +364290,8 @@ async function startRunner(opts) {
|
|
|
363142
364290
|
const msg = err instanceof Error ? err.message : String(err);
|
|
363143
364291
|
log.warn(`Run ${run2.runId} crashed: ${msg}`);
|
|
363144
364292
|
}).finally(() => {
|
|
363145
|
-
|
|
363146
|
-
|
|
363147
|
-
decrementProject(runProject);
|
|
363148
|
-
if (isMobileOp) {
|
|
363149
|
-
if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
|
|
363150
|
-
if (isDiscovery) activeMobileDiscoveries--;
|
|
363151
|
-
if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
|
|
363152
|
-
}
|
|
364293
|
+
if (runWatchdog) runWatchdog.settle();
|
|
364294
|
+
else applyRelease();
|
|
363153
364295
|
});
|
|
363154
364296
|
}
|
|
363155
364297
|
}
|