@validate.qa/runner 1.0.70 → 1.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1377 -351
- package/dist/cli.mjs +1374 -348
- package/package.json +2 -2
package/dist/cli.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) {
|
|
@@ -11298,7 +11497,7 @@ var require_thread_stream = __commonJS({
|
|
|
11298
11497
|
var { version } = require_package();
|
|
11299
11498
|
var { EventEmitter: EventEmitter2 } = __require("events");
|
|
11300
11499
|
var { Worker } = __require("worker_threads");
|
|
11301
|
-
var { join:
|
|
11500
|
+
var { join: join8 } = __require("path");
|
|
11302
11501
|
var { pathToFileURL } = __require("url");
|
|
11303
11502
|
var { wait } = require_wait();
|
|
11304
11503
|
var {
|
|
@@ -11349,7 +11548,7 @@ 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"] ||
|
|
11551
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11353
11552
|
const worker = new Worker(toExecute, {
|
|
11354
11553
|
...opts.workerOpts,
|
|
11355
11554
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -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) => {
|
|
@@ -81172,10 +81371,14 @@ var require_mobile_generator = __commonJS({
|
|
|
81172
81371
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
81173
81372
|
"use strict";
|
|
81174
81373
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81374
|
+
exports.structuralScreenKey = structuralScreenKey;
|
|
81175
81375
|
exports.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
81176
81376
|
exports.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
81177
81377
|
exports.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
81178
81378
|
exports.collapseRetryCycles = collapseRetryCycles;
|
|
81379
|
+
exports.coalesceSameFieldTypes = coalesceSameFieldTypes;
|
|
81380
|
+
exports.coalesceRepeatedTaps = coalesceRepeatedTaps;
|
|
81381
|
+
exports.elideNoOpDetours = elideNoOpDetours;
|
|
81179
81382
|
exports.mobileActionsToPseudoPlaywright = mobileActionsToPseudoPlaywright;
|
|
81180
81383
|
exports.evaluateMobileAssertionStrength = evaluateMobileAssertionStrength;
|
|
81181
81384
|
exports.prepareMobileScenarioAppState = prepareMobileScenarioAppState;
|
|
@@ -81384,6 +81587,22 @@ var require_mobile_generator = __commonJS({
|
|
|
81384
81587
|
MOBILE_ASSERT_TEXT_TOOL,
|
|
81385
81588
|
FINISH_GENERATE_TOOL
|
|
81386
81589
|
];
|
|
81590
|
+
var STRUCTURAL_KEY_MAX_ELEMENTS = 16;
|
|
81591
|
+
function structuralScreenKey(resolved) {
|
|
81592
|
+
if (!resolved)
|
|
81593
|
+
return null;
|
|
81594
|
+
const nativeElements = resolved.screen.elements;
|
|
81595
|
+
const hybridElements = resolved.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81596
|
+
if (nativeElements.length + hybridElements.length < 2)
|
|
81597
|
+
return null;
|
|
81598
|
+
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 ?? ""}`);
|
|
81599
|
+
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 ?? ""}`);
|
|
81600
|
+
const activityOrBundle = resolved.signal.activity ?? resolved.signal.bundleId ?? "";
|
|
81601
|
+
const payload = `${activityOrBundle}
|
|
81602
|
+
${nativeSkeleton.join("\n")}
|
|
81603
|
+
${hybridSkeleton.join("\n")}`;
|
|
81604
|
+
return (0, crypto_1.createHash)("sha1").update(payload).digest("hex");
|
|
81605
|
+
}
|
|
81387
81606
|
function platformForMedium(medium) {
|
|
81388
81607
|
if (medium === "IOS_NATIVE")
|
|
81389
81608
|
return "IOS";
|
|
@@ -81545,18 +81764,39 @@ var require_mobile_generator = __commonJS({
|
|
|
81545
81764
|
}
|
|
81546
81765
|
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
81766
|
}
|
|
81548
|
-
function
|
|
81767
|
+
function alreadyFocusedWithEqualValue(element, value) {
|
|
81768
|
+
return element.focused === true && typeof element.text === "string" && value.length > 0 && element.text === value;
|
|
81769
|
+
}
|
|
81770
|
+
function nativeElementsOf(resolved) {
|
|
81771
|
+
return resolved?.screen.elements ?? [];
|
|
81772
|
+
}
|
|
81773
|
+
function hybridElementsOf(resolved) {
|
|
81774
|
+
return resolved?.hybridWeb ? Array.from(resolved.hybridWeb.values()) : [];
|
|
81775
|
+
}
|
|
81776
|
+
function metadataForElement(element, screenName, platform3, elements = []) {
|
|
81777
|
+
const match = (0, mobile_source_parser_js_2.identityMatchInfo)(element, elements);
|
|
81549
81778
|
return {
|
|
81550
81779
|
screenName,
|
|
81551
81780
|
targetLabel: labelForElement(element),
|
|
81552
81781
|
locatorBundle: (0, mobile_source_parser_js_2.buildLocatorBundle)(element, platform3),
|
|
81553
|
-
bounds: element.bounds
|
|
81782
|
+
bounds: element.bounds,
|
|
81783
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81554
81784
|
};
|
|
81555
81785
|
}
|
|
81556
|
-
function
|
|
81786
|
+
function hybridIdentityMatchInfo(element, elements) {
|
|
81787
|
+
const hybridIdentityValue = (candidate) => nonEmpty3(candidate.id) ?? (candidate.editable === true ? void 0 : nonEmpty3(candidate.text));
|
|
81788
|
+
const chosenValue = hybridIdentityValue(element);
|
|
81789
|
+
if (!chosenValue)
|
|
81790
|
+
return void 0;
|
|
81791
|
+
const sameValue = elements.filter((candidate) => hybridIdentityValue(candidate) === chosenValue);
|
|
81792
|
+
const matchIndex = sameValue.indexOf(element);
|
|
81793
|
+
return matchIndex >= 0 && sameValue.length > 1 ? { matchIndex, expectedMatchCount: sameValue.length } : void 0;
|
|
81794
|
+
}
|
|
81795
|
+
function metadataForHybridElement(element, screenName, elements = []) {
|
|
81557
81796
|
const editable = isEditableHybridElement(element);
|
|
81558
81797
|
const label = labelForHybridElement(element);
|
|
81559
81798
|
const safeText = editable ? void 0 : nonEmpty3(element.text);
|
|
81799
|
+
const match = hybridIdentityMatchInfo(element, elements);
|
|
81560
81800
|
return {
|
|
81561
81801
|
screenName,
|
|
81562
81802
|
targetLabel: label,
|
|
@@ -81565,11 +81805,16 @@ var require_mobile_generator = __commonJS({
|
|
|
81565
81805
|
webviewContext: element.webviewContext,
|
|
81566
81806
|
bounds: element.bounds,
|
|
81567
81807
|
locatorBundle: {
|
|
81568
|
-
|
|
81808
|
+
// WebView controls resolve via `webCss` (above) in the WEBVIEW context, not
|
|
81809
|
+
// by xpath. `LocatorBundle.xpath` is contractually an XPath, so never store a
|
|
81810
|
+
// `css=` selector here — an empty xpath keeps the fallback chain honest and
|
|
81811
|
+
// prevents the css selector being misread as an xpath at replay.
|
|
81812
|
+
xpath: "",
|
|
81569
81813
|
className: element.tag || "webview",
|
|
81570
81814
|
...element.id ? { accessibilityId: element.id } : {},
|
|
81571
81815
|
...safeText ? { text: safeText } : {}
|
|
81572
|
-
}
|
|
81816
|
+
},
|
|
81817
|
+
...match ? { matchIndex: match.matchIndex, expectedMatchCount: match.expectedMatchCount } : {}
|
|
81573
81818
|
};
|
|
81574
81819
|
}
|
|
81575
81820
|
function isEditableHybridElement(element) {
|
|
@@ -82287,7 +82532,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82287
82532
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
82288
82533
|
recordAction: (action) => {
|
|
82289
82534
|
const screenHash = getLatest()?.signal.screenHash;
|
|
82290
|
-
const
|
|
82535
|
+
const structuralKey = structuralScreenKey(getLatest());
|
|
82536
|
+
const stamped = screenHash && !action.metadata.screenHash || structuralKey && !action.metadata.structuralKey ? {
|
|
82537
|
+
...action,
|
|
82538
|
+
metadata: {
|
|
82539
|
+
...action.metadata,
|
|
82540
|
+
...screenHash && !action.metadata.screenHash ? { screenHash } : {},
|
|
82541
|
+
...structuralKey && !action.metadata.structuralKey ? { structuralKey } : {}
|
|
82542
|
+
}
|
|
82543
|
+
} : action;
|
|
82291
82544
|
actions.push((0, shared_1.placeholderizeRecordedMobileAction)(finalizeAction(stamped), credentials));
|
|
82292
82545
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
82293
82546
|
assertCount++;
|
|
@@ -82393,7 +82646,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82393
82646
|
const result2 = await driver.webTap(hybridTap.webviewContext, hybridTap.css);
|
|
82394
82647
|
const absorbed2 = await absorbActionResult(result2, "mobile_tap(webview)", opensAuthenticationSurface(hybridTap));
|
|
82395
82648
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82396
|
-
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2);
|
|
82649
|
+
const metadata = metadataForHybridElement(hybridTap, beforeScreenName2, hybridElementsOf(before2));
|
|
82397
82650
|
const label = metadata.targetLabel ?? hybridTap.css;
|
|
82398
82651
|
const tapAction = {
|
|
82399
82652
|
type: "TAP",
|
|
@@ -82445,7 +82698,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82445
82698
|
const tapAction = {
|
|
82446
82699
|
type: "TAP",
|
|
82447
82700
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82448
|
-
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
82701
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before)),
|
|
82449
82702
|
innerText: null,
|
|
82450
82703
|
url: "",
|
|
82451
82704
|
value: null
|
|
@@ -82485,10 +82738,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82485
82738
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82486
82739
|
}
|
|
82487
82740
|
const beforeScreenName2 = screenName();
|
|
82741
|
+
const before2 = latest();
|
|
82488
82742
|
const result2 = await driver.webType(hybridType.webviewContext, hybridType.css, value);
|
|
82489
82743
|
const absorbed2 = await absorbActionResult(result2, "mobile_type(webview)");
|
|
82490
82744
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82491
|
-
const metadata = metadataForHybridElement(hybridType, beforeScreenName2);
|
|
82745
|
+
const metadata = metadataForHybridElement(hybridType, beforeScreenName2, hybridElementsOf(before2));
|
|
82492
82746
|
const label = metadata.targetLabel ?? hybridType.css;
|
|
82493
82747
|
recordAction({
|
|
82494
82748
|
type: "TYPE",
|
|
@@ -82503,7 +82757,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82503
82757
|
const element = resolveRef(toolArgs.ref);
|
|
82504
82758
|
if (!element)
|
|
82505
82759
|
return unknownRef(toolArgs.ref);
|
|
82760
|
+
if (alreadyFocusedWithEqualValue(element, value)) {
|
|
82761
|
+
return {
|
|
82762
|
+
ok: false,
|
|
82763
|
+
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.`
|
|
82764
|
+
};
|
|
82765
|
+
}
|
|
82506
82766
|
const beforeScreenName = screenName();
|
|
82767
|
+
const before = latest();
|
|
82507
82768
|
const result = await driver.type(value, element.bounds);
|
|
82508
82769
|
const absorbed = await absorbActionResult(result, "mobile_type");
|
|
82509
82770
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82511,7 +82772,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82511
82772
|
type: "TYPE",
|
|
82512
82773
|
value,
|
|
82513
82774
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82514
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82775
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82515
82776
|
});
|
|
82516
82777
|
}
|
|
82517
82778
|
const payload = actionPayload(result, absorbed);
|
|
@@ -82529,6 +82790,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82529
82790
|
return { ok: false, error: "This driver does not support non-clearing WEBVIEW keyboard submit." };
|
|
82530
82791
|
}
|
|
82531
82792
|
const beforeScreenName2 = screenName();
|
|
82793
|
+
const before2 = latest();
|
|
82532
82794
|
const result2 = await driver.webSubmitKeyboard(hybridField.webviewContext, hybridField.css);
|
|
82533
82795
|
const absorbed2 = await absorbActionResult(result2, "mobile_submit_keyboard(webview)");
|
|
82534
82796
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
@@ -82536,7 +82798,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82536
82798
|
type: "TYPE",
|
|
82537
82799
|
value: "\n",
|
|
82538
82800
|
selector: `css=${hybridField.css}`,
|
|
82539
|
-
metadata: metadataForHybridElement(hybridField, beforeScreenName2)
|
|
82801
|
+
metadata: metadataForHybridElement(hybridField, beforeScreenName2, hybridElementsOf(before2))
|
|
82540
82802
|
});
|
|
82541
82803
|
}
|
|
82542
82804
|
return actionPayload(result2, absorbed2);
|
|
@@ -82548,6 +82810,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82548
82810
|
return { ok: false, error: "This driver does not support non-clearing keyboard submit." };
|
|
82549
82811
|
}
|
|
82550
82812
|
const beforeScreenName = screenName();
|
|
82813
|
+
const before = latest();
|
|
82551
82814
|
const result = await driver.submitKeyboard(element.bounds);
|
|
82552
82815
|
const absorbed = await absorbActionResult(result, "mobile_submit_keyboard");
|
|
82553
82816
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82555,7 +82818,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82555
82818
|
type: "TYPE",
|
|
82556
82819
|
value: "\n",
|
|
82557
82820
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82558
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82821
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82559
82822
|
});
|
|
82560
82823
|
}
|
|
82561
82824
|
return actionPayload(result, absorbed);
|
|
@@ -82580,10 +82843,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82580
82843
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW typing." };
|
|
82581
82844
|
}
|
|
82582
82845
|
const beforeScreenName2 = screenName();
|
|
82846
|
+
const before2 = latest();
|
|
82583
82847
|
const result2 = await driver.webType(hybridFill.webviewContext, hybridFill.css, secret);
|
|
82584
82848
|
const absorbed2 = await absorbActionResult(result2, "mobile_fill_credential(webview)");
|
|
82585
82849
|
if (shouldRecordAction(result2, absorbed2)) {
|
|
82586
|
-
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2);
|
|
82850
|
+
const metadata = metadataForHybridElement(hybridFill, beforeScreenName2, hybridElementsOf(before2));
|
|
82587
82851
|
const label = metadata.targetLabel ?? hybridFill.css;
|
|
82588
82852
|
recordAction({
|
|
82589
82853
|
type: "TYPE",
|
|
@@ -82598,7 +82862,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82598
82862
|
const element = resolveRef(toolArgs.ref);
|
|
82599
82863
|
if (!element)
|
|
82600
82864
|
return unknownRef(toolArgs.ref);
|
|
82865
|
+
if (alreadyFocusedWithEqualValue(element, secret)) {
|
|
82866
|
+
return {
|
|
82867
|
+
ok: false,
|
|
82868
|
+
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.`
|
|
82869
|
+
};
|
|
82870
|
+
}
|
|
82601
82871
|
const beforeScreenName = screenName();
|
|
82872
|
+
const before = latest();
|
|
82602
82873
|
const result = await driver.type(secret, element.bounds);
|
|
82603
82874
|
const absorbed = await absorbActionResult(result, "mobile_fill_credential");
|
|
82604
82875
|
if (shouldRecordAction(result, absorbed)) {
|
|
@@ -82606,7 +82877,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82606
82877
|
type: "TYPE",
|
|
82607
82878
|
value: (0, shared_1.mobileCredentialPlaceholder)(key),
|
|
82608
82879
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82609
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82880
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82610
82881
|
});
|
|
82611
82882
|
log2(` fill_credential: recorded {{${key}}} into ${labelForElement(element)}`);
|
|
82612
82883
|
}
|
|
@@ -82634,13 +82905,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82634
82905
|
if (!element)
|
|
82635
82906
|
return unknownRef(toolArgs.ref);
|
|
82636
82907
|
const beforeScreenName = screenName();
|
|
82908
|
+
const before = latest();
|
|
82637
82909
|
const result = await driver.clear(element.bounds);
|
|
82638
82910
|
const absorbed = await absorbActionResult(result, "mobile_clear");
|
|
82639
82911
|
if (shouldRecordAction(result, absorbed)) {
|
|
82640
82912
|
recordAction({
|
|
82641
82913
|
type: "CLEAR",
|
|
82642
82914
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
82643
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
82915
|
+
metadata: metadataForElement(element, beforeScreenName, platform3, nativeElementsOf(before))
|
|
82644
82916
|
});
|
|
82645
82917
|
}
|
|
82646
82918
|
return actionPayload(result, absorbed);
|
|
@@ -82703,7 +82975,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82703
82975
|
case "mobile_assert_visible": {
|
|
82704
82976
|
const hybrid = resolveHybridRef(toolArgs.ref);
|
|
82705
82977
|
if (hybrid) {
|
|
82706
|
-
const metadata = metadataForHybridElement(hybrid, screenName());
|
|
82978
|
+
const metadata = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82707
82979
|
recordAction({
|
|
82708
82980
|
type: "ASSERT_VISIBLE",
|
|
82709
82981
|
selector: `css=${hybrid.css}`,
|
|
@@ -82743,7 +83015,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82743
83015
|
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
83016
|
};
|
|
82745
83017
|
}
|
|
82746
|
-
const metadata2 = metadataForHybridElement(hybrid, screenName());
|
|
83018
|
+
const metadata2 = metadataForHybridElement(hybrid, screenName(), hybridElementsOf(latest()));
|
|
82747
83019
|
const stableWebIdentity = Boolean(nonEmpty3(hybrid.id) || nonEmpty3(hybrid.name));
|
|
82748
83020
|
if (stableWebIdentity) {
|
|
82749
83021
|
recordAction({
|
|
@@ -82796,7 +83068,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82796
83068
|
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
83069
|
};
|
|
82798
83070
|
}
|
|
82799
|
-
const metadata = metadataForElement(element, screenName(), platform3);
|
|
83071
|
+
const metadata = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82800
83072
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82801
83073
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
82802
83074
|
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
@@ -82842,7 +83114,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82842
83114
|
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
83115
|
};
|
|
82844
83116
|
}
|
|
82845
|
-
const metadata2 = metadataForElement(element, screenName(), platform3);
|
|
83117
|
+
const metadata2 = metadataForElement(element, screenName(), platform3, nativeElementsOf(latest()));
|
|
82846
83118
|
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata: metadata2 });
|
|
82847
83119
|
return { ok: true, label: metadata2.targetLabel ?? "element" };
|
|
82848
83120
|
}
|
|
@@ -82892,7 +83164,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82892
83164
|
error: `The live element matching ${JSON.stringify(accessibilityId ?? text)} is not a durable assertion target (${weakReason}).`
|
|
82893
83165
|
};
|
|
82894
83166
|
}
|
|
82895
|
-
const metadata = metadataForElement(liveElement, screenName(), platform3);
|
|
83167
|
+
const metadata = metadataForElement(liveElement, screenName(), platform3, live.screen.elements);
|
|
82896
83168
|
recordAction({
|
|
82897
83169
|
type: "ASSERT_VISIBLE",
|
|
82898
83170
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(liveElement),
|
|
@@ -83020,7 +83292,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83020
83292
|
actions.push(finalizeAction({
|
|
83021
83293
|
type: "ASSERT_VISIBLE",
|
|
83022
83294
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(anchor),
|
|
83023
|
-
metadata: metadataForElement(anchor, screen, platform3)
|
|
83295
|
+
metadata: metadataForElement(anchor, screen, platform3, nativeElementsOf(recording.lastSnapshot))
|
|
83024
83296
|
}));
|
|
83025
83297
|
return true;
|
|
83026
83298
|
}
|
|
@@ -83363,6 +83635,153 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83363
83635
|
}
|
|
83364
83636
|
return out;
|
|
83365
83637
|
}
|
|
83638
|
+
function isDurableMutation(action) {
|
|
83639
|
+
if (action.type === "TYPE" || action.type === "CLEAR")
|
|
83640
|
+
return true;
|
|
83641
|
+
if (action.type === "TAP")
|
|
83642
|
+
return (0, shared_1.isStateValueMobileClassName)(action.metadata.locatorBundle?.className);
|
|
83643
|
+
return false;
|
|
83644
|
+
}
|
|
83645
|
+
function mobileWanderTraceEnabled2() {
|
|
83646
|
+
const v = (process.env.VALIDATEQA_MOBILE_WANDER_TRACE || "").trim().toLowerCase();
|
|
83647
|
+
return v === "true" || v === "1";
|
|
83648
|
+
}
|
|
83649
|
+
function wanderTraceLabel(action) {
|
|
83650
|
+
const identity = action.metadata.targetLabel || action.selector || action.metadata.locatorBundle?.accessibilityId || action.metadata.locatorBundle?.resourceId || "(no identity)";
|
|
83651
|
+
return `${action.type} ${identity}`;
|
|
83652
|
+
}
|
|
83653
|
+
function logWanderDiff(pass, before, after, log2) {
|
|
83654
|
+
if (after.length >= before.length)
|
|
83655
|
+
return;
|
|
83656
|
+
log2(` ${pass}: collapsed ${before.length - after.length} step(s) (${before.length} \u2192 ${after.length}).`);
|
|
83657
|
+
if (!mobileWanderTraceEnabled2())
|
|
83658
|
+
return;
|
|
83659
|
+
const survivors = new Set(after);
|
|
83660
|
+
for (const action of before) {
|
|
83661
|
+
if (!survivors.has(action)) {
|
|
83662
|
+
log2(` [wander] ${pass}: dropped ${wanderTraceLabel(action)}`);
|
|
83663
|
+
}
|
|
83664
|
+
}
|
|
83665
|
+
}
|
|
83666
|
+
function coalesceSameFieldTypes(actions) {
|
|
83667
|
+
if (actions.length <= 1)
|
|
83668
|
+
return actions;
|
|
83669
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83670
|
+
const lastTypeIndexBySelector = /* @__PURE__ */ new Map();
|
|
83671
|
+
const refocusTapIndexesBySelector = /* @__PURE__ */ new Map();
|
|
83672
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83673
|
+
const current = actions[i];
|
|
83674
|
+
if (current.type === "ASSERT_VISIBLE" || current.type === "ASSERT_TEXT") {
|
|
83675
|
+
lastTypeIndexBySelector.clear();
|
|
83676
|
+
refocusTapIndexesBySelector.clear();
|
|
83677
|
+
continue;
|
|
83678
|
+
}
|
|
83679
|
+
if (current.type === "CLEAR") {
|
|
83680
|
+
lastTypeIndexBySelector.delete(current.selector);
|
|
83681
|
+
refocusTapIndexesBySelector.delete(current.selector);
|
|
83682
|
+
continue;
|
|
83683
|
+
}
|
|
83684
|
+
if (isFormCommitAction(current)) {
|
|
83685
|
+
lastTypeIndexBySelector.clear();
|
|
83686
|
+
refocusTapIndexesBySelector.clear();
|
|
83687
|
+
continue;
|
|
83688
|
+
}
|
|
83689
|
+
if (current.type === "TAP") {
|
|
83690
|
+
if (current.selector) {
|
|
83691
|
+
const list = refocusTapIndexesBySelector.get(current.selector) ?? [];
|
|
83692
|
+
list.push(i);
|
|
83693
|
+
refocusTapIndexesBySelector.set(current.selector, list);
|
|
83694
|
+
}
|
|
83695
|
+
continue;
|
|
83696
|
+
}
|
|
83697
|
+
if (current.type !== "TYPE" || !current.selector)
|
|
83698
|
+
continue;
|
|
83699
|
+
const priorIndex = lastTypeIndexBySelector.get(current.selector);
|
|
83700
|
+
if (priorIndex !== void 0) {
|
|
83701
|
+
toDrop.add(priorIndex);
|
|
83702
|
+
for (const tapIndex of refocusTapIndexesBySelector.get(current.selector) ?? []) {
|
|
83703
|
+
if (tapIndex > priorIndex && tapIndex < i)
|
|
83704
|
+
toDrop.add(tapIndex);
|
|
83705
|
+
}
|
|
83706
|
+
}
|
|
83707
|
+
lastTypeIndexBySelector.set(current.selector, i);
|
|
83708
|
+
refocusTapIndexesBySelector.set(current.selector, []);
|
|
83709
|
+
}
|
|
83710
|
+
if (toDrop.size === 0)
|
|
83711
|
+
return actions;
|
|
83712
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83713
|
+
}
|
|
83714
|
+
function coalesceRepeatedTaps(actions) {
|
|
83715
|
+
if (actions.length <= 1)
|
|
83716
|
+
return actions;
|
|
83717
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83718
|
+
const lastTapIndexBySelector = /* @__PURE__ */ new Map();
|
|
83719
|
+
for (let i = 0; i < actions.length; i++) {
|
|
83720
|
+
const action = actions[i];
|
|
83721
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT") {
|
|
83722
|
+
lastTapIndexBySelector.clear();
|
|
83723
|
+
continue;
|
|
83724
|
+
}
|
|
83725
|
+
if (isDurableMutation(action)) {
|
|
83726
|
+
lastTapIndexBySelector.clear();
|
|
83727
|
+
continue;
|
|
83728
|
+
}
|
|
83729
|
+
if (action.type !== "TAP" || !action.selector)
|
|
83730
|
+
continue;
|
|
83731
|
+
const key = action.metadata.structuralKey;
|
|
83732
|
+
const priorIndex = lastTapIndexBySelector.get(action.selector);
|
|
83733
|
+
if (priorIndex !== void 0 && key && actions[priorIndex].metadata.structuralKey === key) {
|
|
83734
|
+
const priorHash = actions[priorIndex].metadata.screenHash;
|
|
83735
|
+
const currentHash = action.metadata.screenHash;
|
|
83736
|
+
const screenAdvanced = !!priorHash && !!currentHash && priorHash !== currentHash;
|
|
83737
|
+
if (!screenAdvanced) {
|
|
83738
|
+
toDrop.add(priorIndex);
|
|
83739
|
+
}
|
|
83740
|
+
}
|
|
83741
|
+
lastTapIndexBySelector.set(action.selector, i);
|
|
83742
|
+
}
|
|
83743
|
+
if (toDrop.size === 0)
|
|
83744
|
+
return actions;
|
|
83745
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83746
|
+
}
|
|
83747
|
+
var MAX_DETOUR_LENGTH = 6;
|
|
83748
|
+
function elideNoOpDetours(actions) {
|
|
83749
|
+
if (actions.length < 2)
|
|
83750
|
+
return actions;
|
|
83751
|
+
const toDrop = /* @__PURE__ */ new Set();
|
|
83752
|
+
for (let j = 0; j < actions.length; j++) {
|
|
83753
|
+
if (toDrop.has(j))
|
|
83754
|
+
continue;
|
|
83755
|
+
if (actions[j].type !== "BACK" && actions[j].type !== "HOME")
|
|
83756
|
+
continue;
|
|
83757
|
+
const keyAfter = actions[j].metadata.structuralKey;
|
|
83758
|
+
if (!keyAfter)
|
|
83759
|
+
continue;
|
|
83760
|
+
let bestStart = null;
|
|
83761
|
+
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83762
|
+
for (let i = j; i >= earliestStart; i--) {
|
|
83763
|
+
if (i < j) {
|
|
83764
|
+
const inside = actions[i];
|
|
83765
|
+
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83766
|
+
break;
|
|
83767
|
+
}
|
|
83768
|
+
}
|
|
83769
|
+
if (i === 0)
|
|
83770
|
+
continue;
|
|
83771
|
+
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83772
|
+
if (i < j && keyBefore && keyBefore === keyAfter) {
|
|
83773
|
+
bestStart = i;
|
|
83774
|
+
}
|
|
83775
|
+
}
|
|
83776
|
+
if (bestStart !== null) {
|
|
83777
|
+
for (let k = bestStart; k <= j; k++)
|
|
83778
|
+
toDrop.add(k);
|
|
83779
|
+
}
|
|
83780
|
+
}
|
|
83781
|
+
if (toDrop.size === 0)
|
|
83782
|
+
return actions;
|
|
83783
|
+
return actions.filter((_, index) => !toDrop.has(index));
|
|
83784
|
+
}
|
|
83366
83785
|
function mobileActionsToPseudoPlaywright(actions) {
|
|
83367
83786
|
const escape = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
83368
83787
|
const lines = ["test('mobile', async ({ page }) => {"];
|
|
@@ -83450,6 +83869,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83450
83869
|
if (actions.length < beforeCycleCollapse) {
|
|
83451
83870
|
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
83452
83871
|
}
|
|
83872
|
+
const beforeTypeCoalesce = actions;
|
|
83873
|
+
actions = coalesceSameFieldTypes(actions);
|
|
83874
|
+
logWanderDiff("coalesceSameFieldTypes", beforeTypeCoalesce, actions, log2);
|
|
83875
|
+
const beforeTapCoalesce = actions;
|
|
83876
|
+
actions = coalesceRepeatedTaps(actions);
|
|
83877
|
+
logWanderDiff("coalesceRepeatedTaps", beforeTapCoalesce, actions, log2);
|
|
83878
|
+
const beforeDetourElision = actions;
|
|
83879
|
+
actions = elideNoOpDetours(actions);
|
|
83880
|
+
logWanderDiff("elideNoOpDetours", beforeDetourElision, actions, log2);
|
|
83453
83881
|
const beforeContainerDrop = actions.length;
|
|
83454
83882
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
83455
83883
|
if (actions.length < beforeContainerDrop) {
|
|
@@ -83634,48 +84062,29 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83634
84062
|
log2(" app data reset skipped by policy \u2014 scenario records after relaunch with preserved data.");
|
|
83635
84063
|
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: false, coldResetUnavailable: false, refuseGenerate: false, reason: "preserve_policy" }, log2);
|
|
83636
84064
|
}
|
|
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);
|
|
84065
|
+
const appDataCleared = await driver.resetAppState?.().catch((err) => {
|
|
84066
|
+
log2(` app-data reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
84067
|
+
return false;
|
|
84068
|
+
}) ?? false;
|
|
84069
|
+
if (appDataCleared) {
|
|
84070
|
+
return applyScenarioEntryDeepLink(driver, target, { appDataCleared: true, coldResetUnavailable: false, refuseGenerate: false }, log2);
|
|
83662
84071
|
}
|
|
83663
84072
|
await driver.relaunch();
|
|
83664
84073
|
if (resetPolicy === "CLEAR_APP_DATA") {
|
|
83665
|
-
log2(" CLEAR_APP_DATA required but iOS
|
|
84074
|
+
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
84075
|
return {
|
|
83667
84076
|
appDataCleared: false,
|
|
83668
84077
|
coldResetUnavailable: true,
|
|
83669
84078
|
refuseGenerate: true,
|
|
83670
|
-
reason: "ios_clear_app_data_unavailable"
|
|
84079
|
+
reason: platform3 === "IOS" ? "ios_clear_app_data_unavailable" : "clear_app_data_unavailable"
|
|
83671
84080
|
};
|
|
83672
84081
|
}
|
|
83673
|
-
log2(" iOS
|
|
84082
|
+
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
84083
|
return applyScenarioEntryDeepLink(driver, target, {
|
|
83675
84084
|
appDataCleared: false,
|
|
83676
84085
|
coldResetUnavailable: true,
|
|
83677
84086
|
refuseGenerate: false,
|
|
83678
|
-
reason: "ios_no_clear_app"
|
|
84087
|
+
reason: platform3 === "IOS" ? "ios_no_clear_app" : platform3 === "ANDROID" ? "android_reset_unavailable" : "reset_unavailable"
|
|
83679
84088
|
}, log2);
|
|
83680
84089
|
}
|
|
83681
84090
|
async function runMobileGeneratePhase(args) {
|
|
@@ -84278,6 +84687,8 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
84278
84687
|
"use strict";
|
|
84279
84688
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
84280
84689
|
exports.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = void 0;
|
|
84690
|
+
exports.timed = timed;
|
|
84691
|
+
exports.timedSync = timedSync;
|
|
84281
84692
|
exports.executeMobileScriptDrivenHeal = executeMobileScriptDrivenHeal;
|
|
84282
84693
|
exports.executeMobileTestWithHealing = executeMobileTestWithHealing2;
|
|
84283
84694
|
exports.executeMobileLiveHeal = executeMobileLiveHeal2;
|
|
@@ -84487,6 +84898,32 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84487
84898
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
84488
84899
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
84489
84900
|
}
|
|
84901
|
+
function formatHealTimingLine(category, label, ms, failed, errMessage) {
|
|
84902
|
+
const base2 = `\u{1FA7A}\u23F1 [${category}] ${label} \u2014 ${ms}ms`;
|
|
84903
|
+
return failed ? `${base2} (FAILED: ${errMessage ?? "unknown error"})` : base2;
|
|
84904
|
+
}
|
|
84905
|
+
async function timed(category, label, onProgress, fn) {
|
|
84906
|
+
const start = Date.now();
|
|
84907
|
+
try {
|
|
84908
|
+
const result = await fn();
|
|
84909
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84910
|
+
return result;
|
|
84911
|
+
} catch (err) {
|
|
84912
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84913
|
+
throw err;
|
|
84914
|
+
}
|
|
84915
|
+
}
|
|
84916
|
+
function timedSync(category, label, onProgress, fn) {
|
|
84917
|
+
const start = Date.now();
|
|
84918
|
+
try {
|
|
84919
|
+
const result = fn();
|
|
84920
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, false));
|
|
84921
|
+
return result;
|
|
84922
|
+
} catch (err) {
|
|
84923
|
+
onProgress?.(formatHealTimingLine(category, label, Date.now() - start, true, err instanceof Error ? err.message : String(err)));
|
|
84924
|
+
throw err;
|
|
84925
|
+
}
|
|
84926
|
+
}
|
|
84490
84927
|
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
84491
84928
|
function isIdentityEqualityXpath(value) {
|
|
84492
84929
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
@@ -84704,21 +85141,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
84704
85141
|
return JSON.stringify(base2?.target) !== JSON.stringify(healed.target);
|
|
84705
85142
|
}
|
|
84706
85143
|
function findMissingChangedLocators(baseSteps, healedSteps, screen) {
|
|
84707
|
-
const
|
|
85144
|
+
const results = [];
|
|
84708
85145
|
healedSteps.forEach((healed, index) => {
|
|
84709
85146
|
if (!stepTargetChanged(baseSteps[index], healed))
|
|
84710
85147
|
return;
|
|
84711
85148
|
const candidate = stepToLocatorCandidate(healed);
|
|
84712
85149
|
if (!candidate)
|
|
84713
85150
|
return;
|
|
84714
|
-
|
|
84715
|
-
|
|
84716
|
-
|
|
84717
|
-
|
|
84718
|
-
});
|
|
85151
|
+
const matchCount = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen).length;
|
|
85152
|
+
const label = healed.target?.primary ?? JSON.stringify(candidate);
|
|
85153
|
+
const expressions = stepIdentityExpressions(healed);
|
|
85154
|
+
if (matchCount === 0) {
|
|
85155
|
+
results.push({ label, expressions, kind: "missing" });
|
|
85156
|
+
} else if (matchCount > 1) {
|
|
85157
|
+
results.push({ label, expressions, kind: "ambiguous", matchCount });
|
|
84719
85158
|
}
|
|
84720
85159
|
});
|
|
84721
|
-
return
|
|
85160
|
+
return results;
|
|
84722
85161
|
}
|
|
84723
85162
|
function changedStepExpressions(baseSteps, healedSteps) {
|
|
84724
85163
|
const out = [];
|
|
@@ -84947,10 +85386,10 @@ ${args.directive}`);
|
|
|
84947
85386
|
let content;
|
|
84948
85387
|
let finishReason;
|
|
84949
85388
|
try {
|
|
84950
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
85389
|
+
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
85390
|
arm();
|
|
84952
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
84953
|
-
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
85391
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85392
|
+
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true }));
|
|
84954
85393
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84955
85394
|
finishReason = response?.choices?.[0]?.finish_reason ?? void 0;
|
|
84956
85395
|
} catch {
|
|
@@ -84993,14 +85432,18 @@ ${args.directive}`);
|
|
|
84993
85432
|
continue;
|
|
84994
85433
|
}
|
|
84995
85434
|
if (activeFailingSource && isLocatorNotFound(activeError) && !isDrift(activeError)) {
|
|
84996
|
-
const
|
|
84997
|
-
const missing =
|
|
85435
|
+
const failingSourceSnapshot = activeFailingSource;
|
|
85436
|
+
const missing = timedSync("device-op", `phase1-attempt-${attempt}-static-verify`, options.onProgress, () => {
|
|
85437
|
+
const screen = (0, mobile_source_parser_js_1.parseMobileSource)(failingSourceSnapshot, platform3);
|
|
85438
|
+
return findMissingChangedLocators(baseSteps, healedSteps, screen);
|
|
85439
|
+
});
|
|
84998
85440
|
if (missing.length > 0) {
|
|
84999
85441
|
for (const entry of missing)
|
|
85000
85442
|
pushUnique(triedLocators, entry.expressions);
|
|
85001
85443
|
markRecoverableTurn();
|
|
85002
85444
|
lastVerdict = "fix";
|
|
85003
|
-
|
|
85445
|
+
const first = missing[0];
|
|
85446
|
+
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
85447
|
continue;
|
|
85005
85448
|
}
|
|
85006
85449
|
}
|
|
@@ -85014,9 +85457,10 @@ ${args.directive}`);
|
|
|
85014
85457
|
pendingDirective = "The proposed fix could not be device-verified here. If it is not clearly correct, revise it.";
|
|
85015
85458
|
continue;
|
|
85016
85459
|
}
|
|
85460
|
+
const runNativeTest = options.runNativeTest;
|
|
85017
85461
|
let verify;
|
|
85018
85462
|
try {
|
|
85019
|
-
verify = await options.runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables });
|
|
85463
|
+
verify = await timed("reexec", `phase1-attempt-${attempt}-device-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables }));
|
|
85020
85464
|
} catch {
|
|
85021
85465
|
return {
|
|
85022
85466
|
passed: false,
|
|
@@ -85123,6 +85567,9 @@ ${args.directive}`);
|
|
|
85123
85567
|
}
|
|
85124
85568
|
}
|
|
85125
85569
|
async function executeMobileTestWithHealing2(steps, target, ctx, options) {
|
|
85570
|
+
return timed("total", "heal-total", options.onProgress, () => executeMobileTestWithHealingImpl(steps, target, ctx, options));
|
|
85571
|
+
}
|
|
85572
|
+
async function executeMobileTestWithHealingImpl(steps, target, ctx, options) {
|
|
85126
85573
|
const invalidAuthSetup = detectInvalidMobileAuthSetup(steps, ctx.error);
|
|
85127
85574
|
if (invalidAuthSetup) {
|
|
85128
85575
|
return { passed: false, verdict: "test_invalid", healReason: invalidAuthSetup };
|
|
@@ -85249,9 +85696,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85249
85696
|
const candidate = partsToCandidate(parts);
|
|
85250
85697
|
if (!hasIdentitySignal(candidate))
|
|
85251
85698
|
return;
|
|
85252
|
-
const
|
|
85253
|
-
|
|
85254
|
-
|
|
85699
|
+
const matches = (0, mobile_source_parser_js_1.resolveLocatorAgainstScreen)(candidate, screen);
|
|
85700
|
+
const target = matches.find((el) => el.bounds.width > 0 && el.bounds.height > 0) ?? matches[0];
|
|
85701
|
+
if (target)
|
|
85702
|
+
await driver.tap(target.bounds);
|
|
85255
85703
|
}
|
|
85256
85704
|
function buildLiveUserContent(args) {
|
|
85257
85705
|
const parts = [];
|
|
@@ -85316,7 +85764,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85316
85764
|
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
85317
85765
|
let snap;
|
|
85318
85766
|
try {
|
|
85319
|
-
snap = await liveDriver.snapshot();
|
|
85767
|
+
snap = await timed("device-op", `phase2-pass-${pass}-snapshot`, options.onProgress, () => liveDriver.snapshot());
|
|
85320
85768
|
} catch {
|
|
85321
85769
|
return {
|
|
85322
85770
|
passed: false,
|
|
@@ -85342,10 +85790,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85342
85790
|
];
|
|
85343
85791
|
let content;
|
|
85344
85792
|
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)(() => {
|
|
85793
|
+
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
85794
|
arm();
|
|
85347
|
-
return aiClient.chat.completions.create({ model, messages, max_tokens:
|
|
85348
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
85795
|
+
return aiClient.chat.completions.create({ model, messages, max_tokens: 16e3, temperature: 0.1, ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(model) }, { signal });
|
|
85796
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true })));
|
|
85349
85797
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
85350
85798
|
} catch {
|
|
85351
85799
|
continue;
|
|
@@ -85377,7 +85825,7 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
85377
85825
|
const healedSteps = parsed.steps;
|
|
85378
85826
|
let verify;
|
|
85379
85827
|
try {
|
|
85380
|
-
verify = await runNativeTest({ steps: healedSteps, target, testVariables });
|
|
85828
|
+
verify = await timed("reexec", `phase2-pass-${pass}-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables }));
|
|
85381
85829
|
} catch {
|
|
85382
85830
|
return {
|
|
85383
85831
|
passed: false,
|
|
@@ -89231,7 +89679,7 @@ var require_kill_tree = __commonJS({
|
|
|
89231
89679
|
var node_child_process_1 = __require("child_process");
|
|
89232
89680
|
var node_util_1 = __require("util");
|
|
89233
89681
|
var node_fs_1 = __require("fs");
|
|
89234
|
-
var
|
|
89682
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
89235
89683
|
var PS_TIMEOUT_MS = 2e3;
|
|
89236
89684
|
var PS_BUFFER = 1024 * 1024;
|
|
89237
89685
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -89285,7 +89733,7 @@ var require_kill_tree = __commonJS({
|
|
|
89285
89733
|
timer.unref?.();
|
|
89286
89734
|
let stdout;
|
|
89287
89735
|
try {
|
|
89288
|
-
const result = await
|
|
89736
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89289
89737
|
maxBuffer: PS_BUFFER,
|
|
89290
89738
|
signal: controller.signal
|
|
89291
89739
|
});
|
|
@@ -89323,7 +89771,7 @@ var require_kill_tree = __commonJS({
|
|
|
89323
89771
|
timer.unref?.();
|
|
89324
89772
|
let stdout;
|
|
89325
89773
|
try {
|
|
89326
|
-
const result = await
|
|
89774
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
89327
89775
|
maxBuffer: PS_BUFFER,
|
|
89328
89776
|
signal: controller.signal
|
|
89329
89777
|
});
|
|
@@ -89400,7 +89848,7 @@ var require_kill_tree = __commonJS({
|
|
|
89400
89848
|
timer.unref?.();
|
|
89401
89849
|
let stdout;
|
|
89402
89850
|
try {
|
|
89403
|
-
const result = await
|
|
89851
|
+
const result = await execFileAsync4("ps", ["-A", "-o", "pid=,ppid="], {
|
|
89404
89852
|
maxBuffer: PS_BUFFER,
|
|
89405
89853
|
signal: controller.signal
|
|
89406
89854
|
});
|
|
@@ -99279,8 +99727,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99279
99727
|
onRetry: (attempt, reason, delayMs) => logs2.push(` \u26A0 scenario-gen AI call retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
99280
99728
|
});
|
|
99281
99729
|
} catch (aiErr) {
|
|
99282
|
-
const
|
|
99283
|
-
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${
|
|
99730
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
99731
|
+
logs2.push(` AI call failed at iteration ${iteration} (after retries): ${errMsg3}`);
|
|
99284
99732
|
throw (0, ai_retry_js_1.markAIProviderError)(aiErr);
|
|
99285
99733
|
}
|
|
99286
99734
|
const iterDuration = Date.now() - iterStart;
|
|
@@ -99323,8 +99771,8 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
99323
99771
|
try {
|
|
99324
99772
|
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
99325
99773
|
} catch (parseErr) {
|
|
99326
|
-
const
|
|
99327
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content:
|
|
99774
|
+
const errMsg3 = `Invalid tool arguments: ${parseErr.message}`;
|
|
99775
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: errMsg3 });
|
|
99328
99776
|
continue;
|
|
99329
99777
|
}
|
|
99330
99778
|
if (toolName === "finish_generate") {
|
|
@@ -100582,7 +101030,7 @@ var require_util4 = __commonJS({
|
|
|
100582
101030
|
}
|
|
100583
101031
|
path = url.path;
|
|
100584
101032
|
}
|
|
100585
|
-
var
|
|
101033
|
+
var isAbsolute2 = exports.isAbsolute(path);
|
|
100586
101034
|
var parts = path.split(/\/+/);
|
|
100587
101035
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
100588
101036
|
part = parts[i];
|
|
@@ -100602,7 +101050,7 @@ var require_util4 = __commonJS({
|
|
|
100602
101050
|
}
|
|
100603
101051
|
path = parts.join("/");
|
|
100604
101052
|
if (path === "") {
|
|
100605
|
-
path =
|
|
101053
|
+
path = isAbsolute2 ? "/" : ".";
|
|
100606
101054
|
}
|
|
100607
101055
|
if (url) {
|
|
100608
101056
|
url.path = path;
|
|
@@ -100611,7 +101059,7 @@ var require_util4 = __commonJS({
|
|
|
100611
101059
|
return path;
|
|
100612
101060
|
}
|
|
100613
101061
|
exports.normalize = normalize;
|
|
100614
|
-
function
|
|
101062
|
+
function join8(aRoot, aPath) {
|
|
100615
101063
|
if (aRoot === "") {
|
|
100616
101064
|
aRoot = ".";
|
|
100617
101065
|
}
|
|
@@ -100643,7 +101091,7 @@ var require_util4 = __commonJS({
|
|
|
100643
101091
|
}
|
|
100644
101092
|
return joined;
|
|
100645
101093
|
}
|
|
100646
|
-
exports.join =
|
|
101094
|
+
exports.join = join8;
|
|
100647
101095
|
exports.isAbsolute = function(aPath) {
|
|
100648
101096
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
100649
101097
|
};
|
|
@@ -100816,7 +101264,7 @@ var require_util4 = __commonJS({
|
|
|
100816
101264
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
100817
101265
|
}
|
|
100818
101266
|
}
|
|
100819
|
-
sourceURL =
|
|
101267
|
+
sourceURL = join8(urlGenerate(parsed), sourceURL);
|
|
100820
101268
|
}
|
|
100821
101269
|
return normalize(sourceURL);
|
|
100822
101270
|
}
|
|
@@ -314529,8 +314977,8 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314529
314977
|
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel)
|
|
314530
314978
|
})), { label: `script-heal-${attempt}` });
|
|
314531
314979
|
} catch (aiErr) {
|
|
314532
|
-
const
|
|
314533
|
-
logs2.push(`\u26A0 AI call failed: ${
|
|
314980
|
+
const errMsg3 = aiErr instanceof Error ? aiErr.message : String(aiErr);
|
|
314981
|
+
logs2.push(`\u26A0 AI call failed: ${errMsg3}`);
|
|
314534
314982
|
attemptHistory.push({
|
|
314535
314983
|
phase: "script",
|
|
314536
314984
|
attempt,
|
|
@@ -314538,13 +314986,13 @@ Try a different fix approach. This is NOT your final script-driven attempt yet,
|
|
|
314538
314986
|
passed: false,
|
|
314539
314987
|
changedCode: false,
|
|
314540
314988
|
summary: "AI call failed before returning a heal result",
|
|
314541
|
-
errorSummary: truncateForPrompt(
|
|
314989
|
+
errorSummary: truncateForPrompt(errMsg3),
|
|
314542
314990
|
failedStep: summarizeFailedStep(currentStepResults)
|
|
314543
314991
|
});
|
|
314544
314992
|
if (feedbackEnabled) {
|
|
314545
|
-
lastTransportFailure = truncateForPrompt(
|
|
314993
|
+
lastTransportFailure = truncateForPrompt(errMsg3, 200) ?? "the AI request failed";
|
|
314546
314994
|
} else {
|
|
314547
|
-
currentError = `AI call failed on attempt ${attempt}: ${
|
|
314995
|
+
currentError = `AI call failed on attempt ${attempt}: ${errMsg3}`;
|
|
314548
314996
|
}
|
|
314549
314997
|
markRecoverableTurn();
|
|
314550
314998
|
continue;
|
|
@@ -314828,8 +315276,8 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314828
315276
|
// item 5b
|
|
314829
315277
|
});
|
|
314830
315278
|
} catch (runErr) {
|
|
314831
|
-
const
|
|
314832
|
-
logs2.push(`\u{1F4A5} Native execution threw: ${
|
|
315279
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
315280
|
+
logs2.push(`\u{1F4A5} Native execution threw: ${errMsg3}`);
|
|
314833
315281
|
attemptHistory.push({
|
|
314834
315282
|
phase: "script",
|
|
314835
315283
|
attempt,
|
|
@@ -314837,9 +315285,9 @@ ${currentError.slice(0, 2e3)}`
|
|
|
314837
315285
|
passed: false,
|
|
314838
315286
|
changedCode: true,
|
|
314839
315287
|
summary: truncateForPrompt(changeDesc) ?? "Applied code fix, but native execution threw before verification completed",
|
|
314840
|
-
errorSummary: truncateForPrompt(
|
|
315288
|
+
errorSummary: truncateForPrompt(errMsg3)
|
|
314841
315289
|
});
|
|
314842
|
-
currentError = `Native execution threw: ${
|
|
315290
|
+
currentError = `Native execution threw: ${errMsg3}`;
|
|
314843
315291
|
currentStepResults = [];
|
|
314844
315292
|
continue;
|
|
314845
315293
|
}
|
|
@@ -315595,9 +316043,9 @@ Last error (after heal attempts): ${lastErrorSnippet}` : `Heal aborted: same err
|
|
|
315595
316043
|
previousErrorSignature = errorSig;
|
|
315596
316044
|
}
|
|
315597
316045
|
} catch (runErr) {
|
|
315598
|
-
const
|
|
315599
|
-
logs2.push(`\u{1F4A5} Native run threw: ${
|
|
315600
|
-
currentError =
|
|
316046
|
+
const errMsg3 = runErr instanceof Error ? runErr.message : String(runErr);
|
|
316047
|
+
logs2.push(`\u{1F4A5} Native run threw: ${errMsg3}`);
|
|
316048
|
+
currentError = errMsg3;
|
|
315601
316049
|
currentStepResults = [];
|
|
315602
316050
|
}
|
|
315603
316051
|
} else {
|
|
@@ -316159,8 +316607,8 @@ When you call finish_fix, preserve locators only when they are stable structural
|
|
|
316159
316607
|
}
|
|
316160
316608
|
}
|
|
316161
316609
|
} catch (toolErr) {
|
|
316162
|
-
const
|
|
316163
|
-
let errorContent = `Error: ${
|
|
316610
|
+
const errMsg3 = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
316611
|
+
let errorContent = `Error: ${errMsg3}`;
|
|
316164
316612
|
try {
|
|
316165
316613
|
const consoleResult = await mcpClient.callTool({ name: "browser_console_messages", arguments: {} });
|
|
316166
316614
|
const consoleText = Array.isArray(consoleResult.content) ? consoleResult.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n") : String(consoleResult.content ?? "");
|
|
@@ -316181,8 +316629,8 @@ ${errorContent}`;
|
|
|
316181
316629
|
}
|
|
316182
316630
|
}
|
|
316183
316631
|
} catch (mcpErr) {
|
|
316184
|
-
const
|
|
316185
|
-
logs2.push(`\u{1F4A5} MCP exploration failed: ${
|
|
316632
|
+
const errMsg3 = mcpErr instanceof Error ? mcpErr.message : String(mcpErr);
|
|
316633
|
+
logs2.push(`\u{1F4A5} MCP exploration failed: ${errMsg3}`);
|
|
316186
316634
|
}
|
|
316187
316635
|
if (verdict === "app_bug") {
|
|
316188
316636
|
phase2AttemptHistory.push({
|
|
@@ -316776,9 +317224,9 @@ ${testBlocks}
|
|
|
316776
317224
|
}
|
|
316777
317225
|
emit(` \u2713 ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
316778
317226
|
} catch (err) {
|
|
316779
|
-
const
|
|
316780
|
-
toolResultContent = `ERROR: ${
|
|
316781
|
-
emit(` \u2717 ${
|
|
317227
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
317228
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
317229
|
+
emit(` \u2717 ${errMsg3}`);
|
|
316782
317230
|
}
|
|
316783
317231
|
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
|
|
316784
317232
|
}
|
|
@@ -323062,8 +323510,8 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
323062
323510
|
const snapItems = Array.isArray(snapResult.content) ? snapResult.content : [];
|
|
323063
323511
|
snapshotText = extractInlineSnapshotYaml(snapItems.find((c) => c.type === "text")?.text || null);
|
|
323064
323512
|
} catch (err) {
|
|
323065
|
-
const
|
|
323066
|
-
const isTimeout = /timeout|timed out|TimeoutError/i.test(
|
|
323513
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
323514
|
+
const isTimeout = /timeout|timed out|TimeoutError/i.test(errMsg3);
|
|
323067
323515
|
return {
|
|
323068
323516
|
snapshotText: null,
|
|
323069
323517
|
meta: {
|
|
@@ -327139,9 +327587,9 @@ ${currentSummary}`;
|
|
|
327139
327587
|
});
|
|
327140
327588
|
}
|
|
327141
327589
|
} catch (err) {
|
|
327142
|
-
const
|
|
327143
|
-
logs2.push(` Error persisting page: ${
|
|
327144
|
-
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${
|
|
327590
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327591
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327592
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: `ERROR persisting page: ${errMsg3}` });
|
|
327145
327593
|
}
|
|
327146
327594
|
continue;
|
|
327147
327595
|
}
|
|
@@ -327343,12 +327791,12 @@ ${currentSummary}`;
|
|
|
327343
327791
|
})
|
|
327344
327792
|
});
|
|
327345
327793
|
} catch (err) {
|
|
327346
|
-
const
|
|
327347
|
-
logs2.push(` Error persisting page: ${
|
|
327794
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327795
|
+
logs2.push(` Error persisting page: ${errMsg3}`);
|
|
327348
327796
|
toolResults.push({
|
|
327349
327797
|
role: "tool",
|
|
327350
327798
|
tool_call_id: toolCall.id,
|
|
327351
|
-
content: `ERROR persisting page: ${
|
|
327799
|
+
content: `ERROR persisting page: ${errMsg3}`
|
|
327352
327800
|
});
|
|
327353
327801
|
}
|
|
327354
327802
|
continue;
|
|
@@ -327383,12 +327831,12 @@ ${currentSummary}`;
|
|
|
327383
327831
|
content: JSON.stringify({ acknowledged: true })
|
|
327384
327832
|
});
|
|
327385
327833
|
} catch (err) {
|
|
327386
|
-
const
|
|
327387
|
-
logs2.push(` Error persisting transition: ${
|
|
327834
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
327835
|
+
logs2.push(` Error persisting transition: ${errMsg3}`);
|
|
327388
327836
|
toolResults.push({
|
|
327389
327837
|
role: "tool",
|
|
327390
327838
|
tool_call_id: toolCall.id,
|
|
327391
|
-
content: `ERROR persisting transition: ${
|
|
327839
|
+
content: `ERROR persisting transition: ${errMsg3}`
|
|
327392
327840
|
});
|
|
327393
327841
|
}
|
|
327394
327842
|
continue;
|
|
@@ -328084,15 +328532,15 @@ Exploration complete: ${summary}`);
|
|
|
328084
328532
|
toolResultContent = (0, snapshot_context_compactor_js_1.formatMCPToolResult)(toolResultContent, MAX_TOOL_RESULT_CHARS);
|
|
328085
328533
|
logs2.push(` OK: ${toolResultContent.slice(0, 200)}${toolResultContent.length > 200 ? "..." : ""}`);
|
|
328086
328534
|
} catch (err) {
|
|
328087
|
-
const
|
|
328088
|
-
toolResultContent = `ERROR: ${
|
|
328089
|
-
logs2.push(` Error: ${
|
|
328535
|
+
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
328536
|
+
toolResultContent = `ERROR: ${errMsg3}`;
|
|
328537
|
+
logs2.push(` Error: ${errMsg3}`);
|
|
328090
328538
|
if (useV2) {
|
|
328091
328539
|
(0, exploration_state_js_1.reduceEvent)(state2, {
|
|
328092
328540
|
type: "ACTION_FAILED",
|
|
328093
328541
|
route: state2.currentRoute,
|
|
328094
328542
|
action: `${toolName}(${JSON.stringify(toolArgs).slice(0, 100)})`,
|
|
328095
|
-
error:
|
|
328543
|
+
error: errMsg3
|
|
328096
328544
|
});
|
|
328097
328545
|
}
|
|
328098
328546
|
}
|
|
@@ -330941,10 +331389,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
330941
331389
|
onLog: options.onLog,
|
|
330942
331390
|
onProvenCommands: recordProvenCommands
|
|
330943
331391
|
});
|
|
330944
|
-
const
|
|
331392
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
330945
331393
|
const fixPrompt = [
|
|
330946
331394
|
"Your previous attempt did not produce a valid result file.",
|
|
330947
|
-
`Error: ${
|
|
331395
|
+
`Error: ${errMsg3}`,
|
|
330948
331396
|
`Re-read BRIEF.md and write a valid SurveyorOutput JSON to ${resultFilePath}.`,
|
|
330949
331397
|
"Single JSON object. No prose. Then exit."
|
|
330950
331398
|
].join("\n");
|
|
@@ -333085,7 +333533,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333085
333533
|
log2(`First-pass discovery-result invalid or missing: ${firstError.message}`);
|
|
333086
333534
|
}
|
|
333087
333535
|
if (!parsedResult) {
|
|
333088
|
-
const
|
|
333536
|
+
const errMsg3 = firstError?.message ?? (grokError instanceof Error ? grokError.message : "unknown error");
|
|
333089
333537
|
if (planDebounceTimerRef.current) {
|
|
333090
333538
|
clearTimeout(planDebounceTimerRef.current);
|
|
333091
333539
|
planDebounceTimerRef.current = void 0;
|
|
@@ -333110,7 +333558,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
333110
333558
|
""
|
|
333111
333559
|
].join("\n");
|
|
333112
333560
|
const fixPrompt = [
|
|
333113
|
-
`Your previous attempt did not produce a valid result file. Error: ${
|
|
333561
|
+
`Your previous attempt did not produce a valid result file. Error: ${errMsg3}`,
|
|
333114
333562
|
"Re-read BRIEF.md.",
|
|
333115
333563
|
phaseContext,
|
|
333116
333564
|
`Continue from where you left off \u2014 already verified tests are in ${testsDirPath}/`,
|
|
@@ -334739,10 +335187,10 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
334739
335187
|
onLog: parserLog,
|
|
334740
335188
|
onProvenCommands: recordProvenCommands
|
|
334741
335189
|
});
|
|
334742
|
-
const
|
|
335190
|
+
const errMsg3 = firstError?.message ?? (claudeError instanceof Error ? claudeError.message : "unknown error");
|
|
334743
335191
|
const fixPrompt = [
|
|
334744
335192
|
"Your previous attempt did not leave a valid result file.",
|
|
334745
|
-
`Error: ${
|
|
335193
|
+
`Error: ${errMsg3}`,
|
|
334746
335194
|
`Writing a valid SurveyorOutput JSON object to ${resultFilePath} is your ONLY priority now.`,
|
|
334747
335195
|
"Procedure: navigate to at most 6-8 of the main pages, then IMMEDIATELY use",
|
|
334748
335196
|
`the Write tool to save the SurveyorOutput JSON to ${resultFilePath}. Do this`,
|
|
@@ -338623,8 +339071,8 @@ var require_resource_governor = __commonJS({
|
|
|
338623
339071
|
function isDiscoveryLike(mode) {
|
|
338624
339072
|
return mode === "discovery";
|
|
338625
339073
|
}
|
|
338626
|
-
function evaluateRunAdmission2(options, request) {
|
|
338627
|
-
const snapshot = getResourceSnapshot2();
|
|
339074
|
+
function evaluateRunAdmission2(options, request, snapshotOverride) {
|
|
339075
|
+
const snapshot = snapshotOverride ?? getResourceSnapshot2();
|
|
338628
339076
|
const candidateWeight = modeWeight(request.mode, options.weights);
|
|
338629
339077
|
const currentUnits = activeUnits(request.activeByType, options.weights);
|
|
338630
339078
|
const memory = pressureFactor(snapshot.memoryPct, options.memorySoftPct, options.memoryHardPct);
|
|
@@ -341122,8 +341570,8 @@ var require_adm_zip = __commonJS({
|
|
|
341122
341570
|
return null;
|
|
341123
341571
|
}
|
|
341124
341572
|
function fixPath(zipPath) {
|
|
341125
|
-
const { join:
|
|
341126
|
-
return
|
|
341573
|
+
const { join: join8, normalize, sep } = pth.posix;
|
|
341574
|
+
return join8(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
|
|
341127
341575
|
}
|
|
341128
341576
|
function filenameFilter(filterfn) {
|
|
341129
341577
|
if (filterfn instanceof RegExp) {
|
|
@@ -343958,11 +344406,11 @@ var require_winchars = __commonJS({
|
|
|
343958
344406
|
var require_strip_absolute_path = __commonJS({
|
|
343959
344407
|
"../../node_modules/tar/lib/strip-absolute-path.js"(exports, module) {
|
|
343960
344408
|
"use strict";
|
|
343961
|
-
var { isAbsolute, parse } = __require("path").win32;
|
|
344409
|
+
var { isAbsolute: isAbsolute2, parse } = __require("path").win32;
|
|
343962
344410
|
module.exports = (path) => {
|
|
343963
344411
|
let r = "";
|
|
343964
344412
|
let parsed = parse(path);
|
|
343965
|
-
while (
|
|
344413
|
+
while (isAbsolute2(path) || parsed.root) {
|
|
343966
344414
|
const root = path.charAt(0) === "/" && path.slice(0, 4) !== "//?/" ? "/" : parsed.root;
|
|
343967
344415
|
path = path.slice(root.length);
|
|
343968
344416
|
r += root;
|
|
@@ -346474,7 +346922,7 @@ var require_update = __commonJS({
|
|
|
346474
346922
|
var require_opts_arg = __commonJS({
|
|
346475
346923
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports, module) {
|
|
346476
346924
|
"use strict";
|
|
346477
|
-
var { promisify:
|
|
346925
|
+
var { promisify: promisify4 } = __require("util");
|
|
346478
346926
|
var fs = __require("fs");
|
|
346479
346927
|
var optsArg = (opts) => {
|
|
346480
346928
|
if (!opts)
|
|
@@ -346488,9 +346936,9 @@ var require_opts_arg = __commonJS({
|
|
|
346488
346936
|
else
|
|
346489
346937
|
throw new TypeError("invalid options argument");
|
|
346490
346938
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
346491
|
-
opts.mkdirAsync =
|
|
346939
|
+
opts.mkdirAsync = promisify4(opts.mkdir);
|
|
346492
346940
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
346493
|
-
opts.statAsync =
|
|
346941
|
+
opts.statAsync = promisify4(opts.stat);
|
|
346494
346942
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
346495
346943
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
346496
346944
|
return opts;
|
|
@@ -346743,10 +347191,10 @@ var require_chownr = __commonJS({
|
|
|
346743
347191
|
}
|
|
346744
347192
|
} : (path2, uid, gid) => lchownSync(path2, uid, gid);
|
|
346745
347193
|
var nodeVersion = process.version;
|
|
346746
|
-
var
|
|
347194
|
+
var readdir3 = (path2, options, cb) => fs.readdir(path2, options, cb);
|
|
346747
347195
|
var readdirSync = (path2, options) => fs.readdirSync(path2, options);
|
|
346748
347196
|
if (/^v4\./.test(nodeVersion))
|
|
346749
|
-
|
|
347197
|
+
readdir3 = (path2, options, cb) => fs.readdir(path2, cb);
|
|
346750
347198
|
var chown = (cpath, uid, gid, cb) => {
|
|
346751
347199
|
fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, (er) => {
|
|
346752
347200
|
cb(er && er.code !== "ENOENT" ? er : null);
|
|
@@ -346773,7 +347221,7 @@ var require_chownr = __commonJS({
|
|
|
346773
347221
|
}
|
|
346774
347222
|
};
|
|
346775
347223
|
var chownr = (p, uid, gid, cb) => {
|
|
346776
|
-
|
|
347224
|
+
readdir3(p, { withFileTypes: true }, (er, children) => {
|
|
346777
347225
|
if (er) {
|
|
346778
347226
|
if (er.code === "ENOENT")
|
|
346779
347227
|
return cb();
|
|
@@ -347047,7 +347495,7 @@ var require_path_reservations = __commonJS({
|
|
|
347047
347495
|
var assert = __require("assert");
|
|
347048
347496
|
var normalize = require_normalize_unicode();
|
|
347049
347497
|
var stripSlashes = require_strip_trailing_slashes();
|
|
347050
|
-
var { join:
|
|
347498
|
+
var { join: join8 } = __require("path");
|
|
347051
347499
|
var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
|
|
347052
347500
|
var isWindows = platform3 === "win32";
|
|
347053
347501
|
module.exports = () => {
|
|
@@ -347056,7 +347504,7 @@ var require_path_reservations = __commonJS({
|
|
|
347056
347504
|
const getDirs = (path) => {
|
|
347057
347505
|
const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
|
|
347058
347506
|
if (set.length) {
|
|
347059
|
-
path2 =
|
|
347507
|
+
path2 = join8(set[set.length - 1], path2);
|
|
347060
347508
|
}
|
|
347061
347509
|
set.push(path2 || "/");
|
|
347062
347510
|
return set;
|
|
@@ -347124,7 +347572,7 @@ var require_path_reservations = __commonJS({
|
|
|
347124
347572
|
};
|
|
347125
347573
|
const reserve = (paths, fn) => {
|
|
347126
347574
|
paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
|
|
347127
|
-
return stripSlashes(
|
|
347575
|
+
return stripSlashes(join8(normalize(p))).toLowerCase();
|
|
347128
347576
|
});
|
|
347129
347577
|
const dirs = new Set(
|
|
347130
347578
|
paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
|
|
@@ -347179,7 +347627,7 @@ var require_unpack = __commonJS({
|
|
|
347179
347627
|
var fs = __require("fs");
|
|
347180
347628
|
var fsm = require_fs_minipass();
|
|
347181
347629
|
var path = __require("path");
|
|
347182
|
-
var
|
|
347630
|
+
var mkdir3 = require_mkdir();
|
|
347183
347631
|
var wc = require_winchars();
|
|
347184
347632
|
var pathReservations = require_path_reservations();
|
|
347185
347633
|
var stripAbsolutePath = require_strip_absolute_path();
|
|
@@ -347430,7 +347878,7 @@ var require_unpack = __commonJS({
|
|
|
347430
347878
|
}
|
|
347431
347879
|
}
|
|
347432
347880
|
[MKDIR](dir, mode, cb) {
|
|
347433
|
-
|
|
347881
|
+
mkdir3(normPath(dir), {
|
|
347434
347882
|
uid: this.uid,
|
|
347435
347883
|
gid: this.gid,
|
|
347436
347884
|
processUid: this.processUid,
|
|
@@ -347833,7 +348281,7 @@ var require_unpack = __commonJS({
|
|
|
347833
348281
|
}
|
|
347834
348282
|
[MKDIR](dir, mode) {
|
|
347835
348283
|
try {
|
|
347836
|
-
return
|
|
348284
|
+
return mkdir3.sync(normPath(dir), {
|
|
347837
348285
|
uid: this.uid,
|
|
347838
348286
|
gid: this.gid,
|
|
347839
348287
|
processUid: this.processUid,
|
|
@@ -351339,7 +351787,7 @@ var require_process_inventory = __commonJS({
|
|
|
351339
351787
|
var node_child_process_1 = __require("child_process");
|
|
351340
351788
|
var node_os_1 = __importDefault(__require("os"));
|
|
351341
351789
|
var node_util_1 = __require("util");
|
|
351342
|
-
var
|
|
351790
|
+
var execFileAsync4 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
351343
351791
|
var DEFAULT_LIMIT = 60;
|
|
351344
351792
|
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
351345
351793
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -351401,7 +351849,7 @@ var require_process_inventory = __commonJS({
|
|
|
351401
351849
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
351402
351850
|
timer.unref?.();
|
|
351403
351851
|
try {
|
|
351404
|
-
const { stdout } = await
|
|
351852
|
+
const { stdout } = await execFileAsync4(command, args, {
|
|
351405
351853
|
maxBuffer: 1024 * 1024,
|
|
351406
351854
|
signal: controller.signal
|
|
351407
351855
|
});
|
|
@@ -352547,7 +352995,7 @@ var require_package4 = __commonJS({
|
|
|
352547
352995
|
"package.json"(exports, module) {
|
|
352548
352996
|
module.exports = {
|
|
352549
352997
|
name: "@validate.qa/runner",
|
|
352550
|
-
version: "1.0.
|
|
352998
|
+
version: "1.0.71",
|
|
352551
352999
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
352552
353000
|
bin: {
|
|
352553
353001
|
"validate-runner": "dist/cli.js"
|
|
@@ -352556,7 +353004,7 @@ var require_package4 = __commonJS({
|
|
|
352556
353004
|
build: "tsup",
|
|
352557
353005
|
dev: "tsx src/cli.ts",
|
|
352558
353006
|
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",
|
|
353007
|
+
"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
353008
|
prepublishOnly: "npm run build"
|
|
352561
353009
|
},
|
|
352562
353010
|
files: [
|
|
@@ -352634,7 +353082,7 @@ function getConfigDir() {
|
|
|
352634
353082
|
}
|
|
352635
353083
|
|
|
352636
353084
|
// src/runner.ts
|
|
352637
|
-
import { join as
|
|
353085
|
+
import { join as join7 } from "path";
|
|
352638
353086
|
import { homedir as homedir3 } from "os";
|
|
352639
353087
|
import { createHash } from "crypto";
|
|
352640
353088
|
|
|
@@ -353229,6 +353677,69 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
353229
353677
|
};
|
|
353230
353678
|
}
|
|
353231
353679
|
|
|
353680
|
+
// src/services/mobile/ios-simulator-reset.ts
|
|
353681
|
+
import { execFile } from "child_process";
|
|
353682
|
+
import { promisify } from "util";
|
|
353683
|
+
import { mkdir, readdir, rm } from "fs/promises";
|
|
353684
|
+
import { join as join3, isAbsolute } from "path";
|
|
353685
|
+
var execFileAsync = promisify(execFile);
|
|
353686
|
+
var SIMCTL_TIMEOUT_MS = 15e3;
|
|
353687
|
+
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;
|
|
353688
|
+
function isIosSimulatorUdid(udid) {
|
|
353689
|
+
return typeof udid === "string" && SIMULATOR_UDID_RE.test(udid.trim());
|
|
353690
|
+
}
|
|
353691
|
+
function errMsg(err) {
|
|
353692
|
+
return err instanceof Error ? err.message : String(err);
|
|
353693
|
+
}
|
|
353694
|
+
async function simctl(args) {
|
|
353695
|
+
const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
|
|
353696
|
+
timeout: SIMCTL_TIMEOUT_MS,
|
|
353697
|
+
encoding: "utf-8"
|
|
353698
|
+
});
|
|
353699
|
+
return stdout.trim();
|
|
353700
|
+
}
|
|
353701
|
+
async function clearIosSimulatorAppData(udid, bundleId, env = process.env) {
|
|
353702
|
+
if (!isIosSimulatorUdid(udid)) {
|
|
353703
|
+
return { ok: false, detail: `'${udid}' is not a simulator UDID \u2014 physical iOS devices cannot be cleared` };
|
|
353704
|
+
}
|
|
353705
|
+
let container;
|
|
353706
|
+
try {
|
|
353707
|
+
container = await simctl(["get_app_container", udid, bundleId, "data"]);
|
|
353708
|
+
} catch (err) {
|
|
353709
|
+
return { ok: false, detail: `simctl get_app_container failed (app not installed?): ${errMsg(err)}` };
|
|
353710
|
+
}
|
|
353711
|
+
if (!isAbsolute(container) || !container.includes("/Containers/Data/Application/")) {
|
|
353712
|
+
return { ok: false, detail: `unexpected app container path: ${container}` };
|
|
353713
|
+
}
|
|
353714
|
+
try {
|
|
353715
|
+
await simctl(["spawn", udid, "defaults", "delete", bundleId]);
|
|
353716
|
+
} catch {
|
|
353717
|
+
}
|
|
353718
|
+
try {
|
|
353719
|
+
const entries = await readdir(container);
|
|
353720
|
+
for (const entry of entries) {
|
|
353721
|
+
await rm(join3(container, entry), { recursive: true, force: true });
|
|
353722
|
+
}
|
|
353723
|
+
for (const dir of ["Documents", "Library/Caches", "Library/Preferences", "tmp"]) {
|
|
353724
|
+
await mkdir(join3(container, dir), { recursive: true });
|
|
353725
|
+
}
|
|
353726
|
+
} catch (err) {
|
|
353727
|
+
return { ok: false, detail: `app container wipe failed: ${errMsg(err)}` };
|
|
353728
|
+
}
|
|
353729
|
+
const keychainPolicy = (env.VALIDATEQA_MOBILE_IOS_KEYCHAIN_RESET ?? "").trim().toLowerCase();
|
|
353730
|
+
if (keychainPolicy !== "never") {
|
|
353731
|
+
try {
|
|
353732
|
+
await simctl(["keychain", udid, "reset"]);
|
|
353733
|
+
} catch (err) {
|
|
353734
|
+
return {
|
|
353735
|
+
ok: false,
|
|
353736
|
+
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.`
|
|
353737
|
+
};
|
|
353738
|
+
}
|
|
353739
|
+
}
|
|
353740
|
+
return { ok: true };
|
|
353741
|
+
}
|
|
353742
|
+
|
|
353232
353743
|
// src/services/mobile/appium-actions.ts
|
|
353233
353744
|
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
353234
353745
|
var APPIUM_WEBVIEW_CONTEXT_TIMEOUT_MS = 12e4;
|
|
@@ -353411,9 +353922,12 @@ async function tapAt(sessionPath, bounds) {
|
|
|
353411
353922
|
}
|
|
353412
353923
|
}
|
|
353413
353924
|
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
353414
|
-
if (platform3 !== "ANDROID") return false;
|
|
353415
353925
|
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
353416
353926
|
if (policy === "never") return false;
|
|
353927
|
+
if (platform3 === "IOS") {
|
|
353928
|
+
return isIosSimulatorUdid(deviceId);
|
|
353929
|
+
}
|
|
353930
|
+
if (platform3 !== "ANDROID") return false;
|
|
353417
353931
|
if (policy === "always") return true;
|
|
353418
353932
|
return !!deviceId && deviceId.startsWith("emulator-");
|
|
353419
353933
|
}
|
|
@@ -354292,12 +354806,37 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354292
354806
|
});
|
|
354293
354807
|
}
|
|
354294
354808
|
|
|
354809
|
+
// src/services/device-quarantine.ts
|
|
354810
|
+
var quarantinedDevices = /* @__PURE__ */ new Set();
|
|
354811
|
+
var DeviceQuarantinedError = class extends Error {
|
|
354812
|
+
constructor(deviceId) {
|
|
354813
|
+
super(
|
|
354814
|
+
`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.`
|
|
354815
|
+
);
|
|
354816
|
+
this.name = "DeviceQuarantinedError";
|
|
354817
|
+
}
|
|
354818
|
+
};
|
|
354819
|
+
function quarantineDevice(deviceId) {
|
|
354820
|
+
quarantinedDevices.add(deviceId);
|
|
354821
|
+
}
|
|
354822
|
+
function unquarantineDevice(deviceId) {
|
|
354823
|
+
quarantinedDevices.delete(deviceId);
|
|
354824
|
+
}
|
|
354825
|
+
function isDeviceQuarantined(deviceId) {
|
|
354826
|
+
return typeof deviceId === "string" && quarantinedDevices.has(deviceId);
|
|
354827
|
+
}
|
|
354828
|
+
function assertDeviceNotQuarantined(deviceId) {
|
|
354829
|
+
if (isDeviceQuarantined(deviceId)) {
|
|
354830
|
+
throw new DeviceQuarantinedError(deviceId);
|
|
354831
|
+
}
|
|
354832
|
+
}
|
|
354833
|
+
|
|
354295
354834
|
// src/services/appium-executor.ts
|
|
354296
354835
|
var import_runner_core8 = __toESM(require_dist2());
|
|
354297
|
-
import { execFile as
|
|
354298
|
-
import { promisify as
|
|
354836
|
+
import { execFile as execFile3 } from "child_process";
|
|
354837
|
+
import { promisify as promisify3 } from "util";
|
|
354299
354838
|
import { tmpdir as tmpdir3 } from "os";
|
|
354300
|
-
import { join as
|
|
354839
|
+
import { join as join5 } from "path";
|
|
354301
354840
|
|
|
354302
354841
|
// src/services/device-discovery.ts
|
|
354303
354842
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -354604,9 +355143,6 @@ function countReadyDevicesByKind(platform3, snapshot = getCachedDeviceSnapshot()
|
|
|
354604
355143
|
function selectFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(), requirements = {}) {
|
|
354605
355144
|
const ready = snapshot.filter((d) => d.platform === platform3 && d.isAvailable && d.state === "BOOTED");
|
|
354606
355145
|
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
355146
|
let candidates = requirements.resetPolicy === "CLEAR_APP_DATA" ? ready.filter((d) => !d.isPhysical) : [...ready];
|
|
354611
355147
|
const appId = requirements.appId?.trim();
|
|
354612
355148
|
if (appId) {
|
|
@@ -354655,23 +355191,23 @@ function pickFreeDevice(platform3, leased, snapshot = getCachedDeviceSnapshot(),
|
|
|
354655
355191
|
|
|
354656
355192
|
// src/services/mobile/mobile-network-capture.ts
|
|
354657
355193
|
var import_runner_core7 = __toESM(require_dist2());
|
|
354658
|
-
import { execFile, execFileSync as execFileSync3 } from "child_process";
|
|
355194
|
+
import { execFile as execFile2, execFileSync as execFileSync3 } from "child_process";
|
|
354659
355195
|
import { randomUUID } from "crypto";
|
|
354660
|
-
import { mkdtemp, rm, writeFile, readFile, readdir, mkdir, unlink } from "fs/promises";
|
|
355196
|
+
import { mkdtemp, rm as rm2, writeFile, readFile, readdir as readdir2, mkdir as mkdir2, unlink } from "fs/promises";
|
|
354661
355197
|
import { tmpdir as tmpdir2, networkInterfaces } from "os";
|
|
354662
|
-
import { join as
|
|
354663
|
-
import { promisify } from "util";
|
|
355198
|
+
import { join as join4 } from "path";
|
|
355199
|
+
import { promisify as promisify2 } from "util";
|
|
354664
355200
|
import {
|
|
354665
355201
|
getLocal,
|
|
354666
355202
|
generateCACertificate
|
|
354667
355203
|
} from "mockttp";
|
|
354668
|
-
var
|
|
355204
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
354669
355205
|
var MAX_CALLS = 1e3;
|
|
354670
355206
|
var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
|
|
354671
355207
|
var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
354672
355208
|
var CA_KEY_BITS = 2048;
|
|
354673
355209
|
var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
|
|
354674
|
-
var PENDING_CLEANUP_DIR =
|
|
355210
|
+
var PENDING_CLEANUP_DIR = join4(tmpdir2(), "validateqa-mobile-capture-pending");
|
|
354675
355211
|
var IOS_SIM_HOST_PROXY_ENV = "VALIDATEQA_MOBILE_IOS_SIM_HOST_PROXY";
|
|
354676
355212
|
var STALE_MARKER_RECONCILE_MS = 6 * 60 * 60 * 1e3;
|
|
354677
355213
|
function envFlag(name) {
|
|
@@ -354853,7 +355389,7 @@ var MitmProxy = class {
|
|
|
354853
355389
|
try {
|
|
354854
355390
|
await server3.stop();
|
|
354855
355391
|
} catch (err) {
|
|
354856
|
-
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${
|
|
355392
|
+
this.onLog?.(`[mobile-net] proxy stop error (ignored): ${errMsg2(err)}`);
|
|
354857
355393
|
}
|
|
354858
355394
|
this.onLog?.(
|
|
354859
355395
|
`[mobile-net] proxy closed: ${this.calls.length} calls captured` + (this.dropped > 0 ? ` (${this.dropped} dropped over cap)` : "")
|
|
@@ -354878,7 +355414,7 @@ var MitmProxy = class {
|
|
|
354878
355414
|
isHttps
|
|
354879
355415
|
});
|
|
354880
355416
|
} catch (err) {
|
|
354881
|
-
this.onLog?.(`[mobile-net] request capture error (ignored): ${
|
|
355417
|
+
this.onLog?.(`[mobile-net] request capture error (ignored): ${errMsg2(err)}`);
|
|
354882
355418
|
}
|
|
354883
355419
|
}
|
|
354884
355420
|
// ── response → finalize the matched request ───────────────────────────────
|
|
@@ -354921,7 +355457,7 @@ var MitmProxy = class {
|
|
|
354921
355457
|
intercepted: true
|
|
354922
355458
|
});
|
|
354923
355459
|
} catch (err) {
|
|
354924
|
-
this.onLog?.(`[mobile-net] response capture error (ignored): ${
|
|
355460
|
+
this.onLog?.(`[mobile-net] response capture error (ignored): ${errMsg2(err)}`);
|
|
354925
355461
|
}
|
|
354926
355462
|
}
|
|
354927
355463
|
// ── tls-client-error → record WHY interception failed ─────────────────────
|
|
@@ -354998,17 +355534,17 @@ async function generateCaCert(dir, onLog) {
|
|
|
354998
355534
|
// rely on stop() to remove the cert from the device promptly after the run
|
|
354999
355535
|
// rather than on a short expiry.
|
|
355000
355536
|
});
|
|
355001
|
-
const caCertPath =
|
|
355537
|
+
const caCertPath = join4(dir, "validateqa-mobile-ca.pem");
|
|
355002
355538
|
await writeFile(caCertPath, cert, "utf-8");
|
|
355003
355539
|
return { caCertPath, caKeyPem: key, caCertPem: cert };
|
|
355004
355540
|
} catch (err) {
|
|
355005
|
-
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${
|
|
355541
|
+
onLog?.(`[mobile-net] CA generation failed; running plaintext-only: ${errMsg2(err)}`);
|
|
355006
355542
|
return null;
|
|
355007
355543
|
}
|
|
355008
355544
|
}
|
|
355009
355545
|
async function androidReadProxy(deviceId) {
|
|
355010
355546
|
try {
|
|
355011
|
-
const { stdout } = await
|
|
355547
|
+
const { stdout } = await execFileAsync2(
|
|
355012
355548
|
"adb",
|
|
355013
355549
|
["-s", deviceId, "shell", "settings", "get", "global", "http_proxy"],
|
|
355014
355550
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
@@ -355021,33 +355557,33 @@ async function androidReadProxy(deviceId) {
|
|
|
355021
355557
|
}
|
|
355022
355558
|
async function androidSetProxy(deviceId, hostPort, onLog) {
|
|
355023
355559
|
try {
|
|
355024
|
-
await
|
|
355560
|
+
await execFileAsync2(
|
|
355025
355561
|
"adb",
|
|
355026
355562
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", hostPort],
|
|
355027
355563
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355028
355564
|
);
|
|
355029
355565
|
return true;
|
|
355030
355566
|
} catch (err) {
|
|
355031
|
-
onLog?.(`[mobile-net] adb set http_proxy failed: ${
|
|
355567
|
+
onLog?.(`[mobile-net] adb set http_proxy failed: ${errMsg2(err)}`);
|
|
355032
355568
|
return false;
|
|
355033
355569
|
}
|
|
355034
355570
|
}
|
|
355035
355571
|
async function androidClearProxy(deviceId, originalProxy, onLog) {
|
|
355036
355572
|
const restoreValue = originalProxy && originalProxy.length > 0 ? originalProxy : ":0";
|
|
355037
355573
|
try {
|
|
355038
|
-
await
|
|
355574
|
+
await execFileAsync2(
|
|
355039
355575
|
"adb",
|
|
355040
355576
|
["-s", deviceId, "shell", "settings", "put", "global", "http_proxy", restoreValue],
|
|
355041
355577
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355042
355578
|
);
|
|
355043
355579
|
} catch (err) {
|
|
355044
|
-
onLog?.(`[mobile-net] adb restore http_proxy failed: ${
|
|
355580
|
+
onLog?.(`[mobile-net] adb restore http_proxy failed: ${errMsg2(err)}`);
|
|
355045
355581
|
}
|
|
355046
355582
|
}
|
|
355047
355583
|
async function androidInstallCa(deviceId, caCertPath) {
|
|
355048
355584
|
const devicePath = "/sdcard/validateqa-mobile-ca.pem";
|
|
355049
355585
|
try {
|
|
355050
|
-
await
|
|
355586
|
+
await execFileAsync2("adb", ["-s", deviceId, "push", caCertPath, devicePath], {
|
|
355051
355587
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355052
355588
|
});
|
|
355053
355589
|
return {
|
|
@@ -355055,23 +355591,23 @@ async function androidInstallCa(deviceId, caCertPath) {
|
|
|
355055
355591
|
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
355592
|
};
|
|
355057
355593
|
} catch (err) {
|
|
355058
|
-
return { installed: false, note: `CA push failed: ${
|
|
355594
|
+
return { installed: false, note: `CA push failed: ${errMsg2(err)}` };
|
|
355059
355595
|
}
|
|
355060
355596
|
}
|
|
355061
355597
|
async function androidRemoveCa(deviceId, onLog) {
|
|
355062
355598
|
try {
|
|
355063
|
-
await
|
|
355599
|
+
await execFileAsync2(
|
|
355064
355600
|
"adb",
|
|
355065
355601
|
["-s", deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"],
|
|
355066
355602
|
{ timeout: DEVICE_CMD_TIMEOUT_MS }
|
|
355067
355603
|
);
|
|
355068
355604
|
} catch (err) {
|
|
355069
|
-
onLog?.(`[mobile-net] adb remove CA failed: ${
|
|
355605
|
+
onLog?.(`[mobile-net] adb remove CA failed: ${errMsg2(err)}`);
|
|
355070
355606
|
}
|
|
355071
355607
|
}
|
|
355072
355608
|
async function iosSimTrustCa(deviceId, caCertPath) {
|
|
355073
355609
|
try {
|
|
355074
|
-
await
|
|
355610
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "add-root-cert", caCertPath], {
|
|
355075
355611
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355076
355612
|
});
|
|
355077
355613
|
return {
|
|
@@ -355079,18 +355615,18 @@ async function iosSimTrustCa(deviceId, caCertPath) {
|
|
|
355079
355615
|
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
355616
|
};
|
|
355081
355617
|
} catch (err) {
|
|
355082
|
-
return { trusted: false, note: `simctl add-root-cert failed: ${
|
|
355618
|
+
return { trusted: false, note: `simctl add-root-cert failed: ${errMsg2(err)}` };
|
|
355083
355619
|
}
|
|
355084
355620
|
}
|
|
355085
355621
|
async function iosSimRemoveCa(deviceId, onLog) {
|
|
355086
355622
|
if (process.env.VALIDATEQA_MOBILE_SIM_KEYCHAIN_RESET === "1") {
|
|
355087
355623
|
try {
|
|
355088
|
-
await
|
|
355624
|
+
await execFileAsync2("xcrun", ["simctl", "keychain", deviceId, "reset"], {
|
|
355089
355625
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355090
355626
|
});
|
|
355091
355627
|
onLog?.(`[mobile-net] simctl keychain reset run for ${deviceId} (opt-in; cleared ALL sim certs).`);
|
|
355092
355628
|
} catch (err) {
|
|
355093
|
-
onLog?.(`[mobile-net] simctl keychain reset failed: ${
|
|
355629
|
+
onLog?.(`[mobile-net] simctl keychain reset failed: ${errMsg2(err)}`);
|
|
355094
355630
|
}
|
|
355095
355631
|
return;
|
|
355096
355632
|
}
|
|
@@ -355098,7 +355634,7 @@ async function iosSimRemoveCa(deviceId, onLog) {
|
|
|
355098
355634
|
`[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
355635
|
);
|
|
355100
355636
|
}
|
|
355101
|
-
function
|
|
355637
|
+
function errMsg2(err) {
|
|
355102
355638
|
return err instanceof Error ? err.message : String(err);
|
|
355103
355639
|
}
|
|
355104
355640
|
function isProcessAlive(pid) {
|
|
@@ -355189,7 +355725,7 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355189
355725
|
let service;
|
|
355190
355726
|
let prior;
|
|
355191
355727
|
try {
|
|
355192
|
-
const { stdout: order } = await
|
|
355728
|
+
const { stdout: order } = await execFileAsync2("networksetup", ["-listnetworkserviceorder"], {
|
|
355193
355729
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
355194
355730
|
});
|
|
355195
355731
|
const svc = parsePrimaryNetworkService(order);
|
|
@@ -355198,26 +355734,26 @@ async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
|
355198
355734
|
}
|
|
355199
355735
|
service = svc;
|
|
355200
355736
|
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
355201
|
-
|
|
355202
|
-
|
|
355737
|
+
execFileAsync2("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
355738
|
+
execFileAsync2("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
355203
355739
|
]);
|
|
355204
355740
|
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
355205
355741
|
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
355206
|
-
await
|
|
355742
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355207
355743
|
}
|
|
355208
355744
|
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
355209
355745
|
return { configured: true, service, prior };
|
|
355210
355746
|
} catch (err) {
|
|
355211
355747
|
if (service && prior) {
|
|
355212
|
-
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${
|
|
355748
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg2(err)}`);
|
|
355213
355749
|
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
355214
355750
|
try {
|
|
355215
|
-
await
|
|
355751
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355216
355752
|
} catch {
|
|
355217
355753
|
}
|
|
355218
355754
|
}
|
|
355219
355755
|
}
|
|
355220
|
-
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${
|
|
355756
|
+
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg2(err)}` };
|
|
355221
355757
|
}
|
|
355222
355758
|
}
|
|
355223
355759
|
async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
@@ -355227,9 +355763,9 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
|
355227
355763
|
};
|
|
355228
355764
|
for (const args of buildHostProxyRestoreCommands(service, restore)) {
|
|
355229
355765
|
try {
|
|
355230
|
-
await
|
|
355766
|
+
await execFileAsync2("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
355231
355767
|
} catch (err) {
|
|
355232
|
-
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${
|
|
355768
|
+
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg2(err)}`);
|
|
355233
355769
|
}
|
|
355234
355770
|
}
|
|
355235
355771
|
onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
|
|
@@ -355237,11 +355773,11 @@ async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
|
355237
355773
|
var activeCleanups = /* @__PURE__ */ new Map();
|
|
355238
355774
|
var exitHandlersInstalled = false;
|
|
355239
355775
|
function markerPath(sessionKey) {
|
|
355240
|
-
return
|
|
355776
|
+
return join4(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
|
|
355241
355777
|
}
|
|
355242
355778
|
async function writePendingCleanup(sessionKey, marker) {
|
|
355243
355779
|
try {
|
|
355244
|
-
await
|
|
355780
|
+
await mkdir2(PENDING_CLEANUP_DIR, { recursive: true });
|
|
355245
355781
|
await writeFile(markerPath(sessionKey), JSON.stringify(marker), "utf-8");
|
|
355246
355782
|
} catch {
|
|
355247
355783
|
}
|
|
@@ -355281,7 +355817,7 @@ function restoreDeviceSync(marker, onLog) {
|
|
|
355281
355817
|
async function reconcilePendingCleanups(onLog) {
|
|
355282
355818
|
let files;
|
|
355283
355819
|
try {
|
|
355284
|
-
files = await
|
|
355820
|
+
files = await readdir2(PENDING_CLEANUP_DIR);
|
|
355285
355821
|
} catch {
|
|
355286
355822
|
return;
|
|
355287
355823
|
}
|
|
@@ -355289,7 +355825,7 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
355289
355825
|
if (!file.endsWith(".json")) continue;
|
|
355290
355826
|
const sessionKey = file.slice(0, -".json".length);
|
|
355291
355827
|
if (activeCleanups.has(sessionKey)) continue;
|
|
355292
|
-
const full =
|
|
355828
|
+
const full = join4(PENDING_CLEANUP_DIR, file);
|
|
355293
355829
|
try {
|
|
355294
355830
|
const raw = await readFile(full, "utf-8");
|
|
355295
355831
|
const marker = JSON.parse(raw);
|
|
@@ -355342,7 +355878,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355342
355878
|
let caKeyPem = null;
|
|
355343
355879
|
let caCertPem = null;
|
|
355344
355880
|
try {
|
|
355345
|
-
tmpDir = await mkdtemp(
|
|
355881
|
+
tmpDir = await mkdtemp(join4(tmpdir2(), "validateqa-mobile-ca-"));
|
|
355346
355882
|
const ca = await generateCaCert(tmpDir, onLog);
|
|
355347
355883
|
if (ca) {
|
|
355348
355884
|
caCertPath = ca.caCertPath;
|
|
@@ -355350,7 +355886,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355350
355886
|
caCertPem = ca.caCertPem;
|
|
355351
355887
|
}
|
|
355352
355888
|
} catch (err) {
|
|
355353
|
-
onLog?.(`[mobile-net] CA setup degraded: ${
|
|
355889
|
+
onLog?.(`[mobile-net] CA setup degraded: ${errMsg2(err)}`);
|
|
355354
355890
|
}
|
|
355355
355891
|
if (!caKeyPem || !caCertPem) {
|
|
355356
355892
|
try {
|
|
@@ -355358,9 +355894,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355358
355894
|
caKeyPem = fallback.key;
|
|
355359
355895
|
caCertPem = fallback.cert;
|
|
355360
355896
|
} catch (err) {
|
|
355361
|
-
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${
|
|
355897
|
+
onLog?.(`[mobile-net] fatal CA failure, cannot start proxy: ${errMsg2(err)}`);
|
|
355362
355898
|
if (tmpDir) {
|
|
355363
|
-
await
|
|
355899
|
+
await rm2(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
355364
355900
|
}
|
|
355365
355901
|
throw err instanceof Error ? err : new Error(String(err));
|
|
355366
355902
|
}
|
|
@@ -355415,7 +355951,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
355415
355951
|
}
|
|
355416
355952
|
}
|
|
355417
355953
|
} catch (err) {
|
|
355418
|
-
wiringReason = `device wiring degraded: ${
|
|
355954
|
+
wiringReason = `device wiring degraded: ${errMsg2(err)}`;
|
|
355419
355955
|
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
355420
355956
|
}
|
|
355421
355957
|
if (proxySet || caInstalledOnDevice || hostProxySet) {
|
|
@@ -355504,9 +356040,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
355504
356040
|
await proxy.close();
|
|
355505
356041
|
if (capturedTmpDir) {
|
|
355506
356042
|
try {
|
|
355507
|
-
await
|
|
356043
|
+
await rm2(capturedTmpDir, { recursive: true, force: true });
|
|
355508
356044
|
} catch (err) {
|
|
355509
|
-
onLog?.(`[mobile-net] temp CA cleanup failed: ${
|
|
356045
|
+
onLog?.(`[mobile-net] temp CA cleanup failed: ${errMsg2(err)}`);
|
|
355510
356046
|
}
|
|
355511
356047
|
}
|
|
355512
356048
|
const apiCount = finalCalls.filter((c) => c.isApi).length;
|
|
@@ -355539,11 +356075,11 @@ var MOBILE_NEW_COMMAND_TIMEOUT_SECONDS = (() => {
|
|
|
355539
356075
|
})();
|
|
355540
356076
|
var COLD_BOOT_ENTRY_TIMEOUT_MS = 3e4;
|
|
355541
356077
|
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
355542
|
-
var WD_SESSION_CREATE_TIMEOUT_MS =
|
|
356078
|
+
var WD_SESSION_CREATE_TIMEOUT_MS = Number(process.env.VALIDATEQA_WD_SESSION_CREATE_TIMEOUT_MS) || 3e5;
|
|
355543
356079
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
355544
356080
|
var APPIUM_DRIVER_DEAD_RE2 = /instrumentation process is not running|cannot be proxied to UiAutomator2 server|socket hang up|ECONNRESET/i;
|
|
355545
356081
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
355546
|
-
var
|
|
356082
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
355547
356083
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
355548
356084
|
var WebDriverTransportError = class extends Error {
|
|
355549
356085
|
isTransport = true;
|
|
@@ -355555,6 +356091,13 @@ var WebDriverTransportError = class extends Error {
|
|
|
355555
356091
|
function isTransportError(err) {
|
|
355556
356092
|
return err instanceof WebDriverTransportError || typeof err === "object" && err !== null && err.isTransport === true;
|
|
355557
356093
|
}
|
|
356094
|
+
var MobileAmbiguousMatchError = class extends Error {
|
|
356095
|
+
isAmbiguousMatch = true;
|
|
356096
|
+
constructor(message) {
|
|
356097
|
+
super(message);
|
|
356098
|
+
this.name = "MobileAmbiguousMatchError";
|
|
356099
|
+
}
|
|
356100
|
+
};
|
|
355558
356101
|
function crossPlatformRunReadinessError(steps, authoringPlatform, runPlatform) {
|
|
355559
356102
|
if (!authoringPlatform || authoringPlatform === runPlatform) return null;
|
|
355560
356103
|
const missing = stepsMissingPlatformLocators(steps, runPlatform);
|
|
@@ -355672,11 +356215,11 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
355672
356215
|
return { "appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200) };
|
|
355673
356216
|
}
|
|
355674
356217
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
355675
|
-
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() ||
|
|
356218
|
+
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join5(tmpdir3(), "validateqa-wda");
|
|
355676
356219
|
return {
|
|
355677
356220
|
"appium:wdaLocalPort": readPort("APPIUM_WDA_LOCAL_PORT", 8100),
|
|
355678
356221
|
"appium:mjpegServerPort": readPort("APPIUM_MJPEG_SERVER_PORT", 9100),
|
|
355679
|
-
"appium:derivedDataPath":
|
|
356222
|
+
"appium:derivedDataPath": join5(derivedDataRoot, safeDevice)
|
|
355680
356223
|
};
|
|
355681
356224
|
}
|
|
355682
356225
|
function isAndroidSystemPortBusyError(error2) {
|
|
@@ -355700,6 +356243,7 @@ function androidSystemPortForwardRemovalArgs(target, error2, env = process.env)
|
|
|
355700
356243
|
return ["-s", deviceId, "forward", "--remove", `tcp:${systemPort}`];
|
|
355701
356244
|
}
|
|
355702
356245
|
async function createSession(target) {
|
|
356246
|
+
assertDeviceNotQuarantined(target.deviceId);
|
|
355703
356247
|
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform) : {};
|
|
355704
356248
|
const parallelCaps = mobileParallelSessionCapabilities(
|
|
355705
356249
|
target.platform,
|
|
@@ -355721,6 +356265,13 @@ async function createSession(target) {
|
|
|
355721
356265
|
// whole survey. The runner owns session teardown explicitly, so a long
|
|
355722
356266
|
// idle window is safe. Env-overridable.
|
|
355723
356267
|
"appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
|
|
356268
|
+
// Xcode 27+ relocates Simulator.app out of <DEVELOPER_DIR>/Applications, so
|
|
356269
|
+
// XCUITest's default "open Simulator UI" step fails ("Simulator.app does not
|
|
356270
|
+
// exist") on this toolchain. The simulator is already booted and driven
|
|
356271
|
+
// headlessly via CoreSimulator, so tell XCUITest to skip opening the GUI.
|
|
356272
|
+
// Scoped to simulator UDIDs (a physical iOS device has no such step);
|
|
356273
|
+
// override with APPIUM_IOS_HEADLESS=false to force a visible Simulator run.
|
|
356274
|
+
...isIosSimulatorUdid(target.deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
355724
356275
|
...parallelCaps,
|
|
355725
356276
|
...realCaps
|
|
355726
356277
|
} : {
|
|
@@ -355760,7 +356311,7 @@ async function removeStaleAndroidSystemPortForward(target, error2) {
|
|
|
355760
356311
|
const args = androidSystemPortForwardRemovalArgs(target, error2);
|
|
355761
356312
|
if (!args) return false;
|
|
355762
356313
|
try {
|
|
355763
|
-
await
|
|
356314
|
+
await execFileAsync3("adb", args, { timeout: 1e4, windowsHide: true });
|
|
355764
356315
|
return true;
|
|
355765
356316
|
} catch {
|
|
355766
356317
|
return false;
|
|
@@ -355927,6 +356478,9 @@ async function deleteSession(sessionId) {
|
|
|
355927
356478
|
} catch {
|
|
355928
356479
|
}
|
|
355929
356480
|
}
|
|
356481
|
+
async function deleteAppiumSession(sessionId) {
|
|
356482
|
+
await deleteSession(sessionId);
|
|
356483
|
+
}
|
|
355930
356484
|
async function takeScreenshot(sessionId) {
|
|
355931
356485
|
try {
|
|
355932
356486
|
const result = await wdRequest(`/session/${sessionId}/screenshot`);
|
|
@@ -355998,10 +356552,9 @@ async function stopScreenRecording(sessionId, log2) {
|
|
|
355998
356552
|
return void 0;
|
|
355999
356553
|
}
|
|
356000
356554
|
}
|
|
356001
|
-
function
|
|
356002
|
-
|
|
356003
|
-
|
|
356004
|
-
const parsed = bounds;
|
|
356555
|
+
function parseMobileBoundsLike(value) {
|
|
356556
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.x === "number" && typeof value.y === "number" && typeof value.width === "number" && typeof value.height === "number") {
|
|
356557
|
+
const parsed = value;
|
|
356005
356558
|
if ([parsed.x, parsed.y, parsed.width, parsed.height].every(Number.isFinite)) {
|
|
356006
356559
|
return {
|
|
356007
356560
|
x: parsed.x,
|
|
@@ -356013,6 +356566,9 @@ function boundsFromStep(step) {
|
|
|
356013
356566
|
}
|
|
356014
356567
|
return null;
|
|
356015
356568
|
}
|
|
356569
|
+
function boundsFromStep(step) {
|
|
356570
|
+
return parseMobileBoundsLike(step.metadata?.bounds);
|
|
356571
|
+
}
|
|
356016
356572
|
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
356017
356573
|
if (isCrossPlatform) return null;
|
|
356018
356574
|
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
@@ -356164,6 +356720,131 @@ async function revealLikelyOffscreenElement(sessionId) {
|
|
|
356164
356720
|
});
|
|
356165
356721
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
356166
356722
|
}
|
|
356723
|
+
function strictMatchMode() {
|
|
356724
|
+
const raw = (process.env.VALIDATEQA_MOBILE_STRICT_MATCH ?? "").trim().toLowerCase();
|
|
356725
|
+
if (raw === "strict") return "strict";
|
|
356726
|
+
if (raw === "off") return "off";
|
|
356727
|
+
return "lenient";
|
|
356728
|
+
}
|
|
356729
|
+
var MOBILE_MAX_DISAMBIG_CANDIDATES = 20;
|
|
356730
|
+
var COINCIDENT_CENTER_EPSILON_PX = 2;
|
|
356731
|
+
function isRealElementRect(rect) {
|
|
356732
|
+
return rect.width > 1 && rect.height > 1;
|
|
356733
|
+
}
|
|
356734
|
+
function rectContainsPoint(rect, x, y) {
|
|
356735
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
356736
|
+
}
|
|
356737
|
+
function rectCenter(rect) {
|
|
356738
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
356739
|
+
}
|
|
356740
|
+
function centerDistance(rect, point) {
|
|
356741
|
+
const center = rectCenter(rect);
|
|
356742
|
+
return Math.hypot(center.x - point.x, center.y - point.y);
|
|
356743
|
+
}
|
|
356744
|
+
function chooseRecordedMatch(candidates, opts) {
|
|
356745
|
+
if (candidates.length === 0) return { kind: "ambiguous", count: 0 };
|
|
356746
|
+
if (candidates.length === 1) {
|
|
356747
|
+
return { kind: "matched", elementId: candidates[0].elementId, reason: "single" };
|
|
356748
|
+
}
|
|
356749
|
+
if (typeof opts.matchIndex === "number" && Number.isInteger(opts.matchIndex) && opts.matchIndex >= 0 && opts.matchIndex < candidates.length && typeof opts.expectedMatchCount === "number" && opts.expectedMatchCount === candidates.length) {
|
|
356750
|
+
return { kind: "matched", elementId: candidates[opts.matchIndex].elementId, reason: "matchIndex" };
|
|
356751
|
+
}
|
|
356752
|
+
{
|
|
356753
|
+
const withRect = candidates.filter(
|
|
356754
|
+
(candidate) => candidate.rect != null
|
|
356755
|
+
);
|
|
356756
|
+
if (withRect.length === candidates.length) {
|
|
356757
|
+
const real = withRect.filter((candidate) => isRealElementRect(candidate.rect));
|
|
356758
|
+
if (real.length === 1) {
|
|
356759
|
+
return { kind: "matched", elementId: real[0].elementId, reason: "visible" };
|
|
356760
|
+
}
|
|
356761
|
+
const centers = withRect.map((candidate) => rectCenter(candidate.rect));
|
|
356762
|
+
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);
|
|
356763
|
+
if (coincident) {
|
|
356764
|
+
return { kind: "matched", elementId: withRect[0].elementId, reason: "coincident" };
|
|
356765
|
+
}
|
|
356766
|
+
}
|
|
356767
|
+
}
|
|
356768
|
+
if (opts.recordedBounds) {
|
|
356769
|
+
const bounds = opts.recordedBounds;
|
|
356770
|
+
const center = rectCenter(bounds);
|
|
356771
|
+
const withRect = candidates.filter(
|
|
356772
|
+
(candidate) => candidate.rect != null
|
|
356773
|
+
);
|
|
356774
|
+
const containing = withRect.filter((candidate) => rectContainsPoint(candidate.rect, center.x, center.y));
|
|
356775
|
+
if (containing.length > 0) {
|
|
356776
|
+
const best = [...containing].sort(
|
|
356777
|
+
(a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height || centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356778
|
+
)[0];
|
|
356779
|
+
return { kind: "matched", elementId: best.elementId, reason: "bounds" };
|
|
356780
|
+
}
|
|
356781
|
+
if (withRect.length > 0) {
|
|
356782
|
+
const tolerance = Math.max(bounds.height, 1);
|
|
356783
|
+
const nearest = [...withRect].sort(
|
|
356784
|
+
(a, b) => centerDistance(a.rect, center) - centerDistance(b.rect, center)
|
|
356785
|
+
)[0];
|
|
356786
|
+
if (centerDistance(nearest.rect, center) <= tolerance) {
|
|
356787
|
+
return { kind: "matched", elementId: nearest.elementId, reason: "bounds" };
|
|
356788
|
+
}
|
|
356789
|
+
}
|
|
356790
|
+
}
|
|
356791
|
+
return opts.strict === "strict" ? { kind: "ambiguous", count: candidates.length } : { kind: "fallback", elementId: candidates[0].elementId, count: candidates.length };
|
|
356792
|
+
}
|
|
356793
|
+
function extractElementIdsFromPluralResponse(result) {
|
|
356794
|
+
const rawList = result?.value;
|
|
356795
|
+
if (!Array.isArray(rawList)) return [];
|
|
356796
|
+
const ids = [];
|
|
356797
|
+
for (const entry of rawList) {
|
|
356798
|
+
const record = entry;
|
|
356799
|
+
const id = validateElementId(record?.ELEMENT ?? record?.["element-6066-11e4-a52e-4f735466cecf"]);
|
|
356800
|
+
if (id) ids.push(id);
|
|
356801
|
+
}
|
|
356802
|
+
return ids;
|
|
356803
|
+
}
|
|
356804
|
+
async function findElements(sessionId, strategy) {
|
|
356805
|
+
const result = await wdRequest(`/session/${sessionId}/elements`, {
|
|
356806
|
+
method: "POST",
|
|
356807
|
+
body: JSON.stringify({ using: strategy.using, value: strategy.value })
|
|
356808
|
+
});
|
|
356809
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356810
|
+
}
|
|
356811
|
+
async function findWebViewElements(sessionPath, css) {
|
|
356812
|
+
const result = await wdRequest(`${sessionPath}/elements`, {
|
|
356813
|
+
method: "POST",
|
|
356814
|
+
body: JSON.stringify({ using: "css selector", value: css })
|
|
356815
|
+
});
|
|
356816
|
+
return extractElementIdsFromPluralResponse(result);
|
|
356817
|
+
}
|
|
356818
|
+
async function readElementRect(sessionId, elementId) {
|
|
356819
|
+
try {
|
|
356820
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/rect`);
|
|
356821
|
+
return parseMobileBoundsLike(result?.value);
|
|
356822
|
+
} catch (err) {
|
|
356823
|
+
if (isTransportError(err)) throw err;
|
|
356824
|
+
return null;
|
|
356825
|
+
}
|
|
356826
|
+
}
|
|
356827
|
+
async function disambiguateElementIds(sessionId, candidateIds, opts) {
|
|
356828
|
+
const indexOnly = candidateIds.map((elementId) => ({ elementId }));
|
|
356829
|
+
const indexResult = chooseRecordedMatch(indexOnly, {
|
|
356830
|
+
recordedBounds: null,
|
|
356831
|
+
matchIndex: opts.matchIndex,
|
|
356832
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356833
|
+
strict: opts.strict
|
|
356834
|
+
});
|
|
356835
|
+
if (indexResult.kind === "matched" || candidateIds.length > MOBILE_MAX_DISAMBIG_CANDIDATES) {
|
|
356836
|
+
return indexResult;
|
|
356837
|
+
}
|
|
356838
|
+
const withRects = await Promise.all(
|
|
356839
|
+
candidateIds.map(async (elementId) => ({ elementId, rect: await readElementRect(sessionId, elementId) }))
|
|
356840
|
+
);
|
|
356841
|
+
return chooseRecordedMatch(withRects, {
|
|
356842
|
+
recordedBounds: opts.recordedBounds ?? null,
|
|
356843
|
+
matchIndex: opts.matchIndex,
|
|
356844
|
+
expectedMatchCount: opts.expectedMatchCount,
|
|
356845
|
+
strict: opts.strict
|
|
356846
|
+
});
|
|
356847
|
+
}
|
|
356167
356848
|
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
356168
356849
|
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
356169
356850
|
if (strategies.length === 0) {
|
|
@@ -356171,22 +356852,45 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356171
356852
|
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
356853
|
);
|
|
356173
356854
|
}
|
|
356855
|
+
const mode = strictMatchMode();
|
|
356856
|
+
const matchIndex = typeof target?.matchIndex === "number" ? target.matchIndex : void 0;
|
|
356857
|
+
const expectedMatchCount = typeof target?.expectedMatchCount === "number" ? target.expectedMatchCount : void 0;
|
|
356174
356858
|
const startTime = Date.now();
|
|
356175
356859
|
let lastError = "";
|
|
356860
|
+
let lastAmbiguity = null;
|
|
356176
356861
|
let revealAttempted = false;
|
|
356177
356862
|
while (Date.now() - startTime < timeoutMs) {
|
|
356178
356863
|
for (const strategy of strategies) {
|
|
356179
356864
|
try {
|
|
356180
|
-
const
|
|
356181
|
-
|
|
356182
|
-
|
|
356865
|
+
const ids = await findElements(sessionId, strategy);
|
|
356866
|
+
if (ids.length === 0) continue;
|
|
356867
|
+
if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
356868
|
+
return ids[0];
|
|
356869
|
+
}
|
|
356870
|
+
const result = await disambiguateElementIds(sessionId, ids, {
|
|
356871
|
+
recordedBounds: opts?.recordedBounds,
|
|
356872
|
+
matchIndex,
|
|
356873
|
+
expectedMatchCount,
|
|
356874
|
+
strict: mode
|
|
356183
356875
|
});
|
|
356184
|
-
|
|
356185
|
-
|
|
356876
|
+
if (result.kind === "matched") return result.elementId;
|
|
356877
|
+
const shouldFailClosed = opts?.failClosedOnAmbiguity || result.kind === "ambiguous" && !opts?.tolerateAmbiguity;
|
|
356878
|
+
if (shouldFailClosed) {
|
|
356879
|
+
throw new MobileAmbiguousMatchError(
|
|
356880
|
+
`ambiguous locator: ${result.count} matches, cannot identify the recorded instance (strategy=${strategy.using} value=${strategy.value})`
|
|
356881
|
+
);
|
|
356882
|
+
}
|
|
356883
|
+
opts?.log?.(
|
|
356884
|
+
`\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
356885
|
);
|
|
356187
|
-
|
|
356886
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
356188
356887
|
} catch (err) {
|
|
356189
356888
|
if (isTransportError(err)) throw err;
|
|
356889
|
+
if (err instanceof MobileAmbiguousMatchError) {
|
|
356890
|
+
lastAmbiguity = err;
|
|
356891
|
+
lastError = err.message;
|
|
356892
|
+
continue;
|
|
356893
|
+
}
|
|
356190
356894
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356191
356895
|
}
|
|
356192
356896
|
}
|
|
@@ -356201,6 +356905,7 @@ async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_M
|
|
|
356201
356905
|
}
|
|
356202
356906
|
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
356203
356907
|
}
|
|
356908
|
+
if (lastAmbiguity) throw lastAmbiguity;
|
|
356204
356909
|
throw new Error(`Element not found after ${timeoutMs}ms. Tried ${strategies.length} strategies${revealAttempted ? " plus one reveal scroll" : ""}. Last error: ${lastError}`);
|
|
356205
356910
|
}
|
|
356206
356911
|
async function tapCoordinates(sessionId, bounds) {
|
|
@@ -356234,7 +356939,7 @@ async function prepareAndroidInputEnvironment(deviceId) {
|
|
|
356234
356939
|
];
|
|
356235
356940
|
for (const [namespace, key, value] of settingsToDisable) {
|
|
356236
356941
|
try {
|
|
356237
|
-
await
|
|
356942
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "settings", "put", namespace, key, value], {
|
|
356238
356943
|
timeout: 8e3,
|
|
356239
356944
|
windowsHide: true
|
|
356240
356945
|
});
|
|
@@ -356246,14 +356951,14 @@ async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
|
356246
356951
|
if (!deviceId) {
|
|
356247
356952
|
throw new Error("Android keyboard fallback requires a device id");
|
|
356248
356953
|
}
|
|
356249
|
-
await
|
|
356954
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
|
|
356250
356955
|
timeout: 1e4,
|
|
356251
356956
|
windowsHide: true
|
|
356252
356957
|
});
|
|
356253
356958
|
}
|
|
356254
356959
|
async function pressAndroidKeyboardEnter(deviceId) {
|
|
356255
356960
|
if (!deviceId) throw new Error("Android keyboard Enter fallback requires a device id");
|
|
356256
|
-
await
|
|
356961
|
+
await execFileAsync3("adb", ["-s", deviceId, "shell", "input", "keyevent", "66"], {
|
|
356257
356962
|
timeout: 1e4,
|
|
356258
356963
|
windowsHide: true
|
|
356259
356964
|
});
|
|
@@ -356341,21 +357046,40 @@ async function executeInWebViewContext(sessionId, step, css, body) {
|
|
|
356341
357046
|
const cleanCss = css.startsWith("css=") ? css.slice(4) : css;
|
|
356342
357047
|
await withWebViewContext(sessionPath, webview, () => body(sessionPath, cleanCss));
|
|
356343
357048
|
}
|
|
356344
|
-
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS) {
|
|
357049
|
+
async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
357050
|
+
const mode = strictMatchMode();
|
|
356345
357051
|
const startedAt = Date.now();
|
|
356346
357052
|
let lastError = "";
|
|
356347
357053
|
while (Date.now() - startedAt < timeoutMs) {
|
|
356348
357054
|
try {
|
|
356349
|
-
const
|
|
356350
|
-
|
|
356351
|
-
|
|
356352
|
-
})
|
|
356353
|
-
|
|
356354
|
-
|
|
356355
|
-
|
|
356356
|
-
|
|
357055
|
+
const ids = await findWebViewElements(sessionPath, css);
|
|
357056
|
+
if (ids.length === 0) {
|
|
357057
|
+
lastError = `no match for css=${css}`;
|
|
357058
|
+
} else if (ids.length === 1 || opts?.disambiguate === false || mode === "off") {
|
|
357059
|
+
return ids[0];
|
|
357060
|
+
} else {
|
|
357061
|
+
const candidates = ids.map((elementId) => ({ elementId }));
|
|
357062
|
+
const result = chooseRecordedMatch(candidates, {
|
|
357063
|
+
recordedBounds: null,
|
|
357064
|
+
matchIndex: opts?.matchIndex,
|
|
357065
|
+
expectedMatchCount: opts?.expectedMatchCount,
|
|
357066
|
+
strict: mode
|
|
357067
|
+
});
|
|
357068
|
+
if (result.kind === "matched") {
|
|
357069
|
+
return result.elementId;
|
|
357070
|
+
}
|
|
357071
|
+
if (result.kind === "ambiguous" && !opts?.tolerateAmbiguity) {
|
|
357072
|
+
throw new MobileAmbiguousMatchError(
|
|
357073
|
+
`ambiguous WEBVIEW locator: ${result.count} matches for css=${css}, cannot identify the recorded instance`
|
|
357074
|
+
);
|
|
357075
|
+
}
|
|
357076
|
+
opts?.log?.(
|
|
357077
|
+
`\u26A0 ambiguous WEBVIEW locator (${result.count} matches for css=${css}) \u2014 using the first match. Set VALIDATEQA_MOBILE_STRICT_MATCH=strict to fail instead.`
|
|
357078
|
+
);
|
|
357079
|
+
return result.kind === "ambiguous" ? ids[0] : result.elementId;
|
|
357080
|
+
}
|
|
356357
357081
|
} catch (err) {
|
|
356358
|
-
if (isTransportError(err)) throw err;
|
|
357082
|
+
if (isTransportError(err) || err instanceof MobileAmbiguousMatchError) throw err;
|
|
356359
357083
|
lastError = err instanceof Error ? err.message : String(err);
|
|
356360
357084
|
}
|
|
356361
357085
|
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
@@ -356364,21 +357088,42 @@ async function findWebViewElement(sessionPath, css, timeoutMs = DEFAULT_STEP_TIM
|
|
|
356364
357088
|
`WEBVIEW element not found for css=${css} after ${timeoutMs}ms` + (lastError ? `. Last error: ${lastError}` : "")
|
|
356365
357089
|
);
|
|
356366
357090
|
}
|
|
357091
|
+
function isWebViewHydrationTransient(err) {
|
|
357092
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357093
|
+
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);
|
|
357094
|
+
}
|
|
357095
|
+
var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
|
|
357096
|
+
async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
|
|
357097
|
+
const deadline = Date.now() + Math.max(1e3, timeoutMs);
|
|
357098
|
+
for (; ; ) {
|
|
357099
|
+
try {
|
|
357100
|
+
await executeInWebViewContext(sessionId, step, css, body);
|
|
357101
|
+
return;
|
|
357102
|
+
} catch (err) {
|
|
357103
|
+
if (!isWebViewHydrationTransient(err) || Date.now() >= deadline) throw err;
|
|
357104
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
357105
|
+
}
|
|
357106
|
+
}
|
|
357107
|
+
}
|
|
356367
357108
|
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356368
357109
|
const webCss = stepWebViewCss(step);
|
|
356369
357110
|
if (webCss) {
|
|
356370
|
-
await
|
|
357111
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356371
357112
|
sessionId,
|
|
356372
357113
|
step,
|
|
356373
357114
|
webCss,
|
|
356374
|
-
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css)
|
|
357115
|
+
(sessionPath, css) => clickCssInCurrentContext(sessionPath, css),
|
|
357116
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356375
357117
|
);
|
|
356376
357118
|
return;
|
|
356377
357119
|
}
|
|
356378
357120
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356379
357121
|
if (step.target) {
|
|
356380
357122
|
try {
|
|
356381
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357123
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357124
|
+
platform: platform3,
|
|
357125
|
+
recordedBounds: bounds
|
|
357126
|
+
});
|
|
356382
357127
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356383
357128
|
method: "POST",
|
|
356384
357129
|
body: "{}"
|
|
@@ -356400,11 +357145,12 @@ async function executeType(sessionId, step, context) {
|
|
|
356400
357145
|
const keysValue = keyboardSubmitOnly ? WEBDRIVER_ENTER_KEY : value;
|
|
356401
357146
|
const webCss = stepWebViewCss(step);
|
|
356402
357147
|
if (webCss) {
|
|
356403
|
-
await
|
|
357148
|
+
await executeInWebViewContextWithHydrationRetry(
|
|
356404
357149
|
sessionId,
|
|
356405
357150
|
step,
|
|
356406
357151
|
webCss,
|
|
356407
|
-
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value)
|
|
357152
|
+
(sessionPath2, css) => keyboardSubmitOnly ? submitCssKeyboardInCurrentContext(sessionPath2, css) : typeCssInCurrentContext(sessionPath2, css, value),
|
|
357153
|
+
Math.max(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS)
|
|
356408
357154
|
);
|
|
356409
357155
|
return;
|
|
356410
357156
|
}
|
|
@@ -356415,7 +357161,9 @@ async function executeType(sessionId, step, context) {
|
|
|
356415
357161
|
const locatorStrategies = buildLocatorStrategies(step.target, context.platform);
|
|
356416
357162
|
if (step.target && locatorStrategies.length > 0) {
|
|
356417
357163
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
356418
|
-
platform: context.platform
|
|
357164
|
+
platform: context.platform,
|
|
357165
|
+
recordedBounds: bounds,
|
|
357166
|
+
log: context.log
|
|
356419
357167
|
});
|
|
356420
357168
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
356421
357169
|
method: "POST",
|
|
@@ -356456,22 +357204,28 @@ async function executeType(sessionId, step, context) {
|
|
|
356456
357204
|
}
|
|
356457
357205
|
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356458
357206
|
const webCss = stepWebViewCss(step);
|
|
357207
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356459
357208
|
if (webCss) {
|
|
356460
357209
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356461
|
-
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357210
|
+
const elementId = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357211
|
+
matchIndex: step.target?.matchIndex,
|
|
357212
|
+
expectedMatchCount: step.target?.expectedMatchCount
|
|
357213
|
+
});
|
|
356462
357214
|
await wdRequest(`${sessionPath}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356463
357215
|
});
|
|
356464
357216
|
return;
|
|
356465
357217
|
}
|
|
356466
357218
|
const locatorStrategies = buildLocatorStrategies(step.target, platform3);
|
|
356467
357219
|
if (step.target && locatorStrategies.length > 0) {
|
|
356468
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357220
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357221
|
+
platform: platform3,
|
|
357222
|
+
recordedBounds: bounds
|
|
357223
|
+
});
|
|
356469
357224
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
356470
357225
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
356471
357226
|
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
356472
357227
|
return;
|
|
356473
357228
|
}
|
|
356474
|
-
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
356475
357229
|
await clearField(`/session/${sessionId}`, bounds ?? void 0);
|
|
356476
357230
|
}
|
|
356477
357231
|
async function executeSwipe(sessionId, step) {
|
|
@@ -356532,34 +357286,47 @@ async function executeWaitForElement(sessionId, step, platform3) {
|
|
|
356532
357286
|
sessionId,
|
|
356533
357287
|
step,
|
|
356534
357288
|
webCss,
|
|
356535
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout).then(() => void 0)
|
|
357289
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, timeout, { disambiguate: false }).then(() => void 0)
|
|
356536
357290
|
);
|
|
356537
357291
|
return;
|
|
356538
357292
|
}
|
|
356539
|
-
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
357293
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3, disambiguate: false });
|
|
356540
357294
|
}
|
|
356541
|
-
async function executeAssertVisible(sessionId, step, platform3) {
|
|
357295
|
+
async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356542
357296
|
const webCss = stepWebViewCss(step);
|
|
356543
357297
|
if (webCss) {
|
|
356544
357298
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356545
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357299
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357300
|
+
matchIndex: step.target?.matchIndex,
|
|
357301
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357302
|
+
tolerateAmbiguity: true
|
|
357303
|
+
});
|
|
356546
357304
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/displayed`);
|
|
356547
357305
|
if (!result2?.value) throw new Error(`WEBVIEW element found but not visible: css=${css}`);
|
|
356548
357306
|
});
|
|
356549
357307
|
return;
|
|
356550
357308
|
}
|
|
356551
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357309
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357310
|
+
identityOnly: true,
|
|
357311
|
+
platform: platform3,
|
|
357312
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357313
|
+
tolerateAmbiguity: true
|
|
357314
|
+
});
|
|
356552
357315
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
356553
357316
|
if (!result?.value) {
|
|
356554
357317
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
356555
357318
|
}
|
|
356556
357319
|
}
|
|
356557
|
-
async function executeAssertText(sessionId, step, platform3) {
|
|
357320
|
+
async function executeAssertText(sessionId, step, platform3, isCrossPlatform = false) {
|
|
356558
357321
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
356559
357322
|
const webCss = stepWebViewCss(step);
|
|
356560
357323
|
if (webCss) {
|
|
356561
357324
|
await executeInWebViewContext(sessionId, step, webCss, async (sessionPath, css) => {
|
|
356562
|
-
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step)
|
|
357325
|
+
const elementId2 = await findWebViewElement(sessionPath, css, readStepTimeout(step), {
|
|
357326
|
+
matchIndex: step.target?.matchIndex,
|
|
357327
|
+
expectedMatchCount: step.target?.expectedMatchCount,
|
|
357328
|
+
tolerateAmbiguity: true
|
|
357329
|
+
});
|
|
356563
357330
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/text`);
|
|
356564
357331
|
const actualText2 = typeof result2?.value === "string" ? result2.value : "";
|
|
356565
357332
|
const textCandidates2 = await readElementTextCandidates(sessionId, elementId2, actualText2);
|
|
@@ -356571,7 +357338,12 @@ async function executeAssertText(sessionId, step, platform3) {
|
|
|
356571
357338
|
});
|
|
356572
357339
|
return;
|
|
356573
357340
|
}
|
|
356574
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357341
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357342
|
+
identityOnly: true,
|
|
357343
|
+
platform: platform3,
|
|
357344
|
+
recordedBounds: boundsForRun(step, platform3, isCrossPlatform),
|
|
357345
|
+
tolerateAmbiguity: true
|
|
357346
|
+
});
|
|
356575
357347
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
356576
357348
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
356577
357349
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -356611,6 +357383,8 @@ function acquireControlPortSlot(deviceId) {
|
|
|
356611
357383
|
function releaseControlPortSlot(deviceId) {
|
|
356612
357384
|
controlPortSlotsByDevice.delete(deviceId);
|
|
356613
357385
|
}
|
|
357386
|
+
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357387
|
+
var pinnedDeviceSnapshot = null;
|
|
356614
357388
|
async function executeMobileTest(options) {
|
|
356615
357389
|
const logLines = [];
|
|
356616
357390
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -356618,6 +357392,7 @@ async function executeMobileTest(options) {
|
|
|
356618
357392
|
const { steps: runSteps, missingKeys: missingVariableKeys } = resolveMobileStepValuePlaceholders(options.steps, options.testVariables);
|
|
356619
357393
|
const runPlatform = options.target.platform;
|
|
356620
357394
|
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
357395
|
+
const captureStepScreenshots = options.captureStepScreenshots !== false;
|
|
356621
357396
|
if (missingVariableKeys.length > 0) {
|
|
356622
357397
|
log2(`\u26A0 Unresolved step-value placeholder(s): ${missingVariableKeys.map((k) => `{{${k}}}`).join(", ")} \u2014 no matching test variable configured for this project/run.`);
|
|
356623
357398
|
}
|
|
@@ -356682,32 +357457,42 @@ async function executeMobileTest(options) {
|
|
|
356682
357457
|
await startAppiumServer();
|
|
356683
357458
|
log2("Appium server started");
|
|
356684
357459
|
}
|
|
356685
|
-
log2("Discovering devices...");
|
|
356686
|
-
const devices = discoverAllDevices({ includeApps: true });
|
|
356687
357460
|
const platformFilter = options.target.platform;
|
|
356688
|
-
const
|
|
356689
|
-
|
|
356690
|
-
);
|
|
356691
|
-
if (
|
|
356692
|
-
|
|
356693
|
-
|
|
357461
|
+
const pinnedDeviceId = options.target.assignedDeviceId;
|
|
357462
|
+
const candidateSnapshot = pinnedDeviceSnapshot;
|
|
357463
|
+
const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && candidateSnapshot.deviceId === pinnedDeviceId && Date.now() - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
|
|
357464
|
+
if (reusablePinnedSnapshot) {
|
|
357465
|
+
assertDeviceNotQuarantined(reusablePinnedSnapshot.device.id);
|
|
357466
|
+
selectedDevice = reusablePinnedSnapshot.device;
|
|
357467
|
+
log2(`Reusing known device (skipped device re-scan): ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357468
|
+
} else {
|
|
357469
|
+
log2("Discovering devices...");
|
|
357470
|
+
const devices = discoverAllDevices({ includeApps: true });
|
|
357471
|
+
const matchingDevices = devices.filter(
|
|
357472
|
+
(d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
|
|
356694
357473
|
);
|
|
356695
|
-
|
|
356696
|
-
if (options.target.assignedDeviceId) {
|
|
356697
|
-
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
356698
|
-
if (!selectedDevice) {
|
|
357474
|
+
if (matchingDevices.length === 0) {
|
|
356699
357475
|
throw new Error(
|
|
356700
|
-
`
|
|
357476
|
+
`No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
|
|
356701
357477
|
);
|
|
356702
357478
|
}
|
|
357479
|
+
if (options.target.assignedDeviceId) {
|
|
357480
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
357481
|
+
if (!selectedDevice) {
|
|
357482
|
+
throw new Error(
|
|
357483
|
+
`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.`
|
|
357484
|
+
);
|
|
357485
|
+
}
|
|
357486
|
+
}
|
|
357487
|
+
if (!selectedDevice && options.target.recordedDeviceId) {
|
|
357488
|
+
selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
|
|
357489
|
+
}
|
|
357490
|
+
if (!selectedDevice) {
|
|
357491
|
+
selectedDevice = matchingDevices[0];
|
|
357492
|
+
}
|
|
357493
|
+
log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
|
|
357494
|
+
pinnedDeviceSnapshot = options.target.assignedDeviceId ? { deviceId: options.target.assignedDeviceId, device: selectedDevice, at: Date.now() } : null;
|
|
356703
357495
|
}
|
|
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
357496
|
try {
|
|
356712
357497
|
capture = await startMobileNetworkCapture({
|
|
356713
357498
|
platform: options.target.platform,
|
|
@@ -356774,7 +357559,14 @@ async function executeMobileTest(options) {
|
|
|
356774
357559
|
const resetAllowed = shouldResetMobileAppData(options.target.platform, selectedDevice.id);
|
|
356775
357560
|
if (resetAllowed) {
|
|
356776
357561
|
try {
|
|
356777
|
-
|
|
357562
|
+
if (options.target.platform === "IOS") {
|
|
357563
|
+
await terminateTargetApp(sessionId, "IOS", options.target.appId).catch(() => {
|
|
357564
|
+
});
|
|
357565
|
+
const wiped = await clearIosSimulatorAppData(selectedDevice.id, options.target.appId);
|
|
357566
|
+
if (!wiped.ok) throw new Error(wiped.detail);
|
|
357567
|
+
} else {
|
|
357568
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
357569
|
+
}
|
|
356778
357570
|
clearedAppDataBeforeLaunch = true;
|
|
356779
357571
|
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
357572
|
} catch (err) {
|
|
@@ -356827,17 +357619,21 @@ async function executeMobileTest(options) {
|
|
|
356827
357619
|
const entryStep = selectEntryDriftGateStep(runSteps, runPlatform);
|
|
356828
357620
|
if (entryStep) {
|
|
356829
357621
|
try {
|
|
356830
|
-
const entryTimeout =
|
|
357622
|
+
const entryTimeout = Math.max(DEFAULT_STEP_TIMEOUT_MS, COLD_BOOT_ENTRY_TIMEOUT_MS);
|
|
356831
357623
|
const entryWebCss = stepWebViewCss(entryStep);
|
|
356832
357624
|
if (entryWebCss) {
|
|
356833
357625
|
await executeInWebViewContext(
|
|
356834
357626
|
sessionId,
|
|
356835
357627
|
entryStep,
|
|
356836
357628
|
entryWebCss,
|
|
356837
|
-
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout).then(() => void 0)
|
|
357629
|
+
(sessionPath, css) => findWebViewElement(sessionPath, css, entryTimeout, { disambiguate: false }).then(() => void 0)
|
|
356838
357630
|
);
|
|
356839
357631
|
} else {
|
|
356840
|
-
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357632
|
+
await findElement(sessionId, entryStep.target, entryTimeout, {
|
|
357633
|
+
identityOnly: true,
|
|
357634
|
+
platform: runPlatform,
|
|
357635
|
+
disambiguate: false
|
|
357636
|
+
});
|
|
356841
357637
|
}
|
|
356842
357638
|
} catch (err) {
|
|
356843
357639
|
if (isTransportError(err)) throw err;
|
|
@@ -356897,10 +357693,10 @@ async function executeMobileTest(options) {
|
|
|
356897
357693
|
await executeWaitForElement(sessionId, step, runPlatform);
|
|
356898
357694
|
break;
|
|
356899
357695
|
case "assertVisible":
|
|
356900
|
-
await executeAssertVisible(sessionId, step, runPlatform);
|
|
357696
|
+
await executeAssertVisible(sessionId, step, runPlatform, isCrossPlatform);
|
|
356901
357697
|
break;
|
|
356902
357698
|
case "assertText":
|
|
356903
|
-
await executeAssertText(sessionId, step, runPlatform);
|
|
357699
|
+
await executeAssertText(sessionId, step, runPlatform, isCrossPlatform);
|
|
356904
357700
|
break;
|
|
356905
357701
|
default: {
|
|
356906
357702
|
const _exhaustive = step.action;
|
|
@@ -356919,7 +357715,7 @@ async function executeMobileTest(options) {
|
|
|
356919
357715
|
environmental = stepError;
|
|
356920
357716
|
}
|
|
356921
357717
|
}
|
|
356922
|
-
const screenshot = await takeScreenshot(sessionId).catch(() => null);
|
|
357718
|
+
const screenshot = captureStepScreenshots ? await takeScreenshot(sessionId).catch(() => null) : null;
|
|
356923
357719
|
screenshots.push(screenshot ?? "");
|
|
356924
357720
|
if (status === "failed") {
|
|
356925
357721
|
await recordFirstFailingSource();
|
|
@@ -357181,10 +357977,10 @@ async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
|
357181
357977
|
await executeWaitForElement(sessionId, step, platform3);
|
|
357182
357978
|
return true;
|
|
357183
357979
|
case "assertVisible":
|
|
357184
|
-
await executeAssertVisible(sessionId, step, platform3);
|
|
357980
|
+
await executeAssertVisible(sessionId, step, platform3, true);
|
|
357185
357981
|
return true;
|
|
357186
357982
|
case "assertText":
|
|
357187
|
-
await executeAssertText(sessionId, step, platform3);
|
|
357983
|
+
await executeAssertText(sessionId, step, platform3, true);
|
|
357188
357984
|
return true;
|
|
357189
357985
|
default:
|
|
357190
357986
|
break;
|
|
@@ -357266,7 +358062,15 @@ async function harvestPlatformLocators(options) {
|
|
|
357266
358062
|
const identityStrategies = webViewCss ? [] : buildIdentityLocatorStrategies(step.target);
|
|
357267
358063
|
if (!webViewCss && identityStrategies.length > 0 && step.target) {
|
|
357268
358064
|
try {
|
|
357269
|
-
elementId = await findElement(
|
|
358065
|
+
elementId = await findElement(
|
|
358066
|
+
session.sessionId,
|
|
358067
|
+
step.target,
|
|
358068
|
+
Math.max(DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS),
|
|
358069
|
+
{
|
|
358070
|
+
identityOnly: true,
|
|
358071
|
+
failClosedOnAmbiguity: true
|
|
358072
|
+
}
|
|
358073
|
+
);
|
|
357270
358074
|
} catch (err) {
|
|
357271
358075
|
if (isTransportError(err)) throw err;
|
|
357272
358076
|
elementId = null;
|
|
@@ -357435,7 +358239,7 @@ import { createServer } from "http";
|
|
|
357435
358239
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
357436
358240
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
357437
358241
|
import { homedir as homedir2 } from "os";
|
|
357438
|
-
import { join as
|
|
358242
|
+
import { join as join6, dirname } from "path";
|
|
357439
358243
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
357440
358244
|
init_dist();
|
|
357441
358245
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
@@ -357542,7 +358346,7 @@ async function getWindowSize3() {
|
|
|
357542
358346
|
}
|
|
357543
358347
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
357544
358348
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
357545
|
-
var BRIDGE_PID_FILE =
|
|
358349
|
+
var BRIDGE_PID_FILE = join6(homedir2(), ".validate.qa", "mobile-bridge.pid");
|
|
357546
358350
|
function readBridgePidFile() {
|
|
357547
358351
|
try {
|
|
357548
358352
|
const raw = readFileSync2(BRIDGE_PID_FILE, "utf-8").trim();
|
|
@@ -357854,6 +358658,13 @@ async function startSession(validated, res) {
|
|
|
357854
358658
|
...deviceId ? { "appium:udid": deviceId } : {},
|
|
357855
358659
|
...platformVersion ? { "appium:platformVersion": platformVersion } : {},
|
|
357856
358660
|
"appium:noReset": true,
|
|
358661
|
+
// Same Xcode 27+ workaround as the executor's createSession (appium-executor.ts):
|
|
358662
|
+
// Simulator.app is no longer at <DEVELOPER_DIR>/Applications, so XCUITest's
|
|
358663
|
+
// "open Simulator UI" step fails. The sim is already booted via CoreSimulator;
|
|
358664
|
+
// skip the GUI open. Scoped to simulator UDIDs; APPIUM_IOS_HEADLESS=false
|
|
358665
|
+
// overrides. Verified live: without this, recorder/mirror sessions on iOS
|
|
358666
|
+
// die with "Simulator.app does not exist" while executor sessions work.
|
|
358667
|
+
...isIosSimulatorUdid(deviceId) && process.env.APPIUM_IOS_HEADLESS !== "false" ? { "appium:isHeadless": true } : {},
|
|
357857
358668
|
...realCaps
|
|
357858
358669
|
} : {
|
|
357859
358670
|
platformName: "Android",
|
|
@@ -357872,7 +358683,12 @@ async function startSession(validated, res) {
|
|
|
357872
358683
|
capabilities: {
|
|
357873
358684
|
alwaysMatch: capabilities
|
|
357874
358685
|
}
|
|
357875
|
-
})
|
|
358686
|
+
}),
|
|
358687
|
+
// Session creation is the one legitimately slow WebDriver call (iOS WDA
|
|
358688
|
+
// build/launch can take minutes on a cold toolchain) — same ceiling as
|
|
358689
|
+
// the executor's wdRequest session-create path. Without this, the shared
|
|
358690
|
+
// appiumRequest 10s default aborts every iOS recorder session.
|
|
358691
|
+
signal: AbortSignal.timeout(WD_SESSION_CREATE_TIMEOUT_MS)
|
|
357876
358692
|
});
|
|
357877
358693
|
state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
|
|
357878
358694
|
if (!state.appiumSessionId) {
|
|
@@ -358650,6 +359466,9 @@ function detachMirrorSession(deviceId) {
|
|
|
358650
359466
|
stopAllMirrorClients();
|
|
358651
359467
|
stopSessionReaper();
|
|
358652
359468
|
}
|
|
359469
|
+
function getMirrorSessionId(deviceId) {
|
|
359470
|
+
return runnerChannels.get(deviceId)?.sessionId;
|
|
359471
|
+
}
|
|
358653
359472
|
|
|
358654
359473
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
358655
359474
|
var import_runner_core9 = __toESM(require_dist2());
|
|
@@ -358680,6 +359499,7 @@ var MobileBridgeDriver = class {
|
|
|
358680
359499
|
sessionId;
|
|
358681
359500
|
platform;
|
|
358682
359501
|
appId;
|
|
359502
|
+
deviceId;
|
|
358683
359503
|
allowAppReset;
|
|
358684
359504
|
recoverSession;
|
|
358685
359505
|
maxSessionRecoveries;
|
|
@@ -358700,6 +359520,7 @@ var MobileBridgeDriver = class {
|
|
|
358700
359520
|
this.sessionId = config.sessionId;
|
|
358701
359521
|
this.platform = config.platform;
|
|
358702
359522
|
this.appId = config.appId;
|
|
359523
|
+
this.deviceId = config.deviceId;
|
|
358703
359524
|
this.allowAppReset = config.allowAppReset === true;
|
|
358704
359525
|
this.recoverSession = config.recoverSession;
|
|
358705
359526
|
this.maxSessionRecoveries = Math.max(0, Math.floor(config.maxSessionRecoveries ?? 1));
|
|
@@ -358997,17 +359818,34 @@ var MobileBridgeDriver = class {
|
|
|
358997
359818
|
});
|
|
358998
359819
|
}
|
|
358999
359820
|
/**
|
|
359000
|
-
* Deterministic state reset: clear app data + relaunch
|
|
359001
|
-
*
|
|
359002
|
-
*
|
|
359003
|
-
*
|
|
359004
|
-
*
|
|
359821
|
+
* Deterministic state reset: clear app data + relaunch, per the
|
|
359822
|
+
* runner-computed allowAppReset policy (Android emulators / iOS simulators
|
|
359823
|
+
* only — a personal physical device is never wiped). Android clears via
|
|
359824
|
+
* `mobile: clearApp`; iOS terminates the app and wipes its simulator data
|
|
359825
|
+
* container + keychain through `simctl` (see ios-simulator-reset.ts).
|
|
359826
|
+
* Returns false when unsupported/disallowed or when the clear fails —
|
|
359827
|
+
* callers fall back to relaunch(), preserving today's behavior. On success,
|
|
359828
|
+
* waits for cold-boot content so recording and replay agree on the true
|
|
359829
|
+
* entry screen.
|
|
359005
359830
|
*/
|
|
359006
359831
|
async resetAppState() {
|
|
359007
|
-
if (!this.allowAppReset
|
|
359832
|
+
if (!this.allowAppReset) return false;
|
|
359833
|
+
if (this.platform === "IOS" && !isIosSimulatorUdid(this.deviceId)) return false;
|
|
359008
359834
|
return this.withSessionRecovery("app data reset", async () => {
|
|
359009
359835
|
try {
|
|
359010
|
-
|
|
359836
|
+
if (this.platform === "IOS") {
|
|
359837
|
+
try {
|
|
359838
|
+
await this.executeScript("mobile: terminateApp", [{ ...this.appLifecycleArgs(), timeout: 5e3 }]);
|
|
359839
|
+
} catch (err) {
|
|
359840
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
359841
|
+
}
|
|
359842
|
+
const wiped = await clearIosSimulatorAppData(this.deviceId ?? "", this.appId);
|
|
359843
|
+
if (!wiped.ok) {
|
|
359844
|
+
throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
|
|
359845
|
+
}
|
|
359846
|
+
} else {
|
|
359847
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
359848
|
+
}
|
|
359011
359849
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
359012
359850
|
try {
|
|
359013
359851
|
await waitForColdBootSettle(this.sessionPath);
|
|
@@ -359598,6 +360436,113 @@ function envInt(name, fallback) {
|
|
|
359598
360436
|
}
|
|
359599
360437
|
var MOBILE_DEVICE_CEILING = Math.max(0, envInt("MOBILE_DEVICE_CEILING", 0));
|
|
359600
360438
|
var MOBILE_HEAL_HOLD_MS = Math.max(0, envInt("MOBILE_HEAL_HOLD_MS", 2e4));
|
|
360439
|
+
var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 30 * 6e4));
|
|
360440
|
+
var MOBILE_RUN_ABORT_GRACE_MS = Math.max(0, envInt("MOBILE_RUN_ABORT_GRACE_MS", 2 * 6e4));
|
|
360441
|
+
var MOBILE_CANCEL_POLL_MS = Math.max(0, envInt("MOBILE_CANCEL_POLL_MS", 15e3));
|
|
360442
|
+
var MobileRunWatchdog = class {
|
|
360443
|
+
constructor(options) {
|
|
360444
|
+
this.options = options;
|
|
360445
|
+
}
|
|
360446
|
+
phase = "idle";
|
|
360447
|
+
released = false;
|
|
360448
|
+
quarantined = false;
|
|
360449
|
+
cancelAbortTimer = null;
|
|
360450
|
+
cancelGraceTimer = null;
|
|
360451
|
+
/** Current phase (exposed for tests / observability). */
|
|
360452
|
+
getPhase() {
|
|
360453
|
+
return this.phase;
|
|
360454
|
+
}
|
|
360455
|
+
/** Arm the wall-clock abort timer. Call once, immediately after dispatch. */
|
|
360456
|
+
start() {
|
|
360457
|
+
if (this.phase !== "idle") return;
|
|
360458
|
+
this.phase = "active";
|
|
360459
|
+
if (this.options.abortMs > 0) {
|
|
360460
|
+
this.cancelAbortTimer = this.options.schedule(
|
|
360461
|
+
() => this.triggerAbort(`exceeded the ${Math.round(this.options.abortMs / 6e4)}min wall-clock ceiling`),
|
|
360462
|
+
this.options.abortMs
|
|
360463
|
+
);
|
|
360464
|
+
}
|
|
360465
|
+
}
|
|
360466
|
+
/** Stage 1. Idempotent (a no-op unless still ACTIVE). Also called by the
|
|
360467
|
+
* cancellation observer. Quarantines the device (blocking the run's own
|
|
360468
|
+
* session-recovery from resurrecting the killed session — observed live),
|
|
360469
|
+
* kills the run's session (best-effort), and starts the grace clock;
|
|
360470
|
+
* deliberately does NOT release the lease. */
|
|
360471
|
+
triggerAbort(reason) {
|
|
360472
|
+
if (this.phase !== "active") return;
|
|
360473
|
+
this.phase = "aborting";
|
|
360474
|
+
this.clearAbortTimer();
|
|
360475
|
+
if (this.options.deviceId) {
|
|
360476
|
+
this.quarantined = true;
|
|
360477
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360478
|
+
this.options.callbacks.abort(this.options.deviceId, this.options.runId, reason);
|
|
360479
|
+
} else {
|
|
360480
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} ${reason} \u2014 no leased device to abort; awaiting the run's own settle.`);
|
|
360481
|
+
}
|
|
360482
|
+
if (this.options.graceMs > 0) {
|
|
360483
|
+
this.cancelGraceTimer = this.options.schedule(() => this.forceRelease(), this.options.graceMs);
|
|
360484
|
+
} else {
|
|
360485
|
+
this.forceRelease();
|
|
360486
|
+
}
|
|
360487
|
+
}
|
|
360488
|
+
/** The run's promise settled (normal completion, or after an abort). The single
|
|
360489
|
+
* release owner: releases at most once and lifts any quarantine. Idempotent. */
|
|
360490
|
+
settle() {
|
|
360491
|
+
if (this.phase === "settled") return;
|
|
360492
|
+
const wasForceReleased = this.released;
|
|
360493
|
+
this.phase = "settled";
|
|
360494
|
+
this.clearAbortTimer();
|
|
360495
|
+
this.clearGraceTimer();
|
|
360496
|
+
if (!this.released) {
|
|
360497
|
+
this.released = true;
|
|
360498
|
+
this.options.callbacks.release();
|
|
360499
|
+
}
|
|
360500
|
+
if (this.quarantined && this.options.deviceId) {
|
|
360501
|
+
this.quarantined = false;
|
|
360502
|
+
this.options.callbacks.unquarantine(this.options.deviceId);
|
|
360503
|
+
if (wasForceReleased) {
|
|
360504
|
+
this.options.callbacks.log("info", `Run ${this.options.runId} finally settled \u2014 lifted the quarantine on device ${this.options.deviceId}.`);
|
|
360505
|
+
}
|
|
360506
|
+
}
|
|
360507
|
+
}
|
|
360508
|
+
/** Stage 2. The run did not settle within the grace window after the abort. Force-
|
|
360509
|
+
* release the slot to unwedge capacity; the device stays quarantined (since the
|
|
360510
|
+
* stage-1 abort) until the promise eventually settles. Idempotent and guarded
|
|
360511
|
+
* against a late settle. */
|
|
360512
|
+
forceRelease() {
|
|
360513
|
+
if (this.released || this.phase === "settled") return;
|
|
360514
|
+
this.clearGraceTimer();
|
|
360515
|
+
this.released = true;
|
|
360516
|
+
this.options.callbacks.release();
|
|
360517
|
+
if (this.options.deviceId) {
|
|
360518
|
+
if (!this.quarantined) {
|
|
360519
|
+
this.quarantined = true;
|
|
360520
|
+
this.options.callbacks.quarantine(this.options.deviceId);
|
|
360521
|
+
}
|
|
360522
|
+
this.options.callbacks.log(
|
|
360523
|
+
"warn",
|
|
360524
|
+
`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.`
|
|
360525
|
+
);
|
|
360526
|
+
} else {
|
|
360527
|
+
this.options.callbacks.log("warn", `Run ${this.options.runId} did not settle after abort \u2014 force-released its slot to unwedge mobile capacity.`);
|
|
360528
|
+
}
|
|
360529
|
+
}
|
|
360530
|
+
clearAbortTimer() {
|
|
360531
|
+
if (this.cancelAbortTimer) {
|
|
360532
|
+
this.cancelAbortTimer();
|
|
360533
|
+
this.cancelAbortTimer = null;
|
|
360534
|
+
}
|
|
360535
|
+
}
|
|
360536
|
+
clearGraceTimer() {
|
|
360537
|
+
if (this.cancelGraceTimer) {
|
|
360538
|
+
this.cancelGraceTimer();
|
|
360539
|
+
this.cancelGraceTimer = null;
|
|
360540
|
+
}
|
|
360541
|
+
}
|
|
360542
|
+
};
|
|
360543
|
+
function isServerCancelledRunStatus(status) {
|
|
360544
|
+
return status === "ERROR" || status === "CANCELLED" || status === "CANCELLING";
|
|
360545
|
+
}
|
|
359601
360546
|
function scrubAuditMessages(messages, testVariables) {
|
|
359602
360547
|
if (messages === void 0 || messages === null) return messages;
|
|
359603
360548
|
const compacted = compactForAudit(messages);
|
|
@@ -359673,7 +360618,7 @@ var log = {
|
|
|
359673
360618
|
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
359674
360619
|
};
|
|
359675
360620
|
function getRunsDir() {
|
|
359676
|
-
const dir = process.env.RUNS_DIR ||
|
|
360621
|
+
const dir = process.env.RUNS_DIR || join7(homedir3(), ".validate.qa", "runs");
|
|
359677
360622
|
(0, import_runner_core12.ensurePrivateDirectory)(dir);
|
|
359678
360623
|
return dir;
|
|
359679
360624
|
}
|
|
@@ -361159,9 +362104,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361159
362104
|
// flip the test to needs_manual_authoring), so an environmentally
|
|
361160
362105
|
// aborted pass — wrong screen, system dialog, transport flake —
|
|
361161
362106
|
// must not persist step divergences it could not fairly judge.
|
|
361162
|
-
//
|
|
361163
|
-
//
|
|
361164
|
-
|
|
362107
|
+
// The same applies to a harvest that ADVANCED THROUGH NOTHING
|
|
362108
|
+
// (reachedOrder null): it never got past the entry screen, so its
|
|
362109
|
+
// step judgments are cold-boot/entry noise, not app knowledge —
|
|
362110
|
+
// observed live: a slow WKWebView hydration marked step 1
|
|
362111
|
+
// divergent and permanently dead-ended the test in
|
|
362112
|
+
// needs_manual_authoring with no retry path. Harvested bundles
|
|
362113
|
+
// are still posted: they were positively resolved and the merge
|
|
362114
|
+
// is idempotent.
|
|
362115
|
+
divergentOrders: harvest.environmentalError || harvest.reachedOrder == null ? [] : harvest.divergentOrders,
|
|
361165
362116
|
reachedOrder: harvest.reachedOrder,
|
|
361166
362117
|
logs: harvest.logs,
|
|
361167
362118
|
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
@@ -361605,9 +362556,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
361605
362556
|
sessionId: session.sessionId,
|
|
361606
362557
|
platform: platform3,
|
|
361607
362558
|
appId: session.appId,
|
|
361608
|
-
|
|
361609
|
-
//
|
|
361610
|
-
//
|
|
362559
|
+
deviceId: session.deviceId,
|
|
362560
|
+
// Deterministic-state authorization: Android emulators / iOS
|
|
362561
|
+
// simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
|
|
362562
|
+
// Android default) — a personal physical device must never have
|
|
362563
|
+
// its app data wiped.
|
|
361611
362564
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
361612
362565
|
maxSessionRecoveries: 1,
|
|
361613
362566
|
recoverSession: async (cause) => {
|
|
@@ -362123,6 +363076,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362123
363076
|
sessionId: opened.sessionId,
|
|
362124
363077
|
platform: target.platform,
|
|
362125
363078
|
appId: opened.appId,
|
|
363079
|
+
deviceId: opened.deviceId,
|
|
362126
363080
|
allowAppReset: target.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(target.platform, opened.deviceId)
|
|
362127
363081
|
});
|
|
362128
363082
|
};
|
|
@@ -362735,6 +363689,7 @@ async function startRunner(opts) {
|
|
|
362735
363689
|
else activeMobileOpsByPlatform.set(p, next);
|
|
362736
363690
|
};
|
|
362737
363691
|
const leasedDeviceIds = /* @__PURE__ */ new Map();
|
|
363692
|
+
const activeMobileRuns = /* @__PURE__ */ new Map();
|
|
362738
363693
|
let activeMobileDiscoveries = 0;
|
|
362739
363694
|
const mobileHealHold = /* @__PURE__ */ new Map();
|
|
362740
363695
|
const pruneMobileHealHolds = () => {
|
|
@@ -362985,6 +363940,32 @@ async function startRunner(opts) {
|
|
|
362985
363940
|
}
|
|
362986
363941
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
362987
363942
|
pidsWatchdog.unref();
|
|
363943
|
+
const pollMobileRunCancellations = async () => {
|
|
363944
|
+
if (shuttingDown || activeMobileRuns.size === 0) return;
|
|
363945
|
+
const runIds = [...activeMobileRuns.keys()];
|
|
363946
|
+
try {
|
|
363947
|
+
const res = await fetch(`${serverUrl}/api/runner/runs/status`, {
|
|
363948
|
+
method: "POST",
|
|
363949
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
363950
|
+
body: JSON.stringify({ runIds }),
|
|
363951
|
+
signal: AbortSignal.timeout(1e4)
|
|
363952
|
+
});
|
|
363953
|
+
if (!res.ok) return;
|
|
363954
|
+
const body = await res.json().catch(() => null);
|
|
363955
|
+
if (!body || !Array.isArray(body.runs)) return;
|
|
363956
|
+
for (const row of body.runs) {
|
|
363957
|
+
if (!row || typeof row.id !== "string" || typeof row.status !== "string") continue;
|
|
363958
|
+
if (!isServerCancelledRunStatus(row.status)) continue;
|
|
363959
|
+
const watchdog = activeMobileRuns.get(row.id);
|
|
363960
|
+
if (watchdog) watchdog.triggerAbort(`was cancelled server-side (status ${row.status})`);
|
|
363961
|
+
}
|
|
363962
|
+
} catch {
|
|
363963
|
+
}
|
|
363964
|
+
};
|
|
363965
|
+
const mobileCancelPoll = enableMobile && MOBILE_CANCEL_POLL_MS > 0 ? setInterval(() => {
|
|
363966
|
+
void pollMobileRunCancellations();
|
|
363967
|
+
}, MOBILE_CANCEL_POLL_MS) : null;
|
|
363968
|
+
mobileCancelPoll?.unref?.();
|
|
362988
363969
|
const deferralLogged = /* @__PURE__ */ new Map();
|
|
362989
363970
|
const logDeferredOnce = (runId, line) => {
|
|
362990
363971
|
if (deferralLogged.get(runId) === line) return;
|
|
@@ -363067,14 +364048,17 @@ async function startRunner(opts) {
|
|
|
363067
364048
|
});
|
|
363068
364049
|
const capacity = MOBILE_DEVICE_CEILING > 0 ? Math.min(derived, MOBILE_DEVICE_CEILING) : derived;
|
|
363069
364050
|
const leaseTarget = run2.target ?? run2.deviceRequirements;
|
|
363070
|
-
const
|
|
364051
|
+
const leaseExclusions = {
|
|
364052
|
+
has: (id) => leasedDeviceIds.has(id) || isDeviceQuarantined(id)
|
|
364053
|
+
};
|
|
364054
|
+
const leaseSelection = selectFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot, {
|
|
363071
364055
|
appId: typeof leaseTarget?.appId === "string" ? leaseTarget.appId : void 0,
|
|
363072
364056
|
recordedDeviceId: typeof leaseTarget?.recordedDeviceId === "string" ? leaseTarget.recordedDeviceId : void 0,
|
|
363073
364057
|
platformVersion: typeof leaseTarget?.platformVersion === "string" ? leaseTarget.platformVersion : void 0,
|
|
363074
364058
|
deviceName: typeof run2.deviceRequirements?.deviceName === "string" ? run2.deviceRequirements.deviceName : void 0,
|
|
363075
364059
|
resetPolicy: leaseTarget?.resetPolicy
|
|
363076
364060
|
});
|
|
363077
|
-
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform,
|
|
364061
|
+
const freeDeviceId = leaseSelection.kind === "selected" ? leaseSelection.deviceId : leaseSelection.kind === "unsatisfiable" ? pickFreeDevice(mobilePlatform, leaseExclusions, leaseSnapshot) : null;
|
|
363078
364062
|
if (leaseSelection.kind === "unsatisfiable") {
|
|
363079
364063
|
mobileLeaseError = leaseSelection.reason;
|
|
363080
364064
|
log.warn(` Mobile requirements unsatisfiable for ${run2.runId}: ${leaseSelection.reason}; claiming once to report the configuration error`);
|
|
@@ -363132,6 +364116,54 @@ async function startRunner(opts) {
|
|
|
363132
364116
|
await new Promise((r) => setTimeout(r, staggerMs));
|
|
363133
364117
|
}
|
|
363134
364118
|
claimedThisPoll++;
|
|
364119
|
+
const applyRelease = () => {
|
|
364120
|
+
activeRuns--;
|
|
364121
|
+
decrementType(runMode);
|
|
364122
|
+
decrementProject(runProject);
|
|
364123
|
+
if (isMobileOp) {
|
|
364124
|
+
if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
|
|
364125
|
+
if (isDiscovery) activeMobileDiscoveries--;
|
|
364126
|
+
if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
|
|
364127
|
+
activeMobileRuns.delete(run2.runId);
|
|
364128
|
+
}
|
|
364129
|
+
};
|
|
364130
|
+
const runWatchdog = isMobileOp ? new MobileRunWatchdog({
|
|
364131
|
+
runId: run2.runId,
|
|
364132
|
+
deviceId: leasedDeviceId ?? void 0,
|
|
364133
|
+
abortMs: MOBILE_RUN_WATCHDOG_MS,
|
|
364134
|
+
graceMs: MOBILE_RUN_ABORT_GRACE_MS,
|
|
364135
|
+
// Real timers, unref'd so a pending watchdog never keeps the process
|
|
364136
|
+
// alive on shutdown.
|
|
364137
|
+
schedule: (fn, ms) => {
|
|
364138
|
+
const t = setTimeout(fn, ms);
|
|
364139
|
+
if (typeof t.unref === "function") t.unref();
|
|
364140
|
+
return () => clearTimeout(t);
|
|
364141
|
+
},
|
|
364142
|
+
callbacks: {
|
|
364143
|
+
abort: (deviceId, rid, reason) => {
|
|
364144
|
+
const sessionId = getMirrorSessionId(deviceId);
|
|
364145
|
+
log.warn(
|
|
364146
|
+
`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.`
|
|
364147
|
+
);
|
|
364148
|
+
if (sessionId) void deleteAppiumSession(sessionId);
|
|
364149
|
+
},
|
|
364150
|
+
release: applyRelease,
|
|
364151
|
+
quarantine: (deviceId) => {
|
|
364152
|
+
quarantineDevice(deviceId);
|
|
364153
|
+
},
|
|
364154
|
+
unquarantine: (deviceId) => {
|
|
364155
|
+
unquarantineDevice(deviceId);
|
|
364156
|
+
},
|
|
364157
|
+
log: (level, message) => {
|
|
364158
|
+
if (level === "warn") log.warn(message);
|
|
364159
|
+
else log.info(message);
|
|
364160
|
+
}
|
|
364161
|
+
}
|
|
364162
|
+
}) : null;
|
|
364163
|
+
if (runWatchdog) {
|
|
364164
|
+
activeMobileRuns.set(run2.runId, runWatchdog);
|
|
364165
|
+
runWatchdog.start();
|
|
364166
|
+
}
|
|
363135
364167
|
executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
|
|
363136
364168
|
if (mobilePlatform) {
|
|
363137
364169
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
@@ -363142,14 +364174,8 @@ async function startRunner(opts) {
|
|
|
363142
364174
|
const msg = err instanceof Error ? err.message : String(err);
|
|
363143
364175
|
log.warn(`Run ${run2.runId} crashed: ${msg}`);
|
|
363144
364176
|
}).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
|
-
}
|
|
364177
|
+
if (runWatchdog) runWatchdog.settle();
|
|
364178
|
+
else applyRelease();
|
|
363153
364179
|
});
|
|
363154
364180
|
}
|
|
363155
364181
|
}
|