@validate.qa/runner 1.0.15 → 1.0.17
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 +743 -55
- package/dist/cli.mjs +743 -55
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1861,6 +1861,102 @@ var init_mobile_test_generation = __esm({
|
|
|
1861
1861
|
}
|
|
1862
1862
|
});
|
|
1863
1863
|
|
|
1864
|
+
// ../shared/dist/mobile-cross-platform.js
|
|
1865
|
+
function isMobilePlatform(value) {
|
|
1866
|
+
return value === "IOS" || value === "ANDROID";
|
|
1867
|
+
}
|
|
1868
|
+
function mediumForMobilePlatform(platform3) {
|
|
1869
|
+
return platform3 === "IOS" ? "IOS_NATIVE" : "ANDROID_NATIVE";
|
|
1870
|
+
}
|
|
1871
|
+
function mobilePlatformFromMedium(medium) {
|
|
1872
|
+
if (medium === "IOS_NATIVE")
|
|
1873
|
+
return "IOS";
|
|
1874
|
+
if (medium === "ANDROID_NATIVE")
|
|
1875
|
+
return "ANDROID";
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
function otherMobilePlatform(platform3) {
|
|
1879
|
+
return platform3 === "IOS" ? "ANDROID" : "IOS";
|
|
1880
|
+
}
|
|
1881
|
+
function platformTag(platform3) {
|
|
1882
|
+
return platform3 === "IOS" ? "@ios" : "@android";
|
|
1883
|
+
}
|
|
1884
|
+
function resolvePlatformLocatorBundle(target, platform3) {
|
|
1885
|
+
if (!target)
|
|
1886
|
+
return void 0;
|
|
1887
|
+
if (platform3) {
|
|
1888
|
+
const overlay = target.platformLocators?.[platform3];
|
|
1889
|
+
if (overlay)
|
|
1890
|
+
return overlay;
|
|
1891
|
+
}
|
|
1892
|
+
return target.locatorBundle;
|
|
1893
|
+
}
|
|
1894
|
+
function hasPlatformLocator(target, platform3) {
|
|
1895
|
+
return Boolean(target?.platformLocators?.[platform3]);
|
|
1896
|
+
}
|
|
1897
|
+
function hasCrossPlatformIdentity(bundle) {
|
|
1898
|
+
if (!bundle)
|
|
1899
|
+
return false;
|
|
1900
|
+
return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
1901
|
+
}
|
|
1902
|
+
function stepIsDivergentForPlatform(step, platform3) {
|
|
1903
|
+
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
1904
|
+
}
|
|
1905
|
+
function stepAppliesToPlatform(step, platform3) {
|
|
1906
|
+
const platforms = step.platforms;
|
|
1907
|
+
if (!platforms || platforms.length === 0)
|
|
1908
|
+
return true;
|
|
1909
|
+
if (!platform3)
|
|
1910
|
+
return true;
|
|
1911
|
+
return platforms.includes(platform3);
|
|
1912
|
+
}
|
|
1913
|
+
function stepsMissingPlatformLocators(steps, platform3) {
|
|
1914
|
+
return steps.filter((step) => {
|
|
1915
|
+
if (!LOCATOR_REQUIRING_ACTIONS.has(step.action))
|
|
1916
|
+
return false;
|
|
1917
|
+
if (!stepAppliesToPlatform(step, platform3))
|
|
1918
|
+
return false;
|
|
1919
|
+
if (hasPlatformLocator(step.target, platform3))
|
|
1920
|
+
return false;
|
|
1921
|
+
if (stepIsDivergentForPlatform(step, platform3))
|
|
1922
|
+
return false;
|
|
1923
|
+
return hasCrossPlatformIdentity(step.target?.locatorBundle);
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
function stepsNeedingReviewForPlatform(steps, platform3) {
|
|
1927
|
+
return steps.filter((step) => {
|
|
1928
|
+
if (!LOCATOR_REQUIRING_ACTIONS.has(step.action))
|
|
1929
|
+
return false;
|
|
1930
|
+
if (!stepAppliesToPlatform(step, platform3))
|
|
1931
|
+
return false;
|
|
1932
|
+
if (hasPlatformLocator(step.target, platform3))
|
|
1933
|
+
return false;
|
|
1934
|
+
if (stepIsDivergentForPlatform(step, platform3))
|
|
1935
|
+
return true;
|
|
1936
|
+
return !hasCrossPlatformIdentity(step.target?.locatorBundle);
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
function isTestReadyForPlatform(steps, authoringPlatform, runPlatform) {
|
|
1940
|
+
if (runPlatform === authoringPlatform)
|
|
1941
|
+
return true;
|
|
1942
|
+
return stepsMissingPlatformLocators(steps, runPlatform).length === 0 && stepsNeedingReviewForPlatform(steps, runPlatform).length === 0;
|
|
1943
|
+
}
|
|
1944
|
+
var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS;
|
|
1945
|
+
var init_mobile_cross_platform = __esm({
|
|
1946
|
+
"../shared/dist/mobile-cross-platform.js"() {
|
|
1947
|
+
"use strict";
|
|
1948
|
+
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
1949
|
+
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
1950
|
+
"tap",
|
|
1951
|
+
"type",
|
|
1952
|
+
"clear",
|
|
1953
|
+
"waitForElement",
|
|
1954
|
+
"assertVisible",
|
|
1955
|
+
"assertText"
|
|
1956
|
+
]);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1864
1960
|
// ../shared/dist/mobile-deeplink.js
|
|
1865
1961
|
function isSafeDeepLink(value) {
|
|
1866
1962
|
if (typeof value !== "string")
|
|
@@ -3539,6 +3635,7 @@ __export(dist_exports, {
|
|
|
3539
3635
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
3540
3636
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
3541
3637
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
3638
|
+
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
3542
3639
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
3543
3640
|
MOBILE_SNAPSHOT_MAX_SIZE_BYTES: () => MOBILE_SNAPSHOT_MAX_SIZE_BYTES,
|
|
3544
3641
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
@@ -3592,6 +3689,8 @@ __export(dist_exports, {
|
|
|
3592
3689
|
getPlanDefinition: () => getPlanDefinition,
|
|
3593
3690
|
getPlanLabel: () => getPlanLabel,
|
|
3594
3691
|
getTrialDaysRemaining: () => getTrialDaysRemaining,
|
|
3692
|
+
hasCrossPlatformIdentity: () => hasCrossPlatformIdentity,
|
|
3693
|
+
hasPlatformLocator: () => hasPlatformLocator,
|
|
3595
3694
|
hostOfOrigin: () => hostOfOrigin,
|
|
3596
3695
|
isAddonType: () => isAddonType,
|
|
3597
3696
|
isApiTest: () => isApiTest,
|
|
@@ -3601,23 +3700,34 @@ __export(dist_exports, {
|
|
|
3601
3700
|
isCreditPackAmountCents: () => isCreditPackAmountCents,
|
|
3602
3701
|
isDbPricingPlan: () => isDbPricingPlan,
|
|
3603
3702
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
3703
|
+
isMobilePlatform: () => isMobilePlatform,
|
|
3604
3704
|
isPricingPlan: () => isPricingPlan,
|
|
3605
3705
|
isRunnableStep: () => isRunnableStep,
|
|
3606
3706
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
3607
3707
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
3708
|
+
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
3608
3709
|
isTrialExpired: () => isTrialExpired,
|
|
3609
3710
|
isUIVariant: () => isUIVariant,
|
|
3610
3711
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
3712
|
+
mediumForMobilePlatform: () => mediumForMobilePlatform,
|
|
3713
|
+
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
3611
3714
|
normalizeControlName: () => normalizeControlName,
|
|
3715
|
+
otherMobilePlatform: () => otherMobilePlatform,
|
|
3612
3716
|
parenDepthDelta: () => parenDepthDelta,
|
|
3613
3717
|
planHasBillingPortal: () => planHasBillingPortal,
|
|
3614
3718
|
planRequiresCheckout: () => planRequiresCheckout,
|
|
3719
|
+
platformTag: () => platformTag,
|
|
3615
3720
|
registrableDomain: () => registrableDomain,
|
|
3616
3721
|
resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
|
|
3722
|
+
resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
|
|
3617
3723
|
safeOrigin: () => safeOrigin,
|
|
3618
3724
|
sanitizeTestVariables: () => sanitizeTestVariables,
|
|
3619
3725
|
selectorCandidatesForStep: () => selectorCandidatesForStep,
|
|
3620
3726
|
selectorForValue: () => selectorForValue,
|
|
3727
|
+
stepAppliesToPlatform: () => stepAppliesToPlatform,
|
|
3728
|
+
stepIsDivergentForPlatform: () => stepIsDivergentForPlatform,
|
|
3729
|
+
stepsMissingPlatformLocators: () => stepsMissingPlatformLocators,
|
|
3730
|
+
stepsNeedingReviewForPlatform: () => stepsNeedingReviewForPlatform,
|
|
3621
3731
|
stripEnvironmentalPrefix: () => stripEnvironmentalPrefix,
|
|
3622
3732
|
stripLoginScaffolding: () => stripLoginScaffolding,
|
|
3623
3733
|
summarizeHealth: () => summarizeHealth,
|
|
@@ -3631,6 +3741,7 @@ var init_dist = __esm({
|
|
|
3631
3741
|
init_types();
|
|
3632
3742
|
init_constants();
|
|
3633
3743
|
init_mobile_test_generation();
|
|
3744
|
+
init_mobile_cross_platform();
|
|
3634
3745
|
init_mobile_deeplink();
|
|
3635
3746
|
init_plans();
|
|
3636
3747
|
init_audit_pricing();
|
|
@@ -66797,16 +66908,16 @@ ${statePacket}` },
|
|
|
66797
66908
|
}
|
|
66798
66909
|
const screen = screenName();
|
|
66799
66910
|
if (accessibilityId) {
|
|
66800
|
-
const bundle2 = { xpath: `//*[@content-desc=${
|
|
66911
|
+
const bundle2 = { xpath: `//*[@content-desc=${xpathLiteral2(accessibilityId)} or @name=${xpathLiteral2(accessibilityId)}]`, className: "", accessibilityId };
|
|
66801
66912
|
recordAction({ type: "ASSERT_VISIBLE", selector: accessibilityId, metadata: { screenName: screen, targetLabel: accessibilityId, locatorBundle: bundle2 } });
|
|
66802
66913
|
return { ok: true, label: accessibilityId };
|
|
66803
66914
|
}
|
|
66804
66915
|
const safeText = text;
|
|
66805
|
-
const bundle = { xpath: `//*[@text=${
|
|
66916
|
+
const bundle = { xpath: `//*[@text=${xpathLiteral2(safeText)} or @label=${xpathLiteral2(safeText)}]`, className: "", text: safeText };
|
|
66806
66917
|
recordAction({ type: "ASSERT_VISIBLE", selector: bundle.xpath, metadata: { screenName: screen, targetLabel: safeText, locatorBundle: bundle } });
|
|
66807
66918
|
return { ok: true, label: safeText };
|
|
66808
66919
|
}
|
|
66809
|
-
function
|
|
66920
|
+
function xpathLiteral2(value) {
|
|
66810
66921
|
if (!value.includes("'"))
|
|
66811
66922
|
return `'${value}'`;
|
|
66812
66923
|
if (!value.includes('"'))
|
|
@@ -66885,9 +66996,9 @@ ${statePacket}` },
|
|
|
66885
66996
|
if (claimedVerified && !verified) {
|
|
66886
66997
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
66887
66998
|
}
|
|
66888
|
-
const
|
|
66999
|
+
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
66889
67000
|
const reviewTag = verified ? "@verified" : "@needs-review";
|
|
66890
|
-
const tags = ["@discovered", "@mobile",
|
|
67001
|
+
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
66891
67002
|
const test = {
|
|
66892
67003
|
name: compiled.name,
|
|
66893
67004
|
description: recording.finish?.description ?? compiled.description,
|
|
@@ -328123,7 +328234,7 @@ var require_package4 = __commonJS({
|
|
|
328123
328234
|
"package.json"(exports, module) {
|
|
328124
328235
|
module.exports = {
|
|
328125
328236
|
name: "@validate.qa/runner",
|
|
328126
|
-
version: "1.0.
|
|
328237
|
+
version: "1.0.17",
|
|
328127
328238
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328128
328239
|
bin: {
|
|
328129
328240
|
"validate-runner": "dist/cli.js"
|
|
@@ -329667,6 +329778,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
329667
329778
|
|
|
329668
329779
|
// src/services/appium-executor.ts
|
|
329669
329780
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
329781
|
+
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
329670
329782
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
329671
329783
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
329672
329784
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
@@ -329731,7 +329843,7 @@ async function getWindowSize(sessionId, retries = 3) {
|
|
|
329731
329843
|
}
|
|
329732
329844
|
throw new Error("Failed to get window size: invalid dimensions returned");
|
|
329733
329845
|
}
|
|
329734
|
-
function realDeviceCapabilities(platform3) {
|
|
329846
|
+
function realDeviceCapabilities(platform3, portOffset = 0) {
|
|
329735
329847
|
const env = process.env;
|
|
329736
329848
|
const caps = {};
|
|
329737
329849
|
const num = (v) => {
|
|
@@ -329739,6 +329851,7 @@ function realDeviceCapabilities(platform3) {
|
|
|
329739
329851
|
const n = Number.parseInt(v, 10);
|
|
329740
329852
|
return Number.isFinite(n) ? n : void 0;
|
|
329741
329853
|
};
|
|
329854
|
+
const offset = Number.isFinite(portOffset) && portOffset > 0 ? Math.floor(portOffset) : 0;
|
|
329742
329855
|
if (platform3 === "IOS") {
|
|
329743
329856
|
if (env.APPIUM_XCODE_ORG_ID) {
|
|
329744
329857
|
caps["appium:xcodeOrgId"] = env.APPIUM_XCODE_ORG_ID;
|
|
@@ -329748,15 +329861,15 @@ function realDeviceCapabilities(platform3) {
|
|
|
329748
329861
|
}
|
|
329749
329862
|
if (env.APPIUM_UPDATED_WDA_BUNDLE_ID) caps["appium:updatedWDABundleId"] = env.APPIUM_UPDATED_WDA_BUNDLE_ID;
|
|
329750
329863
|
const wdaPort = num(env.APPIUM_WDA_LOCAL_PORT);
|
|
329751
|
-
if (wdaPort !== void 0) caps["appium:wdaLocalPort"] = wdaPort;
|
|
329864
|
+
if (wdaPort !== void 0) caps["appium:wdaLocalPort"] = wdaPort + offset;
|
|
329752
329865
|
} else {
|
|
329753
329866
|
const systemPort = num(env.APPIUM_SYSTEM_PORT);
|
|
329754
|
-
if (systemPort !== void 0) caps["appium:systemPort"] = systemPort;
|
|
329867
|
+
if (systemPort !== void 0) caps["appium:systemPort"] = systemPort + offset;
|
|
329755
329868
|
}
|
|
329756
329869
|
return caps;
|
|
329757
329870
|
}
|
|
329758
329871
|
async function createSession(target) {
|
|
329759
|
-
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform) : {};
|
|
329872
|
+
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform, target.portOffset) : {};
|
|
329760
329873
|
const capabilities = target.platform === "IOS" ? {
|
|
329761
329874
|
platformName: "iOS",
|
|
329762
329875
|
"appium:automationName": "XCUITest",
|
|
@@ -329994,6 +330107,11 @@ function boundsFromStep(step) {
|
|
|
329994
330107
|
}
|
|
329995
330108
|
return null;
|
|
329996
330109
|
}
|
|
330110
|
+
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
330111
|
+
if (isCrossPlatform) return null;
|
|
330112
|
+
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
330113
|
+
return boundsFromStep(step);
|
|
330114
|
+
}
|
|
329997
330115
|
function locatorStrategyForValue(value) {
|
|
329998
330116
|
const trimmed = value?.trim();
|
|
329999
330117
|
if (!trimmed) {
|
|
@@ -330013,24 +330131,68 @@ function locatorStrategyForValue(value) {
|
|
|
330013
330131
|
{ using: "id", value: trimmed }
|
|
330014
330132
|
];
|
|
330015
330133
|
}
|
|
330016
|
-
function buildLocatorStrategies(target) {
|
|
330134
|
+
function buildLocatorStrategies(target, platform3) {
|
|
330017
330135
|
if (!target) return [];
|
|
330018
330136
|
const strategies = [];
|
|
330019
|
-
const bundle = target
|
|
330137
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330020
330138
|
if (bundle) {
|
|
330021
330139
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330022
330140
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
330023
330141
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330024
330142
|
if (bundle.className) strategies.push({ using: "class name", value: bundle.className });
|
|
330025
330143
|
}
|
|
330026
|
-
|
|
330027
|
-
|
|
330028
|
-
|
|
330144
|
+
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
330145
|
+
strategies.push(...locatorStrategyForValue(target.primary));
|
|
330146
|
+
for (const fallback of target.fallbacks ?? []) {
|
|
330147
|
+
strategies.push(...locatorStrategyForValue(fallback));
|
|
330148
|
+
}
|
|
330029
330149
|
}
|
|
330030
330150
|
return strategies.filter(
|
|
330031
330151
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
330032
330152
|
);
|
|
330033
330153
|
}
|
|
330154
|
+
function dedupeStrategies(strategies) {
|
|
330155
|
+
return strategies.filter(
|
|
330156
|
+
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
330157
|
+
);
|
|
330158
|
+
}
|
|
330159
|
+
function xpathLiteral(value) {
|
|
330160
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
330161
|
+
if (!value.includes('"')) return `"${value}"`;
|
|
330162
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
330163
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
330164
|
+
}
|
|
330165
|
+
function identityTextXpath(value) {
|
|
330166
|
+
const lit = xpathLiteral(value);
|
|
330167
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
330168
|
+
}
|
|
330169
|
+
function isIdentityStrategy(strategy) {
|
|
330170
|
+
if (strategy.using === "accessibility id" || strategy.using === "id") return true;
|
|
330171
|
+
if (strategy.using === "xpath") return strategy.value.includes("@");
|
|
330172
|
+
return false;
|
|
330173
|
+
}
|
|
330174
|
+
function buildIdentityLocatorStrategies(target, platform3) {
|
|
330175
|
+
if (!target) return [];
|
|
330176
|
+
const strategies = [];
|
|
330177
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330178
|
+
if (bundle) {
|
|
330179
|
+
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330180
|
+
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
330181
|
+
for (const value of [bundle.text, bundle.contentDesc]) {
|
|
330182
|
+
const trimmed = value?.trim();
|
|
330183
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath(trimmed) });
|
|
330184
|
+
}
|
|
330185
|
+
if (bundle.xpath && bundle.xpath.includes("@")) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330186
|
+
}
|
|
330187
|
+
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
330188
|
+
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
330189
|
+
for (const strategy of locatorStrategyForValue(value)) {
|
|
330190
|
+
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
330191
|
+
}
|
|
330192
|
+
}
|
|
330193
|
+
}
|
|
330194
|
+
return dedupeStrategies(strategies);
|
|
330195
|
+
}
|
|
330034
330196
|
function readStepTimeout(step) {
|
|
330035
330197
|
const value = step.metadata?.timeout;
|
|
330036
330198
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -330038,10 +330200,12 @@ function readStepTimeout(step) {
|
|
|
330038
330200
|
}
|
|
330039
330201
|
return void 0;
|
|
330040
330202
|
}
|
|
330041
|
-
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS) {
|
|
330042
|
-
const strategies = buildLocatorStrategies(target);
|
|
330203
|
+
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
330204
|
+
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
330043
330205
|
if (strategies.length === 0) {
|
|
330044
|
-
throw new Error(
|
|
330206
|
+
throw new Error(
|
|
330207
|
+
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"
|
|
330208
|
+
);
|
|
330045
330209
|
}
|
|
330046
330210
|
const startTime = Date.now();
|
|
330047
330211
|
let lastError = "";
|
|
@@ -330139,18 +330303,18 @@ function shouldAbortAfterStepFailure(action) {
|
|
|
330139
330303
|
return false;
|
|
330140
330304
|
}
|
|
330141
330305
|
}
|
|
330142
|
-
async function executeTap(sessionId, step) {
|
|
330143
|
-
const bounds =
|
|
330306
|
+
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330307
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330144
330308
|
if (step.target) {
|
|
330145
330309
|
try {
|
|
330146
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330310
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330147
330311
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
330148
330312
|
method: "POST",
|
|
330149
330313
|
body: "{}"
|
|
330150
330314
|
});
|
|
330151
330315
|
return;
|
|
330152
330316
|
} catch (error2) {
|
|
330153
|
-
if (!bounds) throw error2;
|
|
330317
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330154
330318
|
}
|
|
330155
330319
|
}
|
|
330156
330320
|
if (!bounds) {
|
|
@@ -330161,7 +330325,7 @@ async function executeTap(sessionId, step) {
|
|
|
330161
330325
|
async function executeType(sessionId, step, context) {
|
|
330162
330326
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
330163
330327
|
const value = String(step.value);
|
|
330164
|
-
const bounds =
|
|
330328
|
+
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
330165
330329
|
let elementId = null;
|
|
330166
330330
|
let lastInputError = null;
|
|
330167
330331
|
let tappedBounds = false;
|
|
@@ -330178,12 +330342,12 @@ async function executeType(sessionId, step, context) {
|
|
|
330178
330342
|
}
|
|
330179
330343
|
if (step.target) {
|
|
330180
330344
|
try {
|
|
330181
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330345
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
330182
330346
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330183
330347
|
if (!lastInputError) return;
|
|
330184
330348
|
elementId = null;
|
|
330185
330349
|
} catch (error2) {
|
|
330186
|
-
if (!bounds) throw error2;
|
|
330350
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
330187
330351
|
lastInputError = error2;
|
|
330188
330352
|
}
|
|
330189
330353
|
}
|
|
@@ -330231,14 +330395,14 @@ async function executeType(sessionId, step, context) {
|
|
|
330231
330395
|
}
|
|
330232
330396
|
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
330233
330397
|
}
|
|
330234
|
-
async function executeClear(sessionId, step) {
|
|
330235
|
-
const bounds =
|
|
330398
|
+
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330399
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330236
330400
|
let elementId = null;
|
|
330237
330401
|
if (step.target) {
|
|
330238
330402
|
try {
|
|
330239
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330403
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330240
330404
|
} catch (error2) {
|
|
330241
|
-
if (!bounds) throw error2;
|
|
330405
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330242
330406
|
}
|
|
330243
330407
|
}
|
|
330244
330408
|
if (!elementId && bounds) {
|
|
@@ -330307,20 +330471,20 @@ async function executeHome(sessionId, platform3) {
|
|
|
330307
330471
|
body: JSON.stringify({ keycode: 3 })
|
|
330308
330472
|
});
|
|
330309
330473
|
}
|
|
330310
|
-
async function executeWaitForElement(sessionId, step) {
|
|
330474
|
+
async function executeWaitForElement(sessionId, step, platform3) {
|
|
330311
330475
|
const timeout = readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS;
|
|
330312
|
-
await findElement(sessionId, step.target, timeout);
|
|
330476
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
330313
330477
|
}
|
|
330314
|
-
async function executeAssertVisible(sessionId, step) {
|
|
330315
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330478
|
+
async function executeAssertVisible(sessionId, step, platform3) {
|
|
330479
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330316
330480
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
330317
330481
|
if (!result?.value) {
|
|
330318
330482
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
330319
330483
|
}
|
|
330320
330484
|
}
|
|
330321
|
-
async function executeAssertText(sessionId, step) {
|
|
330485
|
+
async function executeAssertText(sessionId, step, platform3) {
|
|
330322
330486
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
330323
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330487
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330324
330488
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
330325
330489
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
330326
330490
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -330355,6 +330519,7 @@ async function executeMobileTest(options) {
|
|
|
330355
330519
|
const screenshots = [];
|
|
330356
330520
|
let apiCalls = [];
|
|
330357
330521
|
let sessionId = null;
|
|
330522
|
+
let selectedDevice;
|
|
330358
330523
|
let capture = null;
|
|
330359
330524
|
let captureStopped = false;
|
|
330360
330525
|
let recordingStarted = false;
|
|
@@ -330402,7 +330567,6 @@ async function executeMobileTest(options) {
|
|
|
330402
330567
|
`No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
|
|
330403
330568
|
);
|
|
330404
330569
|
}
|
|
330405
|
-
let selectedDevice;
|
|
330406
330570
|
if (options.target.assignedDeviceId) {
|
|
330407
330571
|
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
330408
330572
|
if (!selectedDevice) {
|
|
@@ -330443,13 +330607,15 @@ async function executeMobileTest(options) {
|
|
|
330443
330607
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
330444
330608
|
}
|
|
330445
330609
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
330610
|
+
const leasedPortOffset = Math.max(0, matchingDevices.findIndex((d) => d.id === selectedDevice?.id));
|
|
330446
330611
|
try {
|
|
330447
330612
|
sessionId = await createSessionWithRetry({
|
|
330448
330613
|
appId: options.target.appId,
|
|
330449
330614
|
platform: options.target.platform,
|
|
330450
330615
|
deviceId: selectedDevice.id,
|
|
330451
330616
|
platformVersion: options.target.platformVersion ?? selectedDevice.platformVersion,
|
|
330452
|
-
isPhysical: selectedDevice.isPhysical
|
|
330617
|
+
isPhysical: selectedDevice.isPhysical,
|
|
330618
|
+
portOffset: leasedPortOffset
|
|
330453
330619
|
}, (msg) => log2(msg));
|
|
330454
330620
|
} catch (err) {
|
|
330455
330621
|
const base2 = err instanceof Error ? err.message : String(err);
|
|
@@ -330465,6 +330631,12 @@ async function executeMobileTest(options) {
|
|
|
330465
330631
|
throw err;
|
|
330466
330632
|
}
|
|
330467
330633
|
log2(`Session created: ${sessionId}`);
|
|
330634
|
+
if (selectedDevice.id) {
|
|
330635
|
+
try {
|
|
330636
|
+
options.onSessionReady?.(sessionId, selectedDevice.id, options.target.platform);
|
|
330637
|
+
} catch {
|
|
330638
|
+
}
|
|
330639
|
+
}
|
|
330468
330640
|
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
330469
330641
|
log2("Opening a fresh app instance for the test...");
|
|
330470
330642
|
await freshLaunchTargetApp(sessionId, options.target.platform, options.target.appId, log2);
|
|
@@ -330477,8 +330649,34 @@ async function executeMobileTest(options) {
|
|
|
330477
330649
|
}
|
|
330478
330650
|
}
|
|
330479
330651
|
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
330652
|
+
let entryDrift = false;
|
|
330653
|
+
const runPlatform = options.target.platform;
|
|
330654
|
+
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
330655
|
+
const entryStep = options.steps.find(
|
|
330656
|
+
(s) => s.action !== "launchApp" && stepAppliesToPlatform(s, runPlatform) && buildIdentityLocatorStrategies(s.target, runPlatform).length > 0
|
|
330657
|
+
);
|
|
330658
|
+
if (entryStep) {
|
|
330659
|
+
try {
|
|
330660
|
+
await findElement(sessionId, entryStep.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true, platform: runPlatform });
|
|
330661
|
+
} catch (err) {
|
|
330662
|
+
if (isTransportError(err)) throw err;
|
|
330663
|
+
entryDrift = true;
|
|
330664
|
+
const driftMsg = `App started on an unexpected screen \u2014 the recorded flow's first element ("${entryStep.target?.primary ?? entryStep.description}") is not present, so the replay was aborted before it could blind-tap stale coordinates (screen drift; the app may be on onboarding or a different screen than when the test was recorded).`;
|
|
330665
|
+
log2(` \u2717 Entry-screen drift: ${driftMsg}`);
|
|
330666
|
+
stepResults.push({ stepOrder: entryStep.order, status: "failed", duration: 0, error: driftMsg });
|
|
330667
|
+
const driftShot = await takeScreenshot(sessionId).catch(() => null);
|
|
330668
|
+
screenshots.push(driftShot ?? "");
|
|
330669
|
+
}
|
|
330670
|
+
}
|
|
330480
330671
|
let environmental = null;
|
|
330481
330672
|
for (const step of options.steps) {
|
|
330673
|
+
if (entryDrift) break;
|
|
330674
|
+
if (!stepAppliesToPlatform(step, runPlatform)) {
|
|
330675
|
+
log2(`Step ${step.order}: ${step.action} \u2014 skipped (not applicable to ${runPlatform})`);
|
|
330676
|
+
stepResults.push({ stepOrder: step.order, status: "skipped", duration: 0 });
|
|
330677
|
+
screenshots.push("");
|
|
330678
|
+
continue;
|
|
330679
|
+
}
|
|
330482
330680
|
const stepStart = Date.now();
|
|
330483
330681
|
log2(`Step ${step.order}: ${step.action} - ${step.description}`);
|
|
330484
330682
|
let status = "passed";
|
|
@@ -330490,17 +330688,18 @@ async function executeMobileTest(options) {
|
|
|
330490
330688
|
log2(" App launched (handled by session creation)");
|
|
330491
330689
|
break;
|
|
330492
330690
|
case "tap":
|
|
330493
|
-
await executeTap(sessionId, step);
|
|
330691
|
+
await executeTap(sessionId, step, runPlatform, isCrossPlatform);
|
|
330494
330692
|
break;
|
|
330495
330693
|
case "type":
|
|
330496
330694
|
await executeType(sessionId, step, {
|
|
330497
|
-
platform:
|
|
330695
|
+
platform: runPlatform,
|
|
330498
330696
|
deviceId: selectedDevice.id,
|
|
330499
|
-
log: log2
|
|
330697
|
+
log: log2,
|
|
330698
|
+
isCrossPlatform
|
|
330500
330699
|
});
|
|
330501
330700
|
break;
|
|
330502
330701
|
case "clear":
|
|
330503
|
-
await executeClear(sessionId, step);
|
|
330702
|
+
await executeClear(sessionId, step, runPlatform, isCrossPlatform);
|
|
330504
330703
|
break;
|
|
330505
330704
|
case "swipe":
|
|
330506
330705
|
case "scroll":
|
|
@@ -330510,16 +330709,16 @@ async function executeMobileTest(options) {
|
|
|
330510
330709
|
await executeBack(sessionId);
|
|
330511
330710
|
break;
|
|
330512
330711
|
case "home":
|
|
330513
|
-
await executeHome(sessionId,
|
|
330712
|
+
await executeHome(sessionId, runPlatform);
|
|
330514
330713
|
break;
|
|
330515
330714
|
case "waitForElement":
|
|
330516
|
-
await executeWaitForElement(sessionId, step);
|
|
330715
|
+
await executeWaitForElement(sessionId, step, runPlatform);
|
|
330517
330716
|
break;
|
|
330518
330717
|
case "assertVisible":
|
|
330519
|
-
await executeAssertVisible(sessionId, step);
|
|
330718
|
+
await executeAssertVisible(sessionId, step, runPlatform);
|
|
330520
330719
|
break;
|
|
330521
330720
|
case "assertText":
|
|
330522
|
-
await executeAssertText(sessionId, step);
|
|
330721
|
+
await executeAssertText(sessionId, step, runPlatform);
|
|
330523
330722
|
break;
|
|
330524
330723
|
default: {
|
|
330525
330724
|
const _exhaustive = step.action;
|
|
@@ -330556,8 +330755,14 @@ async function executeMobileTest(options) {
|
|
|
330556
330755
|
break;
|
|
330557
330756
|
}
|
|
330558
330757
|
}
|
|
330559
|
-
const
|
|
330560
|
-
const
|
|
330758
|
+
const executedResults = stepResults.filter((r) => r.status !== "skipped");
|
|
330759
|
+
const verifyOrders = new Set(
|
|
330760
|
+
options.steps.filter((s) => VERIFICATION_ACTIONS.has(s.action)).map((s) => s.order)
|
|
330761
|
+
);
|
|
330762
|
+
const anyVerificationPassed = stepResults.some((r) => r.status === "passed" && verifyOrders.has(r.stepOrder));
|
|
330763
|
+
const crossPlatformUnverified = isCrossPlatform && verifyOrders.size > 0 && !anyVerificationPassed;
|
|
330764
|
+
const allPassed = !environmental && executedResults.length > 0 && !crossPlatformUnverified && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
330765
|
+
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null) ?? (crossPlatformUnverified ? `Cross-platform run on ${runPlatform} verified no outcome \u2014 every assertion/wait step was skipped or unresolved. Author a ${runPlatform} assertion or resolve the divergence.` : null);
|
|
330561
330766
|
await stopCapture();
|
|
330562
330767
|
await stopRecording();
|
|
330563
330768
|
return {
|
|
@@ -330569,6 +330774,8 @@ async function executeMobileTest(options) {
|
|
|
330569
330774
|
logs: logLines.join("\n"),
|
|
330570
330775
|
error: firstError,
|
|
330571
330776
|
duration: Date.now() - startTime,
|
|
330777
|
+
deviceId: selectedDevice?.id,
|
|
330778
|
+
deviceName: selectedDevice?.name,
|
|
330572
330779
|
environmental: environmental ?? void 0
|
|
330573
330780
|
};
|
|
330574
330781
|
} catch (err) {
|
|
@@ -330585,6 +330792,8 @@ async function executeMobileTest(options) {
|
|
|
330585
330792
|
logs: logLines.join("\n"),
|
|
330586
330793
|
error: errorMsg,
|
|
330587
330794
|
duration: Date.now() - startTime,
|
|
330795
|
+
deviceId: selectedDevice?.id,
|
|
330796
|
+
deviceName: selectedDevice?.name,
|
|
330588
330797
|
// A throw before/around the step loop is a setup/environment problem
|
|
330589
330798
|
// (no device booted, app not installed, session could not be created,
|
|
330590
330799
|
// Appium unreachable) — never a genuine test assertion failure.
|
|
@@ -330594,6 +330803,12 @@ async function executeMobileTest(options) {
|
|
|
330594
330803
|
await stopCapture();
|
|
330595
330804
|
await stopRecording();
|
|
330596
330805
|
if (sessionId) {
|
|
330806
|
+
if (selectedDevice?.id) {
|
|
330807
|
+
try {
|
|
330808
|
+
options.onSessionEnding?.(selectedDevice.id);
|
|
330809
|
+
} catch {
|
|
330810
|
+
}
|
|
330811
|
+
}
|
|
330597
330812
|
try {
|
|
330598
330813
|
log2("Closing the app on the device...");
|
|
330599
330814
|
await terminateTargetApp(sessionId, options.target.platform, options.target.appId);
|
|
@@ -330652,7 +330867,9 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330652
330867
|
platform: target.platform,
|
|
330653
330868
|
deviceId: selectedDevice.id,
|
|
330654
330869
|
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
330655
|
-
isPhysical: selectedDevice.isPhysical
|
|
330870
|
+
isPhysical: selectedDevice.isPhysical,
|
|
330871
|
+
// Distinct control port per concurrent physical device (no-op for emulators/sims).
|
|
330872
|
+
portOffset: Math.max(0, matchingDevices.findIndex((d) => d.id === selectedDevice?.id))
|
|
330656
330873
|
});
|
|
330657
330874
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
330658
330875
|
try {
|
|
@@ -330664,6 +330881,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330664
330881
|
return {
|
|
330665
330882
|
sessionId,
|
|
330666
330883
|
deviceId: selectedDevice.id,
|
|
330884
|
+
deviceName: selectedDevice.name,
|
|
330667
330885
|
isPhysical: selectedDevice.isPhysical === true,
|
|
330668
330886
|
platform: target.platform,
|
|
330669
330887
|
appId: target.appId,
|
|
@@ -330678,6 +330896,184 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330678
330896
|
}
|
|
330679
330897
|
};
|
|
330680
330898
|
}
|
|
330899
|
+
async function readElementAttribute(sessionId, elementId, name) {
|
|
330900
|
+
try {
|
|
330901
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(name)}`);
|
|
330902
|
+
const value = result?.value;
|
|
330903
|
+
if (typeof value !== "string") return void 0;
|
|
330904
|
+
const trimmed = value.trim();
|
|
330905
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
330906
|
+
} catch (err) {
|
|
330907
|
+
if (isTransportError(err)) throw err;
|
|
330908
|
+
return void 0;
|
|
330909
|
+
}
|
|
330910
|
+
}
|
|
330911
|
+
function harvestFallbackXpath(platform3, attrs) {
|
|
330912
|
+
if (platform3 === "IOS") {
|
|
330913
|
+
if (attrs.name) return `//*[@name=${xpathLiteral(attrs.name)}]`;
|
|
330914
|
+
const text = attrs.label ?? attrs.value;
|
|
330915
|
+
if (text) return identityTextXpath(text);
|
|
330916
|
+
return "//*";
|
|
330917
|
+
}
|
|
330918
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral(attrs.resourceId)}]`;
|
|
330919
|
+
if (attrs.text) return identityTextXpath(attrs.text);
|
|
330920
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral(attrs.contentDesc)}]`;
|
|
330921
|
+
return "//*";
|
|
330922
|
+
}
|
|
330923
|
+
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
330924
|
+
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
330925
|
+
if (platform3 === "IOS") {
|
|
330926
|
+
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
330927
|
+
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
330928
|
+
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
330929
|
+
const text2 = label ?? value;
|
|
330930
|
+
if (!name && !text2) return null;
|
|
330931
|
+
const bundle2 = {
|
|
330932
|
+
xpath: harvestFallbackXpath("IOS", { name, label, value }),
|
|
330933
|
+
className
|
|
330934
|
+
};
|
|
330935
|
+
if (name) bundle2.accessibilityId = name;
|
|
330936
|
+
if (text2) bundle2.text = text2;
|
|
330937
|
+
return bundle2;
|
|
330938
|
+
}
|
|
330939
|
+
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
330940
|
+
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
330941
|
+
const text = await readElementAttribute(sessionId, elementId, "text");
|
|
330942
|
+
if (!resourceId && !contentDesc && !text) return null;
|
|
330943
|
+
const bundle = {
|
|
330944
|
+
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
|
330945
|
+
className
|
|
330946
|
+
};
|
|
330947
|
+
if (contentDesc) bundle.accessibilityId = contentDesc;
|
|
330948
|
+
if (resourceId) bundle.resourceId = resourceId;
|
|
330949
|
+
if (text) bundle.text = text;
|
|
330950
|
+
if (contentDesc) bundle.contentDesc = contentDesc;
|
|
330951
|
+
return bundle;
|
|
330952
|
+
}
|
|
330953
|
+
function isStateAdvancingAction(action) {
|
|
330954
|
+
return action === "tap" || action === "type" || action === "clear" || action === "swipe" || action === "scroll" || action === "back" || action === "home";
|
|
330955
|
+
}
|
|
330956
|
+
function stepNeedsLocator(action) {
|
|
330957
|
+
return action === "tap" || action === "type" || action === "clear" || action === "waitForElement" || action === "assertVisible" || action === "assertText";
|
|
330958
|
+
}
|
|
330959
|
+
async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
330960
|
+
switch (step.action) {
|
|
330961
|
+
case "tap":
|
|
330962
|
+
if (!elementId) return false;
|
|
330963
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
330964
|
+
return true;
|
|
330965
|
+
case "type": {
|
|
330966
|
+
if (step.value === void 0 || step.value === null || step.value === "") return true;
|
|
330967
|
+
if (!elementId) return false;
|
|
330968
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
330969
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330970
|
+
const err = await trySendElementValue(sessionId, elementId, String(step.value));
|
|
330971
|
+
return !err;
|
|
330972
|
+
}
|
|
330973
|
+
case "clear":
|
|
330974
|
+
if (!elementId) return false;
|
|
330975
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
330976
|
+
return true;
|
|
330977
|
+
case "swipe":
|
|
330978
|
+
case "scroll":
|
|
330979
|
+
await executeSwipe(sessionId, step);
|
|
330980
|
+
return true;
|
|
330981
|
+
case "back":
|
|
330982
|
+
await executeBack(sessionId);
|
|
330983
|
+
return true;
|
|
330984
|
+
case "home":
|
|
330985
|
+
await executeHome(sessionId, platform3);
|
|
330986
|
+
return true;
|
|
330987
|
+
case "launchApp":
|
|
330988
|
+
case "waitForElement":
|
|
330989
|
+
case "assertVisible":
|
|
330990
|
+
case "assertText":
|
|
330991
|
+
return true;
|
|
330992
|
+
}
|
|
330993
|
+
}
|
|
330994
|
+
async function harvestPlatformLocators(options) {
|
|
330995
|
+
const platform3 = options.target.platform;
|
|
330996
|
+
const logLines = [];
|
|
330997
|
+
const log2 = (msg) => {
|
|
330998
|
+
logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
330999
|
+
};
|
|
331000
|
+
const bundles = {};
|
|
331001
|
+
const divergentOrders = [];
|
|
331002
|
+
let reachedOrder = null;
|
|
331003
|
+
let environmentalError;
|
|
331004
|
+
log2(`Cross-platform locator discovery \u2014 resolving ${platform3} locators for ${options.steps.length} steps`);
|
|
331005
|
+
const session = await createMobileDiscoverySession({
|
|
331006
|
+
appId: options.target.appId,
|
|
331007
|
+
platform: platform3,
|
|
331008
|
+
platformVersion: options.target.platformVersion,
|
|
331009
|
+
recordedDeviceId: options.target.recordedDeviceId,
|
|
331010
|
+
assignedDeviceId: options.target.assignedDeviceId,
|
|
331011
|
+
launchMode: "LAUNCH_APP"
|
|
331012
|
+
});
|
|
331013
|
+
options.onSessionReady?.(session.sessionId, session.deviceId, platform3);
|
|
331014
|
+
try {
|
|
331015
|
+
await freshLaunchTargetApp(session.sessionId, platform3, options.target.appId, log2);
|
|
331016
|
+
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
331017
|
+
try {
|
|
331018
|
+
await openDeepLink(session.sessionId, platform3, options.target.deeplink, options.target.appId);
|
|
331019
|
+
} catch (err) {
|
|
331020
|
+
log2(` Deep link failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
331021
|
+
}
|
|
331022
|
+
}
|
|
331023
|
+
await restoreTargetAppIfExternal(session.sessionId, { appId: options.target.appId, platform: platform3 }, log2, "harvest start", false);
|
|
331024
|
+
for (const step of options.steps) {
|
|
331025
|
+
if (step.action === "launchApp") continue;
|
|
331026
|
+
if (!stepAppliesToPlatform(step, platform3)) continue;
|
|
331027
|
+
let elementId = null;
|
|
331028
|
+
const identityStrategies = buildIdentityLocatorStrategies(step.target);
|
|
331029
|
+
if (identityStrategies.length > 0 && step.target) {
|
|
331030
|
+
try {
|
|
331031
|
+
elementId = await findElement(session.sessionId, step.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true });
|
|
331032
|
+
} catch (err) {
|
|
331033
|
+
if (isTransportError(err)) throw err;
|
|
331034
|
+
elementId = null;
|
|
331035
|
+
}
|
|
331036
|
+
}
|
|
331037
|
+
if (elementId) {
|
|
331038
|
+
const bundle = await readNativeElementBundle(session.sessionId, elementId, platform3);
|
|
331039
|
+
if (bundle) {
|
|
331040
|
+
bundles[step.order] = bundle;
|
|
331041
|
+
log2(` \u2713 Step ${step.order} (${step.action}) resolved on ${platform3}`);
|
|
331042
|
+
} else {
|
|
331043
|
+
divergentOrders.push(step.order);
|
|
331044
|
+
log2(` \u26A0 Step ${step.order} (${step.action}) resolved but has no durable identity \u2014 flagged divergent`);
|
|
331045
|
+
}
|
|
331046
|
+
} else if (stepNeedsLocator(step.action)) {
|
|
331047
|
+
divergentOrders.push(step.order);
|
|
331048
|
+
log2(` \u26A0 Step ${step.order} (${step.action}) could not be resolved on ${platform3} \u2014 needs manual authoring`);
|
|
331049
|
+
}
|
|
331050
|
+
const advanced = await driveHarvestStep(session.sessionId, step, elementId, platform3);
|
|
331051
|
+
if (!advanced && isStateAdvancingAction(step.action)) {
|
|
331052
|
+
log2(` \u2717 Could not advance step ${step.order} (${step.action}) on ${platform3}; stopping harvest early`);
|
|
331053
|
+
break;
|
|
331054
|
+
}
|
|
331055
|
+
reachedOrder = step.order;
|
|
331056
|
+
await restoreTargetAppIfExternal(session.sessionId, { appId: options.target.appId, platform: platform3 }, log2, `harvest step ${step.order}`, false);
|
|
331057
|
+
}
|
|
331058
|
+
} catch (err) {
|
|
331059
|
+
environmentalError = err instanceof Error ? err.message : String(err);
|
|
331060
|
+
log2(` \u2717 Harvest aborted: ${environmentalError}`);
|
|
331061
|
+
} finally {
|
|
331062
|
+
options.onSessionEnding?.(session.deviceId);
|
|
331063
|
+
await session.stop().catch(() => {
|
|
331064
|
+
});
|
|
331065
|
+
}
|
|
331066
|
+
return {
|
|
331067
|
+
platform: platform3,
|
|
331068
|
+
bundles,
|
|
331069
|
+
divergentOrders,
|
|
331070
|
+
reachedOrder,
|
|
331071
|
+
deviceId: session.deviceId,
|
|
331072
|
+
deviceName: session.deviceName,
|
|
331073
|
+
logs: logLines.join("\n"),
|
|
331074
|
+
...environmentalError ? { environmentalError } : {}
|
|
331075
|
+
};
|
|
331076
|
+
}
|
|
330681
331077
|
|
|
330682
331078
|
// src/mobile-device-queue.ts
|
|
330683
331079
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
@@ -331389,7 +331785,146 @@ async function handleSessionStop(_req, res) {
|
|
|
331389
331785
|
stopSessionReaper();
|
|
331390
331786
|
sendJSON(res, 200, { ok: true });
|
|
331391
331787
|
}
|
|
331788
|
+
var runnerChannels = /* @__PURE__ */ new Map();
|
|
331789
|
+
function readDeviceIdParam(req) {
|
|
331790
|
+
try {
|
|
331791
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
331792
|
+
const raw = url.searchParams.get("deviceId");
|
|
331793
|
+
if (!raw) return null;
|
|
331794
|
+
const trimmed = raw.trim();
|
|
331795
|
+
if (!trimmed || trimmed.length > MAX_DEVICE_ID_LENGTH || !DEVICE_ID_PATTERN.test(trimmed)) return null;
|
|
331796
|
+
return trimmed;
|
|
331797
|
+
} catch {
|
|
331798
|
+
return null;
|
|
331799
|
+
}
|
|
331800
|
+
}
|
|
331801
|
+
async function isSessionAlive(sessionId) {
|
|
331802
|
+
try {
|
|
331803
|
+
validateSessionId(sessionId);
|
|
331804
|
+
await appiumRequest2(`/session/${sessionId}/window/rect`);
|
|
331805
|
+
return true;
|
|
331806
|
+
} catch (err) {
|
|
331807
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
331808
|
+
return !/no such session|invalid session id|session is either terminated|deleted session/i.test(msg);
|
|
331809
|
+
}
|
|
331810
|
+
}
|
|
331811
|
+
function writeFrameToChannelClient(ch, client, frame) {
|
|
331812
|
+
try {
|
|
331813
|
+
const buffered = typeof client.writableLength === "number" ? client.writableLength : typeof client.socket?.writableLength === "number" ? client.socket.writableLength : 0;
|
|
331814
|
+
if (buffered > SSE_MAX_BUFFERED_BYTES) {
|
|
331815
|
+
try {
|
|
331816
|
+
client.end();
|
|
331817
|
+
} catch {
|
|
331818
|
+
}
|
|
331819
|
+
ch.clients.delete(client);
|
|
331820
|
+
stopChannelPollingIfIdle(ch);
|
|
331821
|
+
return;
|
|
331822
|
+
}
|
|
331823
|
+
client.write(`data: ${JSON.stringify(frame)}
|
|
331824
|
+
|
|
331825
|
+
`);
|
|
331826
|
+
} catch {
|
|
331827
|
+
}
|
|
331828
|
+
}
|
|
331829
|
+
async function pollRunnerChannel(ch) {
|
|
331830
|
+
if (ch.clients.size === 0 || ch.polling) return;
|
|
331831
|
+
ch.polling = true;
|
|
331832
|
+
try {
|
|
331833
|
+
validateSessionId(ch.sessionId);
|
|
331834
|
+
const [screenshot, source] = await Promise.all([
|
|
331835
|
+
getScreenshot(`/session/${ch.sessionId}`),
|
|
331836
|
+
getPageSource(`/session/${ch.sessionId}`)
|
|
331837
|
+
]);
|
|
331838
|
+
if (!screenshot && !source) {
|
|
331839
|
+
ch.nullTicks++;
|
|
331840
|
+
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
331841
|
+
const alive = await isSessionAlive(ch.sessionId);
|
|
331842
|
+
if (!alive) {
|
|
331843
|
+
closeRunnerChannel(ch);
|
|
331844
|
+
return;
|
|
331845
|
+
}
|
|
331846
|
+
ch.nullTicks = 0;
|
|
331847
|
+
}
|
|
331848
|
+
} else {
|
|
331849
|
+
ch.nullTicks = 0;
|
|
331850
|
+
}
|
|
331851
|
+
const frame = { timestamp: Date.now(), screenshot: screenshot ?? null, source: source ?? null };
|
|
331852
|
+
ch.lastFrame = frame;
|
|
331853
|
+
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
331854
|
+
} catch {
|
|
331855
|
+
} finally {
|
|
331856
|
+
ch.polling = false;
|
|
331857
|
+
}
|
|
331858
|
+
}
|
|
331859
|
+
function ensureChannelPolling(ch) {
|
|
331860
|
+
if (ch.pollTimer) return;
|
|
331861
|
+
ch.pollTimer = setInterval(() => {
|
|
331862
|
+
void pollRunnerChannel(ch);
|
|
331863
|
+
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
331864
|
+
ch.pollTimer.unref?.();
|
|
331865
|
+
}
|
|
331866
|
+
function stopChannelPollingIfIdle(ch) {
|
|
331867
|
+
if (ch.clients.size === 0 && ch.pollTimer) {
|
|
331868
|
+
clearInterval(ch.pollTimer);
|
|
331869
|
+
ch.pollTimer = null;
|
|
331870
|
+
}
|
|
331871
|
+
}
|
|
331872
|
+
function closeRunnerChannel(ch) {
|
|
331873
|
+
if (ch.pollTimer) {
|
|
331874
|
+
clearInterval(ch.pollTimer);
|
|
331875
|
+
ch.pollTimer = null;
|
|
331876
|
+
}
|
|
331877
|
+
for (const client of ch.clients) {
|
|
331878
|
+
try {
|
|
331879
|
+
client.end();
|
|
331880
|
+
} catch {
|
|
331881
|
+
}
|
|
331882
|
+
}
|
|
331883
|
+
ch.clients.clear();
|
|
331884
|
+
ch.lastFrame = null;
|
|
331885
|
+
runnerChannels.delete(ch.deviceId);
|
|
331886
|
+
}
|
|
331887
|
+
function closeAllRunnerChannels() {
|
|
331888
|
+
for (const ch of [...runnerChannels.values()]) closeRunnerChannel(ch);
|
|
331889
|
+
}
|
|
331890
|
+
async function serveRunnerMirror(req, res, ch) {
|
|
331891
|
+
res.writeHead(200, {
|
|
331892
|
+
"Content-Type": "text/event-stream",
|
|
331893
|
+
"Cache-Control": "no-cache",
|
|
331894
|
+
"Connection": "keep-alive",
|
|
331895
|
+
"Access-Control-Allow-Origin": getAllowedOrigin(req),
|
|
331896
|
+
"X-Content-Type-Options": "nosniff"
|
|
331897
|
+
});
|
|
331898
|
+
ch.clients.add(res);
|
|
331899
|
+
ensureChannelPolling(ch);
|
|
331900
|
+
if (ch.lastFrame) {
|
|
331901
|
+
writeFrameToChannelClient(ch, res, ch.lastFrame);
|
|
331902
|
+
} else {
|
|
331903
|
+
await pollRunnerChannel(ch);
|
|
331904
|
+
}
|
|
331905
|
+
req.on("close", () => {
|
|
331906
|
+
ch.clients.delete(res);
|
|
331907
|
+
stopChannelPollingIfIdle(ch);
|
|
331908
|
+
});
|
|
331909
|
+
}
|
|
331392
331910
|
async function handleMirror(req, res) {
|
|
331911
|
+
const deviceId = readDeviceIdParam(req);
|
|
331912
|
+
if (deviceId) {
|
|
331913
|
+
const ch = runnerChannels.get(deviceId);
|
|
331914
|
+
if (ch) {
|
|
331915
|
+
await serveRunnerMirror(req, res, ch);
|
|
331916
|
+
return;
|
|
331917
|
+
}
|
|
331918
|
+
sendJSON(res, 404, { error: `No live mirror for device ${deviceId}.` });
|
|
331919
|
+
return;
|
|
331920
|
+
}
|
|
331921
|
+
if (!state.appiumSessionId && runnerChannels.size === 1) {
|
|
331922
|
+
const soleChannel = runnerChannels.values().next().value;
|
|
331923
|
+
if (soleChannel) {
|
|
331924
|
+
await serveRunnerMirror(req, res, soleChannel);
|
|
331925
|
+
return;
|
|
331926
|
+
}
|
|
331927
|
+
}
|
|
331393
331928
|
if (!state.appiumSessionId) {
|
|
331394
331929
|
sendJSON(res, 400, { error: "No active Appium session. Call /bridge/session/start first." });
|
|
331395
331930
|
return;
|
|
@@ -331836,6 +332371,7 @@ async function startMobileBridge(options) {
|
|
|
331836
332371
|
}
|
|
331837
332372
|
async function stopMobileBridge() {
|
|
331838
332373
|
stopAllMirrorClients();
|
|
332374
|
+
closeAllRunnerChannels();
|
|
331839
332375
|
stopSessionReaper();
|
|
331840
332376
|
clearBridgePidFile();
|
|
331841
332377
|
if (state.appiumSessionId) {
|
|
@@ -331884,8 +332420,31 @@ function getBridgeState() {
|
|
|
331884
332420
|
function setExecutorBusy(busy) {
|
|
331885
332421
|
executorBusyCount = busy ? executorBusyCount + 1 : Math.max(0, executorBusyCount - 1);
|
|
331886
332422
|
}
|
|
331887
|
-
function attachMirrorSession(sessionId, platform3) {
|
|
332423
|
+
function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
331888
332424
|
validateSessionId(sessionId);
|
|
332425
|
+
if (deviceId) {
|
|
332426
|
+
const existing = runnerChannels.get(deviceId);
|
|
332427
|
+
if (existing && existing.sessionId !== sessionId) closeRunnerChannel(existing);
|
|
332428
|
+
const ch = runnerChannels.get(deviceId) ?? {
|
|
332429
|
+
deviceId,
|
|
332430
|
+
sessionId,
|
|
332431
|
+
platform: platform3,
|
|
332432
|
+
clients: /* @__PURE__ */ new Set(),
|
|
332433
|
+
lastFrame: null,
|
|
332434
|
+
pollTimer: null,
|
|
332435
|
+
nullTicks: 0,
|
|
332436
|
+
polling: false
|
|
332437
|
+
};
|
|
332438
|
+
ch.sessionId = sessionId;
|
|
332439
|
+
ch.platform = platform3;
|
|
332440
|
+
ch.nullTicks = 0;
|
|
332441
|
+
runnerChannels.set(deviceId, ch);
|
|
332442
|
+
if (ch.clients.size > 0) {
|
|
332443
|
+
ensureChannelPolling(ch);
|
|
332444
|
+
void pollRunnerChannel(ch);
|
|
332445
|
+
}
|
|
332446
|
+
return;
|
|
332447
|
+
}
|
|
331889
332448
|
if (state.appiumSessionId && state.appiumSessionId !== sessionId) {
|
|
331890
332449
|
stopAllMirrorClients();
|
|
331891
332450
|
}
|
|
@@ -331900,8 +332459,12 @@ function attachMirrorSession(sessionId, platform3) {
|
|
|
331900
332459
|
void pollAndBroadcastFrame();
|
|
331901
332460
|
}
|
|
331902
332461
|
}
|
|
331903
|
-
function detachMirrorSession(
|
|
331904
|
-
if (
|
|
332462
|
+
function detachMirrorSession(deviceId) {
|
|
332463
|
+
if (deviceId) {
|
|
332464
|
+
const ch = runnerChannels.get(deviceId);
|
|
332465
|
+
if (ch) closeRunnerChannel(ch);
|
|
332466
|
+
return;
|
|
332467
|
+
}
|
|
331905
332468
|
state.appiumSessionId = null;
|
|
331906
332469
|
state.platform = null;
|
|
331907
332470
|
mirrorSessionOwner = null;
|
|
@@ -333768,6 +334331,102 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
333768
334331
|
log.fail("ERROR - No discovery context");
|
|
333769
334332
|
return;
|
|
333770
334333
|
}
|
|
334334
|
+
const locatorCtx = ctx;
|
|
334335
|
+
if (locatorCtx.mode === "locator-discovery") {
|
|
334336
|
+
const harvestTarget = locatorCtx.target;
|
|
334337
|
+
const targetTestCaseId = locatorCtx.targetTestCaseId;
|
|
334338
|
+
const platform3 = locatorCtx.platform ?? harvestTarget?.platform;
|
|
334339
|
+
if (!platform3 || !harvestTarget?.appId || !targetTestCaseId) {
|
|
334340
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334341
|
+
status: "ERROR",
|
|
334342
|
+
error: "CONFIG_ISSUE: locator-discovery run is missing platform/appId/targetTestCaseId."
|
|
334343
|
+
});
|
|
334344
|
+
log.fail("ERROR - malformed locator-discovery context");
|
|
334345
|
+
return;
|
|
334346
|
+
}
|
|
334347
|
+
if (getBridgeState().hasActiveSession) {
|
|
334348
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334349
|
+
status: "ERROR",
|
|
334350
|
+
error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; retry once it ends."
|
|
334351
|
+
});
|
|
334352
|
+
log.fail("ERROR - device busy (recording) for locator discovery");
|
|
334353
|
+
return;
|
|
334354
|
+
}
|
|
334355
|
+
const leasedName = assignedDeviceId ? getCachedDeviceSnapshot().find((d) => d.id === assignedDeviceId)?.name : void 0;
|
|
334356
|
+
liveBrowserHandle.patch({
|
|
334357
|
+
mode: "discovery",
|
|
334358
|
+
testName: "Cross-platform locator discovery",
|
|
334359
|
+
note: `${platform3} locator discovery`,
|
|
334360
|
+
surface: "mobile",
|
|
334361
|
+
mobilePlatform: platform3,
|
|
334362
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
|
|
334363
|
+
...leasedName ? { deviceName: leasedName } : {}
|
|
334364
|
+
});
|
|
334365
|
+
requestLiveBrowserHeartbeat(true);
|
|
334366
|
+
log.info(`Cross-platform locator discovery \u2014 ${platform3} / ${harvestTarget.appId} for test ${targetTestCaseId}`);
|
|
334367
|
+
setExecutorBusy(true);
|
|
334368
|
+
try {
|
|
334369
|
+
const harvest = await harvestPlatformLocators({
|
|
334370
|
+
steps: locatorCtx.sourceSteps ?? [],
|
|
334371
|
+
target: {
|
|
334372
|
+
appId: harvestTarget.appId,
|
|
334373
|
+
platform: platform3,
|
|
334374
|
+
platformVersion: harvestTarget.platformVersion,
|
|
334375
|
+
recordedDeviceId: harvestTarget.recordedDeviceId,
|
|
334376
|
+
assignedDeviceId,
|
|
334377
|
+
launchMode: harvestTarget.launchMode,
|
|
334378
|
+
deeplink: harvestTarget.deeplink
|
|
334379
|
+
},
|
|
334380
|
+
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
334381
|
+
attachMirrorSession(sessionId, sessionPlatform, deviceId);
|
|
334382
|
+
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
334383
|
+
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
334384
|
+
requestLiveBrowserHeartbeat(true);
|
|
334385
|
+
},
|
|
334386
|
+
onSessionEnding: (deviceId) => {
|
|
334387
|
+
detachMirrorSession(deviceId);
|
|
334388
|
+
}
|
|
334389
|
+
});
|
|
334390
|
+
let queuedRunId = null;
|
|
334391
|
+
let resultPosted = false;
|
|
334392
|
+
try {
|
|
334393
|
+
const res = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/locator-discovery-result`, {
|
|
334394
|
+
method: "POST",
|
|
334395
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
334396
|
+
body: JSON.stringify({
|
|
334397
|
+
bundles: harvest.bundles,
|
|
334398
|
+
divergentOrders: harvest.divergentOrders,
|
|
334399
|
+
reachedOrder: harvest.reachedOrder,
|
|
334400
|
+
logs: harvest.logs,
|
|
334401
|
+
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
334402
|
+
}),
|
|
334403
|
+
signal: AbortSignal.timeout(3e4)
|
|
334404
|
+
});
|
|
334405
|
+
if (res.ok) {
|
|
334406
|
+
resultPosted = true;
|
|
334407
|
+
const data = await res.json().catch(() => null);
|
|
334408
|
+
queuedRunId = data?.queuedRunId ?? null;
|
|
334409
|
+
} else {
|
|
334410
|
+
log.warn(`[locator-discovery] result POST returned ${res.status}`);
|
|
334411
|
+
}
|
|
334412
|
+
} catch (postErr) {
|
|
334413
|
+
log.warn(`[locator-discovery] result POST failed: ${postErr instanceof Error ? postErr.message : String(postErr)}`);
|
|
334414
|
+
}
|
|
334415
|
+
const passed = resultPosted && !harvest.environmentalError && harvest.reachedOrder != null;
|
|
334416
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334417
|
+
status: passed ? "PASSED" : "ERROR",
|
|
334418
|
+
error: !resultPosted ? "CONFIG_ISSUE: failed to persist harvested cross-platform locators (result POST did not succeed); retriable." : harvest.environmentalError ? `CONFIG_ISSUE: ${harvest.environmentalError}` : harvest.reachedOrder == null ? "CONFIG_ISSUE: locator discovery could not reach any step on the target platform." : void 0,
|
|
334419
|
+
logs: harvest.logs,
|
|
334420
|
+
duration: Date.now() - startTime
|
|
334421
|
+
});
|
|
334422
|
+
log[passed ? "ok" : "fail"](
|
|
334423
|
+
`Locator discovery ${passed ? "complete" : "ended"} \u2014 ${Object.keys(harvest.bundles).length} overlays, ${harvest.divergentOrders.length} divergent${queuedRunId ? ` (queued run ${queuedRunId})` : ""}`
|
|
334424
|
+
);
|
|
334425
|
+
} finally {
|
|
334426
|
+
setExecutorBusy(false);
|
|
334427
|
+
}
|
|
334428
|
+
return;
|
|
334429
|
+
}
|
|
333771
334430
|
log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
|
|
333772
334431
|
const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
|
|
333773
334432
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
@@ -334087,7 +334746,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334087
334746
|
testName: ctx.featureName ?? "Mobile discovery",
|
|
334088
334747
|
note: `${platform3} mobile discovery`,
|
|
334089
334748
|
surface: "mobile",
|
|
334090
|
-
mobilePlatform: platform3
|
|
334749
|
+
mobilePlatform: platform3,
|
|
334750
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {}
|
|
334091
334751
|
});
|
|
334092
334752
|
requestLiveBrowserHeartbeat(true);
|
|
334093
334753
|
mobileOnLog(`Mobile discovery (${ctx.mode}) \u2014 ${platform3} / ${mobileTarget.appId}`);
|
|
@@ -334105,6 +334765,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334105
334765
|
deeplink: mobileTarget.deeplink
|
|
334106
334766
|
}, {
|
|
334107
334767
|
onDeviceSelected: async (device) => {
|
|
334768
|
+
liveBrowserHandle.patch({ deviceId: device.deviceId, deviceName: device.name });
|
|
334108
334769
|
try {
|
|
334109
334770
|
capture = await startMobileNetworkCapture({
|
|
334110
334771
|
platform: platform3,
|
|
@@ -334123,7 +334784,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334123
334784
|
}
|
|
334124
334785
|
});
|
|
334125
334786
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
334126
|
-
attachMirrorSession(session.sessionId, platform3);
|
|
334787
|
+
attachMirrorSession(session.sessionId, platform3, session.deviceId);
|
|
334127
334788
|
const driver = new MobileBridgeDriver({
|
|
334128
334789
|
sessionId: session.sessionId,
|
|
334129
334790
|
platform: platform3,
|
|
@@ -334167,7 +334828,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334167
334828
|
}
|
|
334168
334829
|
}
|
|
334169
334830
|
if (session) {
|
|
334170
|
-
detachMirrorSession(session.
|
|
334831
|
+
detachMirrorSession(session.deviceId);
|
|
334171
334832
|
try {
|
|
334172
334833
|
await session.stop();
|
|
334173
334834
|
} catch {
|
|
@@ -334444,6 +335105,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334444
335105
|
return;
|
|
334445
335106
|
}
|
|
334446
335107
|
log.info(`\u{1F4F1} Mobile test - ${target.platform} / ${target.appId}`);
|
|
335108
|
+
const leasedDeviceName = assignedDeviceId ? getCachedDeviceSnapshot().find((d) => d.id === assignedDeviceId)?.name : void 0;
|
|
335109
|
+
liveBrowserHandle.patch({
|
|
335110
|
+
surface: "mobile",
|
|
335111
|
+
mobilePlatform: target.platform,
|
|
335112
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
|
|
335113
|
+
...leasedDeviceName ? { deviceName: leasedDeviceName } : {}
|
|
335114
|
+
});
|
|
335115
|
+
requestLiveBrowserHeartbeat(true);
|
|
334447
335116
|
const mobileSteps = run2.steps ?? [];
|
|
334448
335117
|
if (mobileSteps.length === 0) {
|
|
334449
335118
|
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
@@ -334471,7 +335140,20 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334471
335140
|
recordedDeviceId: target.recordedDeviceId,
|
|
334472
335141
|
assignedDeviceId,
|
|
334473
335142
|
launchMode: target.launchMode,
|
|
334474
|
-
deeplink: target.deeplink
|
|
335143
|
+
deeplink: target.deeplink,
|
|
335144
|
+
// Authoring platform (test's session medium). When it differs from
|
|
335145
|
+
// target.platform this is a cross-platform run: no blind coordinate
|
|
335146
|
+
// taps on foreign geometry, and the run must verify an outcome.
|
|
335147
|
+
authoringPlatform: mobilePlatformFromMedium(run2.authoringMedium) ?? void 0
|
|
335148
|
+
},
|
|
335149
|
+
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
335150
|
+
// leased device id) so N parallel Appium runs each show a live mirror.
|
|
335151
|
+
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
335152
|
+
attachMirrorSession(sessionId, sessionPlatform, deviceId);
|
|
335153
|
+
requestLiveBrowserHeartbeat(true);
|
|
335154
|
+
},
|
|
335155
|
+
onSessionEnding: (deviceId) => {
|
|
335156
|
+
detachMirrorSession(deviceId);
|
|
334475
335157
|
}
|
|
334476
335158
|
});
|
|
334477
335159
|
attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
|
|
@@ -334487,6 +335169,12 @@ ${result2.logs}`);
|
|
|
334487
335169
|
if (attemptLogs.length > 1) {
|
|
334488
335170
|
result2 = { ...result2, logs: attemptLogs.join("\n\n") };
|
|
334489
335171
|
}
|
|
335172
|
+
if (result2.deviceId || result2.deviceName) {
|
|
335173
|
+
liveBrowserHandle.patch({
|
|
335174
|
+
...result2.deviceId ? { deviceId: result2.deviceId } : {},
|
|
335175
|
+
...result2.deviceName ? { deviceName: result2.deviceName } : {}
|
|
335176
|
+
});
|
|
335177
|
+
}
|
|
334490
335178
|
const duration2 = Date.now() - startTime;
|
|
334491
335179
|
const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
334492
335180
|
const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
|