@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.js
CHANGED
|
@@ -1856,6 +1856,102 @@ var init_mobile_test_generation = __esm({
|
|
|
1856
1856
|
}
|
|
1857
1857
|
});
|
|
1858
1858
|
|
|
1859
|
+
// ../shared/dist/mobile-cross-platform.js
|
|
1860
|
+
function isMobilePlatform(value) {
|
|
1861
|
+
return value === "IOS" || value === "ANDROID";
|
|
1862
|
+
}
|
|
1863
|
+
function mediumForMobilePlatform(platform3) {
|
|
1864
|
+
return platform3 === "IOS" ? "IOS_NATIVE" : "ANDROID_NATIVE";
|
|
1865
|
+
}
|
|
1866
|
+
function mobilePlatformFromMedium(medium) {
|
|
1867
|
+
if (medium === "IOS_NATIVE")
|
|
1868
|
+
return "IOS";
|
|
1869
|
+
if (medium === "ANDROID_NATIVE")
|
|
1870
|
+
return "ANDROID";
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
function otherMobilePlatform(platform3) {
|
|
1874
|
+
return platform3 === "IOS" ? "ANDROID" : "IOS";
|
|
1875
|
+
}
|
|
1876
|
+
function platformTag(platform3) {
|
|
1877
|
+
return platform3 === "IOS" ? "@ios" : "@android";
|
|
1878
|
+
}
|
|
1879
|
+
function resolvePlatformLocatorBundle(target, platform3) {
|
|
1880
|
+
if (!target)
|
|
1881
|
+
return void 0;
|
|
1882
|
+
if (platform3) {
|
|
1883
|
+
const overlay = target.platformLocators?.[platform3];
|
|
1884
|
+
if (overlay)
|
|
1885
|
+
return overlay;
|
|
1886
|
+
}
|
|
1887
|
+
return target.locatorBundle;
|
|
1888
|
+
}
|
|
1889
|
+
function hasPlatformLocator(target, platform3) {
|
|
1890
|
+
return Boolean(target?.platformLocators?.[platform3]);
|
|
1891
|
+
}
|
|
1892
|
+
function hasCrossPlatformIdentity(bundle) {
|
|
1893
|
+
if (!bundle)
|
|
1894
|
+
return false;
|
|
1895
|
+
return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
1896
|
+
}
|
|
1897
|
+
function stepIsDivergentForPlatform(step, platform3) {
|
|
1898
|
+
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
1899
|
+
}
|
|
1900
|
+
function stepAppliesToPlatform(step, platform3) {
|
|
1901
|
+
const platforms = step.platforms;
|
|
1902
|
+
if (!platforms || platforms.length === 0)
|
|
1903
|
+
return true;
|
|
1904
|
+
if (!platform3)
|
|
1905
|
+
return true;
|
|
1906
|
+
return platforms.includes(platform3);
|
|
1907
|
+
}
|
|
1908
|
+
function stepsMissingPlatformLocators(steps, platform3) {
|
|
1909
|
+
return steps.filter((step) => {
|
|
1910
|
+
if (!LOCATOR_REQUIRING_ACTIONS.has(step.action))
|
|
1911
|
+
return false;
|
|
1912
|
+
if (!stepAppliesToPlatform(step, platform3))
|
|
1913
|
+
return false;
|
|
1914
|
+
if (hasPlatformLocator(step.target, platform3))
|
|
1915
|
+
return false;
|
|
1916
|
+
if (stepIsDivergentForPlatform(step, platform3))
|
|
1917
|
+
return false;
|
|
1918
|
+
return hasCrossPlatformIdentity(step.target?.locatorBundle);
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
function stepsNeedingReviewForPlatform(steps, platform3) {
|
|
1922
|
+
return steps.filter((step) => {
|
|
1923
|
+
if (!LOCATOR_REQUIRING_ACTIONS.has(step.action))
|
|
1924
|
+
return false;
|
|
1925
|
+
if (!stepAppliesToPlatform(step, platform3))
|
|
1926
|
+
return false;
|
|
1927
|
+
if (hasPlatformLocator(step.target, platform3))
|
|
1928
|
+
return false;
|
|
1929
|
+
if (stepIsDivergentForPlatform(step, platform3))
|
|
1930
|
+
return true;
|
|
1931
|
+
return !hasCrossPlatformIdentity(step.target?.locatorBundle);
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
function isTestReadyForPlatform(steps, authoringPlatform, runPlatform) {
|
|
1935
|
+
if (runPlatform === authoringPlatform)
|
|
1936
|
+
return true;
|
|
1937
|
+
return stepsMissingPlatformLocators(steps, runPlatform).length === 0 && stepsNeedingReviewForPlatform(steps, runPlatform).length === 0;
|
|
1938
|
+
}
|
|
1939
|
+
var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS;
|
|
1940
|
+
var init_mobile_cross_platform = __esm({
|
|
1941
|
+
"../shared/dist/mobile-cross-platform.js"() {
|
|
1942
|
+
"use strict";
|
|
1943
|
+
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
1944
|
+
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
1945
|
+
"tap",
|
|
1946
|
+
"type",
|
|
1947
|
+
"clear",
|
|
1948
|
+
"waitForElement",
|
|
1949
|
+
"assertVisible",
|
|
1950
|
+
"assertText"
|
|
1951
|
+
]);
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1859
1955
|
// ../shared/dist/mobile-deeplink.js
|
|
1860
1956
|
function isSafeDeepLink(value) {
|
|
1861
1957
|
if (typeof value !== "string")
|
|
@@ -3534,6 +3630,7 @@ __export(dist_exports, {
|
|
|
3534
3630
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
3535
3631
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
3536
3632
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
3633
|
+
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
3537
3634
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
3538
3635
|
MOBILE_SNAPSHOT_MAX_SIZE_BYTES: () => MOBILE_SNAPSHOT_MAX_SIZE_BYTES,
|
|
3539
3636
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
@@ -3587,6 +3684,8 @@ __export(dist_exports, {
|
|
|
3587
3684
|
getPlanDefinition: () => getPlanDefinition,
|
|
3588
3685
|
getPlanLabel: () => getPlanLabel,
|
|
3589
3686
|
getTrialDaysRemaining: () => getTrialDaysRemaining,
|
|
3687
|
+
hasCrossPlatformIdentity: () => hasCrossPlatformIdentity,
|
|
3688
|
+
hasPlatformLocator: () => hasPlatformLocator,
|
|
3590
3689
|
hostOfOrigin: () => hostOfOrigin,
|
|
3591
3690
|
isAddonType: () => isAddonType,
|
|
3592
3691
|
isApiTest: () => isApiTest,
|
|
@@ -3596,23 +3695,34 @@ __export(dist_exports, {
|
|
|
3596
3695
|
isCreditPackAmountCents: () => isCreditPackAmountCents,
|
|
3597
3696
|
isDbPricingPlan: () => isDbPricingPlan,
|
|
3598
3697
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
3698
|
+
isMobilePlatform: () => isMobilePlatform,
|
|
3599
3699
|
isPricingPlan: () => isPricingPlan,
|
|
3600
3700
|
isRunnableStep: () => isRunnableStep,
|
|
3601
3701
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
3602
3702
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
3703
|
+
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
3603
3704
|
isTrialExpired: () => isTrialExpired,
|
|
3604
3705
|
isUIVariant: () => isUIVariant,
|
|
3605
3706
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
3707
|
+
mediumForMobilePlatform: () => mediumForMobilePlatform,
|
|
3708
|
+
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
3606
3709
|
normalizeControlName: () => normalizeControlName,
|
|
3710
|
+
otherMobilePlatform: () => otherMobilePlatform,
|
|
3607
3711
|
parenDepthDelta: () => parenDepthDelta,
|
|
3608
3712
|
planHasBillingPortal: () => planHasBillingPortal,
|
|
3609
3713
|
planRequiresCheckout: () => planRequiresCheckout,
|
|
3714
|
+
platformTag: () => platformTag,
|
|
3610
3715
|
registrableDomain: () => registrableDomain,
|
|
3611
3716
|
resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
|
|
3717
|
+
resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
|
|
3612
3718
|
safeOrigin: () => safeOrigin,
|
|
3613
3719
|
sanitizeTestVariables: () => sanitizeTestVariables,
|
|
3614
3720
|
selectorCandidatesForStep: () => selectorCandidatesForStep,
|
|
3615
3721
|
selectorForValue: () => selectorForValue,
|
|
3722
|
+
stepAppliesToPlatform: () => stepAppliesToPlatform,
|
|
3723
|
+
stepIsDivergentForPlatform: () => stepIsDivergentForPlatform,
|
|
3724
|
+
stepsMissingPlatformLocators: () => stepsMissingPlatformLocators,
|
|
3725
|
+
stepsNeedingReviewForPlatform: () => stepsNeedingReviewForPlatform,
|
|
3616
3726
|
stripEnvironmentalPrefix: () => stripEnvironmentalPrefix,
|
|
3617
3727
|
stripLoginScaffolding: () => stripLoginScaffolding,
|
|
3618
3728
|
summarizeHealth: () => summarizeHealth,
|
|
@@ -3626,6 +3736,7 @@ var init_dist = __esm({
|
|
|
3626
3736
|
init_types();
|
|
3627
3737
|
init_constants();
|
|
3628
3738
|
init_mobile_test_generation();
|
|
3739
|
+
init_mobile_cross_platform();
|
|
3629
3740
|
init_mobile_deeplink();
|
|
3630
3741
|
init_plans();
|
|
3631
3742
|
init_audit_pricing();
|
|
@@ -66792,16 +66903,16 @@ ${statePacket}` },
|
|
|
66792
66903
|
}
|
|
66793
66904
|
const screen = screenName();
|
|
66794
66905
|
if (accessibilityId) {
|
|
66795
|
-
const bundle2 = { xpath: `//*[@content-desc=${
|
|
66906
|
+
const bundle2 = { xpath: `//*[@content-desc=${xpathLiteral2(accessibilityId)} or @name=${xpathLiteral2(accessibilityId)}]`, className: "", accessibilityId };
|
|
66796
66907
|
recordAction({ type: "ASSERT_VISIBLE", selector: accessibilityId, metadata: { screenName: screen, targetLabel: accessibilityId, locatorBundle: bundle2 } });
|
|
66797
66908
|
return { ok: true, label: accessibilityId };
|
|
66798
66909
|
}
|
|
66799
66910
|
const safeText = text;
|
|
66800
|
-
const bundle = { xpath: `//*[@text=${
|
|
66911
|
+
const bundle = { xpath: `//*[@text=${xpathLiteral2(safeText)} or @label=${xpathLiteral2(safeText)}]`, className: "", text: safeText };
|
|
66801
66912
|
recordAction({ type: "ASSERT_VISIBLE", selector: bundle.xpath, metadata: { screenName: screen, targetLabel: safeText, locatorBundle: bundle } });
|
|
66802
66913
|
return { ok: true, label: safeText };
|
|
66803
66914
|
}
|
|
66804
|
-
function
|
|
66915
|
+
function xpathLiteral2(value) {
|
|
66805
66916
|
if (!value.includes("'"))
|
|
66806
66917
|
return `'${value}'`;
|
|
66807
66918
|
if (!value.includes('"'))
|
|
@@ -66880,9 +66991,9 @@ ${statePacket}` },
|
|
|
66880
66991
|
if (claimedVerified && !verified) {
|
|
66881
66992
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
66882
66993
|
}
|
|
66883
|
-
const
|
|
66994
|
+
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
66884
66995
|
const reviewTag = verified ? "@verified" : "@needs-review";
|
|
66885
|
-
const tags = ["@discovered", "@mobile",
|
|
66996
|
+
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
66886
66997
|
const test = {
|
|
66887
66998
|
name: compiled.name,
|
|
66888
66999
|
description: recording.finish?.description ?? compiled.description,
|
|
@@ -328118,7 +328229,7 @@ var require_package4 = __commonJS({
|
|
|
328118
328229
|
"package.json"(exports2, module2) {
|
|
328119
328230
|
module2.exports = {
|
|
328120
328231
|
name: "@validate.qa/runner",
|
|
328121
|
-
version: "1.0.
|
|
328232
|
+
version: "1.0.17",
|
|
328122
328233
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328123
328234
|
bin: {
|
|
328124
328235
|
"validate-runner": "dist/cli.js"
|
|
@@ -329659,6 +329770,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
329659
329770
|
|
|
329660
329771
|
// src/services/appium-executor.ts
|
|
329661
329772
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
329773
|
+
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
329662
329774
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
329663
329775
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
329664
329776
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
@@ -329723,7 +329835,7 @@ async function getWindowSize(sessionId, retries = 3) {
|
|
|
329723
329835
|
}
|
|
329724
329836
|
throw new Error("Failed to get window size: invalid dimensions returned");
|
|
329725
329837
|
}
|
|
329726
|
-
function realDeviceCapabilities(platform3) {
|
|
329838
|
+
function realDeviceCapabilities(platform3, portOffset = 0) {
|
|
329727
329839
|
const env = process.env;
|
|
329728
329840
|
const caps = {};
|
|
329729
329841
|
const num = (v) => {
|
|
@@ -329731,6 +329843,7 @@ function realDeviceCapabilities(platform3) {
|
|
|
329731
329843
|
const n = Number.parseInt(v, 10);
|
|
329732
329844
|
return Number.isFinite(n) ? n : void 0;
|
|
329733
329845
|
};
|
|
329846
|
+
const offset = Number.isFinite(portOffset) && portOffset > 0 ? Math.floor(portOffset) : 0;
|
|
329734
329847
|
if (platform3 === "IOS") {
|
|
329735
329848
|
if (env.APPIUM_XCODE_ORG_ID) {
|
|
329736
329849
|
caps["appium:xcodeOrgId"] = env.APPIUM_XCODE_ORG_ID;
|
|
@@ -329740,15 +329853,15 @@ function realDeviceCapabilities(platform3) {
|
|
|
329740
329853
|
}
|
|
329741
329854
|
if (env.APPIUM_UPDATED_WDA_BUNDLE_ID) caps["appium:updatedWDABundleId"] = env.APPIUM_UPDATED_WDA_BUNDLE_ID;
|
|
329742
329855
|
const wdaPort = num(env.APPIUM_WDA_LOCAL_PORT);
|
|
329743
|
-
if (wdaPort !== void 0) caps["appium:wdaLocalPort"] = wdaPort;
|
|
329856
|
+
if (wdaPort !== void 0) caps["appium:wdaLocalPort"] = wdaPort + offset;
|
|
329744
329857
|
} else {
|
|
329745
329858
|
const systemPort = num(env.APPIUM_SYSTEM_PORT);
|
|
329746
|
-
if (systemPort !== void 0) caps["appium:systemPort"] = systemPort;
|
|
329859
|
+
if (systemPort !== void 0) caps["appium:systemPort"] = systemPort + offset;
|
|
329747
329860
|
}
|
|
329748
329861
|
return caps;
|
|
329749
329862
|
}
|
|
329750
329863
|
async function createSession(target) {
|
|
329751
|
-
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform) : {};
|
|
329864
|
+
const realCaps = target.isPhysical ? realDeviceCapabilities(target.platform, target.portOffset) : {};
|
|
329752
329865
|
const capabilities = target.platform === "IOS" ? {
|
|
329753
329866
|
platformName: "iOS",
|
|
329754
329867
|
"appium:automationName": "XCUITest",
|
|
@@ -329986,6 +330099,11 @@ function boundsFromStep(step) {
|
|
|
329986
330099
|
}
|
|
329987
330100
|
return null;
|
|
329988
330101
|
}
|
|
330102
|
+
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
330103
|
+
if (isCrossPlatform) return null;
|
|
330104
|
+
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
330105
|
+
return boundsFromStep(step);
|
|
330106
|
+
}
|
|
329989
330107
|
function locatorStrategyForValue(value) {
|
|
329990
330108
|
const trimmed = value?.trim();
|
|
329991
330109
|
if (!trimmed) {
|
|
@@ -330005,24 +330123,68 @@ function locatorStrategyForValue(value) {
|
|
|
330005
330123
|
{ using: "id", value: trimmed }
|
|
330006
330124
|
];
|
|
330007
330125
|
}
|
|
330008
|
-
function buildLocatorStrategies(target) {
|
|
330126
|
+
function buildLocatorStrategies(target, platform3) {
|
|
330009
330127
|
if (!target) return [];
|
|
330010
330128
|
const strategies = [];
|
|
330011
|
-
const bundle = target
|
|
330129
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330012
330130
|
if (bundle) {
|
|
330013
330131
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330014
330132
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
330015
330133
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330016
330134
|
if (bundle.className) strategies.push({ using: "class name", value: bundle.className });
|
|
330017
330135
|
}
|
|
330018
|
-
|
|
330019
|
-
|
|
330020
|
-
|
|
330136
|
+
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
330137
|
+
strategies.push(...locatorStrategyForValue(target.primary));
|
|
330138
|
+
for (const fallback of target.fallbacks ?? []) {
|
|
330139
|
+
strategies.push(...locatorStrategyForValue(fallback));
|
|
330140
|
+
}
|
|
330021
330141
|
}
|
|
330022
330142
|
return strategies.filter(
|
|
330023
330143
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
330024
330144
|
);
|
|
330025
330145
|
}
|
|
330146
|
+
function dedupeStrategies(strategies) {
|
|
330147
|
+
return strategies.filter(
|
|
330148
|
+
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
330149
|
+
);
|
|
330150
|
+
}
|
|
330151
|
+
function xpathLiteral(value) {
|
|
330152
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
330153
|
+
if (!value.includes('"')) return `"${value}"`;
|
|
330154
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
330155
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
330156
|
+
}
|
|
330157
|
+
function identityTextXpath(value) {
|
|
330158
|
+
const lit = xpathLiteral(value);
|
|
330159
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
330160
|
+
}
|
|
330161
|
+
function isIdentityStrategy(strategy) {
|
|
330162
|
+
if (strategy.using === "accessibility id" || strategy.using === "id") return true;
|
|
330163
|
+
if (strategy.using === "xpath") return strategy.value.includes("@");
|
|
330164
|
+
return false;
|
|
330165
|
+
}
|
|
330166
|
+
function buildIdentityLocatorStrategies(target, platform3) {
|
|
330167
|
+
if (!target) return [];
|
|
330168
|
+
const strategies = [];
|
|
330169
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330170
|
+
if (bundle) {
|
|
330171
|
+
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330172
|
+
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
330173
|
+
for (const value of [bundle.text, bundle.contentDesc]) {
|
|
330174
|
+
const trimmed = value?.trim();
|
|
330175
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath(trimmed) });
|
|
330176
|
+
}
|
|
330177
|
+
if (bundle.xpath && bundle.xpath.includes("@")) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330178
|
+
}
|
|
330179
|
+
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
330180
|
+
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
330181
|
+
for (const strategy of locatorStrategyForValue(value)) {
|
|
330182
|
+
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
330183
|
+
}
|
|
330184
|
+
}
|
|
330185
|
+
}
|
|
330186
|
+
return dedupeStrategies(strategies);
|
|
330187
|
+
}
|
|
330026
330188
|
function readStepTimeout(step) {
|
|
330027
330189
|
const value = step.metadata?.timeout;
|
|
330028
330190
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -330030,10 +330192,12 @@ function readStepTimeout(step) {
|
|
|
330030
330192
|
}
|
|
330031
330193
|
return void 0;
|
|
330032
330194
|
}
|
|
330033
|
-
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS) {
|
|
330034
|
-
const strategies = buildLocatorStrategies(target);
|
|
330195
|
+
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
330196
|
+
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
330035
330197
|
if (strategies.length === 0) {
|
|
330036
|
-
throw new Error(
|
|
330198
|
+
throw new Error(
|
|
330199
|
+
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"
|
|
330200
|
+
);
|
|
330037
330201
|
}
|
|
330038
330202
|
const startTime = Date.now();
|
|
330039
330203
|
let lastError = "";
|
|
@@ -330131,18 +330295,18 @@ function shouldAbortAfterStepFailure(action) {
|
|
|
330131
330295
|
return false;
|
|
330132
330296
|
}
|
|
330133
330297
|
}
|
|
330134
|
-
async function executeTap(sessionId, step) {
|
|
330135
|
-
const bounds =
|
|
330298
|
+
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330299
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330136
330300
|
if (step.target) {
|
|
330137
330301
|
try {
|
|
330138
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330302
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330139
330303
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
330140
330304
|
method: "POST",
|
|
330141
330305
|
body: "{}"
|
|
330142
330306
|
});
|
|
330143
330307
|
return;
|
|
330144
330308
|
} catch (error2) {
|
|
330145
|
-
if (!bounds) throw error2;
|
|
330309
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330146
330310
|
}
|
|
330147
330311
|
}
|
|
330148
330312
|
if (!bounds) {
|
|
@@ -330153,7 +330317,7 @@ async function executeTap(sessionId, step) {
|
|
|
330153
330317
|
async function executeType(sessionId, step, context) {
|
|
330154
330318
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
330155
330319
|
const value = String(step.value);
|
|
330156
|
-
const bounds =
|
|
330320
|
+
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
330157
330321
|
let elementId = null;
|
|
330158
330322
|
let lastInputError = null;
|
|
330159
330323
|
let tappedBounds = false;
|
|
@@ -330170,12 +330334,12 @@ async function executeType(sessionId, step, context) {
|
|
|
330170
330334
|
}
|
|
330171
330335
|
if (step.target) {
|
|
330172
330336
|
try {
|
|
330173
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330337
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
330174
330338
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330175
330339
|
if (!lastInputError) return;
|
|
330176
330340
|
elementId = null;
|
|
330177
330341
|
} catch (error2) {
|
|
330178
|
-
if (!bounds) throw error2;
|
|
330342
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
330179
330343
|
lastInputError = error2;
|
|
330180
330344
|
}
|
|
330181
330345
|
}
|
|
@@ -330223,14 +330387,14 @@ async function executeType(sessionId, step, context) {
|
|
|
330223
330387
|
}
|
|
330224
330388
|
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
330225
330389
|
}
|
|
330226
|
-
async function executeClear(sessionId, step) {
|
|
330227
|
-
const bounds =
|
|
330390
|
+
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330391
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330228
330392
|
let elementId = null;
|
|
330229
330393
|
if (step.target) {
|
|
330230
330394
|
try {
|
|
330231
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330395
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330232
330396
|
} catch (error2) {
|
|
330233
|
-
if (!bounds) throw error2;
|
|
330397
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330234
330398
|
}
|
|
330235
330399
|
}
|
|
330236
330400
|
if (!elementId && bounds) {
|
|
@@ -330299,20 +330463,20 @@ async function executeHome(sessionId, platform3) {
|
|
|
330299
330463
|
body: JSON.stringify({ keycode: 3 })
|
|
330300
330464
|
});
|
|
330301
330465
|
}
|
|
330302
|
-
async function executeWaitForElement(sessionId, step) {
|
|
330466
|
+
async function executeWaitForElement(sessionId, step, platform3) {
|
|
330303
330467
|
const timeout = readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS;
|
|
330304
|
-
await findElement(sessionId, step.target, timeout);
|
|
330468
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
330305
330469
|
}
|
|
330306
|
-
async function executeAssertVisible(sessionId, step) {
|
|
330307
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330470
|
+
async function executeAssertVisible(sessionId, step, platform3) {
|
|
330471
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330308
330472
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
330309
330473
|
if (!result?.value) {
|
|
330310
330474
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
330311
330475
|
}
|
|
330312
330476
|
}
|
|
330313
|
-
async function executeAssertText(sessionId, step) {
|
|
330477
|
+
async function executeAssertText(sessionId, step, platform3) {
|
|
330314
330478
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
330315
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330479
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330316
330480
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
330317
330481
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
330318
330482
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -330347,6 +330511,7 @@ async function executeMobileTest(options) {
|
|
|
330347
330511
|
const screenshots = [];
|
|
330348
330512
|
let apiCalls = [];
|
|
330349
330513
|
let sessionId = null;
|
|
330514
|
+
let selectedDevice;
|
|
330350
330515
|
let capture = null;
|
|
330351
330516
|
let captureStopped = false;
|
|
330352
330517
|
let recordingStarted = false;
|
|
@@ -330394,7 +330559,6 @@ async function executeMobileTest(options) {
|
|
|
330394
330559
|
`No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
|
|
330395
330560
|
);
|
|
330396
330561
|
}
|
|
330397
|
-
let selectedDevice;
|
|
330398
330562
|
if (options.target.assignedDeviceId) {
|
|
330399
330563
|
selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
|
|
330400
330564
|
if (!selectedDevice) {
|
|
@@ -330435,13 +330599,15 @@ async function executeMobileTest(options) {
|
|
|
330435
330599
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
330436
330600
|
}
|
|
330437
330601
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
330602
|
+
const leasedPortOffset = Math.max(0, matchingDevices.findIndex((d) => d.id === selectedDevice?.id));
|
|
330438
330603
|
try {
|
|
330439
330604
|
sessionId = await createSessionWithRetry({
|
|
330440
330605
|
appId: options.target.appId,
|
|
330441
330606
|
platform: options.target.platform,
|
|
330442
330607
|
deviceId: selectedDevice.id,
|
|
330443
330608
|
platformVersion: options.target.platformVersion ?? selectedDevice.platformVersion,
|
|
330444
|
-
isPhysical: selectedDevice.isPhysical
|
|
330609
|
+
isPhysical: selectedDevice.isPhysical,
|
|
330610
|
+
portOffset: leasedPortOffset
|
|
330445
330611
|
}, (msg) => log2(msg));
|
|
330446
330612
|
} catch (err) {
|
|
330447
330613
|
const base2 = err instanceof Error ? err.message : String(err);
|
|
@@ -330457,6 +330623,12 @@ async function executeMobileTest(options) {
|
|
|
330457
330623
|
throw err;
|
|
330458
330624
|
}
|
|
330459
330625
|
log2(`Session created: ${sessionId}`);
|
|
330626
|
+
if (selectedDevice.id) {
|
|
330627
|
+
try {
|
|
330628
|
+
options.onSessionReady?.(sessionId, selectedDevice.id, options.target.platform);
|
|
330629
|
+
} catch {
|
|
330630
|
+
}
|
|
330631
|
+
}
|
|
330460
330632
|
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
330461
330633
|
log2("Opening a fresh app instance for the test...");
|
|
330462
330634
|
await freshLaunchTargetApp(sessionId, options.target.platform, options.target.appId, log2);
|
|
@@ -330469,8 +330641,34 @@ async function executeMobileTest(options) {
|
|
|
330469
330641
|
}
|
|
330470
330642
|
}
|
|
330471
330643
|
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
330644
|
+
let entryDrift = false;
|
|
330645
|
+
const runPlatform = options.target.platform;
|
|
330646
|
+
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
330647
|
+
const entryStep = options.steps.find(
|
|
330648
|
+
(s) => s.action !== "launchApp" && stepAppliesToPlatform(s, runPlatform) && buildIdentityLocatorStrategies(s.target, runPlatform).length > 0
|
|
330649
|
+
);
|
|
330650
|
+
if (entryStep) {
|
|
330651
|
+
try {
|
|
330652
|
+
await findElement(sessionId, entryStep.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true, platform: runPlatform });
|
|
330653
|
+
} catch (err) {
|
|
330654
|
+
if (isTransportError(err)) throw err;
|
|
330655
|
+
entryDrift = true;
|
|
330656
|
+
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).`;
|
|
330657
|
+
log2(` \u2717 Entry-screen drift: ${driftMsg}`);
|
|
330658
|
+
stepResults.push({ stepOrder: entryStep.order, status: "failed", duration: 0, error: driftMsg });
|
|
330659
|
+
const driftShot = await takeScreenshot(sessionId).catch(() => null);
|
|
330660
|
+
screenshots.push(driftShot ?? "");
|
|
330661
|
+
}
|
|
330662
|
+
}
|
|
330472
330663
|
let environmental = null;
|
|
330473
330664
|
for (const step of options.steps) {
|
|
330665
|
+
if (entryDrift) break;
|
|
330666
|
+
if (!stepAppliesToPlatform(step, runPlatform)) {
|
|
330667
|
+
log2(`Step ${step.order}: ${step.action} \u2014 skipped (not applicable to ${runPlatform})`);
|
|
330668
|
+
stepResults.push({ stepOrder: step.order, status: "skipped", duration: 0 });
|
|
330669
|
+
screenshots.push("");
|
|
330670
|
+
continue;
|
|
330671
|
+
}
|
|
330474
330672
|
const stepStart = Date.now();
|
|
330475
330673
|
log2(`Step ${step.order}: ${step.action} - ${step.description}`);
|
|
330476
330674
|
let status = "passed";
|
|
@@ -330482,17 +330680,18 @@ async function executeMobileTest(options) {
|
|
|
330482
330680
|
log2(" App launched (handled by session creation)");
|
|
330483
330681
|
break;
|
|
330484
330682
|
case "tap":
|
|
330485
|
-
await executeTap(sessionId, step);
|
|
330683
|
+
await executeTap(sessionId, step, runPlatform, isCrossPlatform);
|
|
330486
330684
|
break;
|
|
330487
330685
|
case "type":
|
|
330488
330686
|
await executeType(sessionId, step, {
|
|
330489
|
-
platform:
|
|
330687
|
+
platform: runPlatform,
|
|
330490
330688
|
deviceId: selectedDevice.id,
|
|
330491
|
-
log: log2
|
|
330689
|
+
log: log2,
|
|
330690
|
+
isCrossPlatform
|
|
330492
330691
|
});
|
|
330493
330692
|
break;
|
|
330494
330693
|
case "clear":
|
|
330495
|
-
await executeClear(sessionId, step);
|
|
330694
|
+
await executeClear(sessionId, step, runPlatform, isCrossPlatform);
|
|
330496
330695
|
break;
|
|
330497
330696
|
case "swipe":
|
|
330498
330697
|
case "scroll":
|
|
@@ -330502,16 +330701,16 @@ async function executeMobileTest(options) {
|
|
|
330502
330701
|
await executeBack(sessionId);
|
|
330503
330702
|
break;
|
|
330504
330703
|
case "home":
|
|
330505
|
-
await executeHome(sessionId,
|
|
330704
|
+
await executeHome(sessionId, runPlatform);
|
|
330506
330705
|
break;
|
|
330507
330706
|
case "waitForElement":
|
|
330508
|
-
await executeWaitForElement(sessionId, step);
|
|
330707
|
+
await executeWaitForElement(sessionId, step, runPlatform);
|
|
330509
330708
|
break;
|
|
330510
330709
|
case "assertVisible":
|
|
330511
|
-
await executeAssertVisible(sessionId, step);
|
|
330710
|
+
await executeAssertVisible(sessionId, step, runPlatform);
|
|
330512
330711
|
break;
|
|
330513
330712
|
case "assertText":
|
|
330514
|
-
await executeAssertText(sessionId, step);
|
|
330713
|
+
await executeAssertText(sessionId, step, runPlatform);
|
|
330515
330714
|
break;
|
|
330516
330715
|
default: {
|
|
330517
330716
|
const _exhaustive = step.action;
|
|
@@ -330548,8 +330747,14 @@ async function executeMobileTest(options) {
|
|
|
330548
330747
|
break;
|
|
330549
330748
|
}
|
|
330550
330749
|
}
|
|
330551
|
-
const
|
|
330552
|
-
const
|
|
330750
|
+
const executedResults = stepResults.filter((r) => r.status !== "skipped");
|
|
330751
|
+
const verifyOrders = new Set(
|
|
330752
|
+
options.steps.filter((s) => VERIFICATION_ACTIONS.has(s.action)).map((s) => s.order)
|
|
330753
|
+
);
|
|
330754
|
+
const anyVerificationPassed = stepResults.some((r) => r.status === "passed" && verifyOrders.has(r.stepOrder));
|
|
330755
|
+
const crossPlatformUnverified = isCrossPlatform && verifyOrders.size > 0 && !anyVerificationPassed;
|
|
330756
|
+
const allPassed = !environmental && executedResults.length > 0 && !crossPlatformUnverified && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
330757
|
+
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);
|
|
330553
330758
|
await stopCapture();
|
|
330554
330759
|
await stopRecording();
|
|
330555
330760
|
return {
|
|
@@ -330561,6 +330766,8 @@ async function executeMobileTest(options) {
|
|
|
330561
330766
|
logs: logLines.join("\n"),
|
|
330562
330767
|
error: firstError,
|
|
330563
330768
|
duration: Date.now() - startTime,
|
|
330769
|
+
deviceId: selectedDevice?.id,
|
|
330770
|
+
deviceName: selectedDevice?.name,
|
|
330564
330771
|
environmental: environmental ?? void 0
|
|
330565
330772
|
};
|
|
330566
330773
|
} catch (err) {
|
|
@@ -330577,6 +330784,8 @@ async function executeMobileTest(options) {
|
|
|
330577
330784
|
logs: logLines.join("\n"),
|
|
330578
330785
|
error: errorMsg,
|
|
330579
330786
|
duration: Date.now() - startTime,
|
|
330787
|
+
deviceId: selectedDevice?.id,
|
|
330788
|
+
deviceName: selectedDevice?.name,
|
|
330580
330789
|
// A throw before/around the step loop is a setup/environment problem
|
|
330581
330790
|
// (no device booted, app not installed, session could not be created,
|
|
330582
330791
|
// Appium unreachable) — never a genuine test assertion failure.
|
|
@@ -330586,6 +330795,12 @@ async function executeMobileTest(options) {
|
|
|
330586
330795
|
await stopCapture();
|
|
330587
330796
|
await stopRecording();
|
|
330588
330797
|
if (sessionId) {
|
|
330798
|
+
if (selectedDevice?.id) {
|
|
330799
|
+
try {
|
|
330800
|
+
options.onSessionEnding?.(selectedDevice.id);
|
|
330801
|
+
} catch {
|
|
330802
|
+
}
|
|
330803
|
+
}
|
|
330589
330804
|
try {
|
|
330590
330805
|
log2("Closing the app on the device...");
|
|
330591
330806
|
await terminateTargetApp(sessionId, options.target.platform, options.target.appId);
|
|
@@ -330644,7 +330859,9 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330644
330859
|
platform: target.platform,
|
|
330645
330860
|
deviceId: selectedDevice.id,
|
|
330646
330861
|
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
330647
|
-
isPhysical: selectedDevice.isPhysical
|
|
330862
|
+
isPhysical: selectedDevice.isPhysical,
|
|
330863
|
+
// Distinct control port per concurrent physical device (no-op for emulators/sims).
|
|
330864
|
+
portOffset: Math.max(0, matchingDevices.findIndex((d) => d.id === selectedDevice?.id))
|
|
330648
330865
|
});
|
|
330649
330866
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
330650
330867
|
try {
|
|
@@ -330656,6 +330873,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330656
330873
|
return {
|
|
330657
330874
|
sessionId,
|
|
330658
330875
|
deviceId: selectedDevice.id,
|
|
330876
|
+
deviceName: selectedDevice.name,
|
|
330659
330877
|
isPhysical: selectedDevice.isPhysical === true,
|
|
330660
330878
|
platform: target.platform,
|
|
330661
330879
|
appId: target.appId,
|
|
@@ -330670,6 +330888,184 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330670
330888
|
}
|
|
330671
330889
|
};
|
|
330672
330890
|
}
|
|
330891
|
+
async function readElementAttribute(sessionId, elementId, name) {
|
|
330892
|
+
try {
|
|
330893
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(name)}`);
|
|
330894
|
+
const value = result?.value;
|
|
330895
|
+
if (typeof value !== "string") return void 0;
|
|
330896
|
+
const trimmed = value.trim();
|
|
330897
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
330898
|
+
} catch (err) {
|
|
330899
|
+
if (isTransportError(err)) throw err;
|
|
330900
|
+
return void 0;
|
|
330901
|
+
}
|
|
330902
|
+
}
|
|
330903
|
+
function harvestFallbackXpath(platform3, attrs) {
|
|
330904
|
+
if (platform3 === "IOS") {
|
|
330905
|
+
if (attrs.name) return `//*[@name=${xpathLiteral(attrs.name)}]`;
|
|
330906
|
+
const text = attrs.label ?? attrs.value;
|
|
330907
|
+
if (text) return identityTextXpath(text);
|
|
330908
|
+
return "//*";
|
|
330909
|
+
}
|
|
330910
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral(attrs.resourceId)}]`;
|
|
330911
|
+
if (attrs.text) return identityTextXpath(attrs.text);
|
|
330912
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral(attrs.contentDesc)}]`;
|
|
330913
|
+
return "//*";
|
|
330914
|
+
}
|
|
330915
|
+
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
330916
|
+
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
330917
|
+
if (platform3 === "IOS") {
|
|
330918
|
+
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
330919
|
+
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
330920
|
+
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
330921
|
+
const text2 = label ?? value;
|
|
330922
|
+
if (!name && !text2) return null;
|
|
330923
|
+
const bundle2 = {
|
|
330924
|
+
xpath: harvestFallbackXpath("IOS", { name, label, value }),
|
|
330925
|
+
className
|
|
330926
|
+
};
|
|
330927
|
+
if (name) bundle2.accessibilityId = name;
|
|
330928
|
+
if (text2) bundle2.text = text2;
|
|
330929
|
+
return bundle2;
|
|
330930
|
+
}
|
|
330931
|
+
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
330932
|
+
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
330933
|
+
const text = await readElementAttribute(sessionId, elementId, "text");
|
|
330934
|
+
if (!resourceId && !contentDesc && !text) return null;
|
|
330935
|
+
const bundle = {
|
|
330936
|
+
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
|
330937
|
+
className
|
|
330938
|
+
};
|
|
330939
|
+
if (contentDesc) bundle.accessibilityId = contentDesc;
|
|
330940
|
+
if (resourceId) bundle.resourceId = resourceId;
|
|
330941
|
+
if (text) bundle.text = text;
|
|
330942
|
+
if (contentDesc) bundle.contentDesc = contentDesc;
|
|
330943
|
+
return bundle;
|
|
330944
|
+
}
|
|
330945
|
+
function isStateAdvancingAction(action) {
|
|
330946
|
+
return action === "tap" || action === "type" || action === "clear" || action === "swipe" || action === "scroll" || action === "back" || action === "home";
|
|
330947
|
+
}
|
|
330948
|
+
function stepNeedsLocator(action) {
|
|
330949
|
+
return action === "tap" || action === "type" || action === "clear" || action === "waitForElement" || action === "assertVisible" || action === "assertText";
|
|
330950
|
+
}
|
|
330951
|
+
async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
330952
|
+
switch (step.action) {
|
|
330953
|
+
case "tap":
|
|
330954
|
+
if (!elementId) return false;
|
|
330955
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
330956
|
+
return true;
|
|
330957
|
+
case "type": {
|
|
330958
|
+
if (step.value === void 0 || step.value === null || step.value === "") return true;
|
|
330959
|
+
if (!elementId) return false;
|
|
330960
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
330961
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330962
|
+
const err = await trySendElementValue(sessionId, elementId, String(step.value));
|
|
330963
|
+
return !err;
|
|
330964
|
+
}
|
|
330965
|
+
case "clear":
|
|
330966
|
+
if (!elementId) return false;
|
|
330967
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
330968
|
+
return true;
|
|
330969
|
+
case "swipe":
|
|
330970
|
+
case "scroll":
|
|
330971
|
+
await executeSwipe(sessionId, step);
|
|
330972
|
+
return true;
|
|
330973
|
+
case "back":
|
|
330974
|
+
await executeBack(sessionId);
|
|
330975
|
+
return true;
|
|
330976
|
+
case "home":
|
|
330977
|
+
await executeHome(sessionId, platform3);
|
|
330978
|
+
return true;
|
|
330979
|
+
case "launchApp":
|
|
330980
|
+
case "waitForElement":
|
|
330981
|
+
case "assertVisible":
|
|
330982
|
+
case "assertText":
|
|
330983
|
+
return true;
|
|
330984
|
+
}
|
|
330985
|
+
}
|
|
330986
|
+
async function harvestPlatformLocators(options) {
|
|
330987
|
+
const platform3 = options.target.platform;
|
|
330988
|
+
const logLines = [];
|
|
330989
|
+
const log2 = (msg) => {
|
|
330990
|
+
logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
330991
|
+
};
|
|
330992
|
+
const bundles = {};
|
|
330993
|
+
const divergentOrders = [];
|
|
330994
|
+
let reachedOrder = null;
|
|
330995
|
+
let environmentalError;
|
|
330996
|
+
log2(`Cross-platform locator discovery \u2014 resolving ${platform3} locators for ${options.steps.length} steps`);
|
|
330997
|
+
const session = await createMobileDiscoverySession({
|
|
330998
|
+
appId: options.target.appId,
|
|
330999
|
+
platform: platform3,
|
|
331000
|
+
platformVersion: options.target.platformVersion,
|
|
331001
|
+
recordedDeviceId: options.target.recordedDeviceId,
|
|
331002
|
+
assignedDeviceId: options.target.assignedDeviceId,
|
|
331003
|
+
launchMode: "LAUNCH_APP"
|
|
331004
|
+
});
|
|
331005
|
+
options.onSessionReady?.(session.sessionId, session.deviceId, platform3);
|
|
331006
|
+
try {
|
|
331007
|
+
await freshLaunchTargetApp(session.sessionId, platform3, options.target.appId, log2);
|
|
331008
|
+
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
331009
|
+
try {
|
|
331010
|
+
await openDeepLink(session.sessionId, platform3, options.target.deeplink, options.target.appId);
|
|
331011
|
+
} catch (err) {
|
|
331012
|
+
log2(` Deep link failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
331013
|
+
}
|
|
331014
|
+
}
|
|
331015
|
+
await restoreTargetAppIfExternal(session.sessionId, { appId: options.target.appId, platform: platform3 }, log2, "harvest start", false);
|
|
331016
|
+
for (const step of options.steps) {
|
|
331017
|
+
if (step.action === "launchApp") continue;
|
|
331018
|
+
if (!stepAppliesToPlatform(step, platform3)) continue;
|
|
331019
|
+
let elementId = null;
|
|
331020
|
+
const identityStrategies = buildIdentityLocatorStrategies(step.target);
|
|
331021
|
+
if (identityStrategies.length > 0 && step.target) {
|
|
331022
|
+
try {
|
|
331023
|
+
elementId = await findElement(session.sessionId, step.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true });
|
|
331024
|
+
} catch (err) {
|
|
331025
|
+
if (isTransportError(err)) throw err;
|
|
331026
|
+
elementId = null;
|
|
331027
|
+
}
|
|
331028
|
+
}
|
|
331029
|
+
if (elementId) {
|
|
331030
|
+
const bundle = await readNativeElementBundle(session.sessionId, elementId, platform3);
|
|
331031
|
+
if (bundle) {
|
|
331032
|
+
bundles[step.order] = bundle;
|
|
331033
|
+
log2(` \u2713 Step ${step.order} (${step.action}) resolved on ${platform3}`);
|
|
331034
|
+
} else {
|
|
331035
|
+
divergentOrders.push(step.order);
|
|
331036
|
+
log2(` \u26A0 Step ${step.order} (${step.action}) resolved but has no durable identity \u2014 flagged divergent`);
|
|
331037
|
+
}
|
|
331038
|
+
} else if (stepNeedsLocator(step.action)) {
|
|
331039
|
+
divergentOrders.push(step.order);
|
|
331040
|
+
log2(` \u26A0 Step ${step.order} (${step.action}) could not be resolved on ${platform3} \u2014 needs manual authoring`);
|
|
331041
|
+
}
|
|
331042
|
+
const advanced = await driveHarvestStep(session.sessionId, step, elementId, platform3);
|
|
331043
|
+
if (!advanced && isStateAdvancingAction(step.action)) {
|
|
331044
|
+
log2(` \u2717 Could not advance step ${step.order} (${step.action}) on ${platform3}; stopping harvest early`);
|
|
331045
|
+
break;
|
|
331046
|
+
}
|
|
331047
|
+
reachedOrder = step.order;
|
|
331048
|
+
await restoreTargetAppIfExternal(session.sessionId, { appId: options.target.appId, platform: platform3 }, log2, `harvest step ${step.order}`, false);
|
|
331049
|
+
}
|
|
331050
|
+
} catch (err) {
|
|
331051
|
+
environmentalError = err instanceof Error ? err.message : String(err);
|
|
331052
|
+
log2(` \u2717 Harvest aborted: ${environmentalError}`);
|
|
331053
|
+
} finally {
|
|
331054
|
+
options.onSessionEnding?.(session.deviceId);
|
|
331055
|
+
await session.stop().catch(() => {
|
|
331056
|
+
});
|
|
331057
|
+
}
|
|
331058
|
+
return {
|
|
331059
|
+
platform: platform3,
|
|
331060
|
+
bundles,
|
|
331061
|
+
divergentOrders,
|
|
331062
|
+
reachedOrder,
|
|
331063
|
+
deviceId: session.deviceId,
|
|
331064
|
+
deviceName: session.deviceName,
|
|
331065
|
+
logs: logLines.join("\n"),
|
|
331066
|
+
...environmentalError ? { environmentalError } : {}
|
|
331067
|
+
};
|
|
331068
|
+
}
|
|
330673
331069
|
|
|
330674
331070
|
// src/mobile-device-queue.ts
|
|
330675
331071
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
@@ -331381,7 +331777,146 @@ async function handleSessionStop(_req, res) {
|
|
|
331381
331777
|
stopSessionReaper();
|
|
331382
331778
|
sendJSON(res, 200, { ok: true });
|
|
331383
331779
|
}
|
|
331780
|
+
var runnerChannels = /* @__PURE__ */ new Map();
|
|
331781
|
+
function readDeviceIdParam(req) {
|
|
331782
|
+
try {
|
|
331783
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
331784
|
+
const raw = url.searchParams.get("deviceId");
|
|
331785
|
+
if (!raw) return null;
|
|
331786
|
+
const trimmed = raw.trim();
|
|
331787
|
+
if (!trimmed || trimmed.length > MAX_DEVICE_ID_LENGTH || !DEVICE_ID_PATTERN.test(trimmed)) return null;
|
|
331788
|
+
return trimmed;
|
|
331789
|
+
} catch {
|
|
331790
|
+
return null;
|
|
331791
|
+
}
|
|
331792
|
+
}
|
|
331793
|
+
async function isSessionAlive(sessionId) {
|
|
331794
|
+
try {
|
|
331795
|
+
validateSessionId(sessionId);
|
|
331796
|
+
await appiumRequest2(`/session/${sessionId}/window/rect`);
|
|
331797
|
+
return true;
|
|
331798
|
+
} catch (err) {
|
|
331799
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
331800
|
+
return !/no such session|invalid session id|session is either terminated|deleted session/i.test(msg);
|
|
331801
|
+
}
|
|
331802
|
+
}
|
|
331803
|
+
function writeFrameToChannelClient(ch, client, frame) {
|
|
331804
|
+
try {
|
|
331805
|
+
const buffered = typeof client.writableLength === "number" ? client.writableLength : typeof client.socket?.writableLength === "number" ? client.socket.writableLength : 0;
|
|
331806
|
+
if (buffered > SSE_MAX_BUFFERED_BYTES) {
|
|
331807
|
+
try {
|
|
331808
|
+
client.end();
|
|
331809
|
+
} catch {
|
|
331810
|
+
}
|
|
331811
|
+
ch.clients.delete(client);
|
|
331812
|
+
stopChannelPollingIfIdle(ch);
|
|
331813
|
+
return;
|
|
331814
|
+
}
|
|
331815
|
+
client.write(`data: ${JSON.stringify(frame)}
|
|
331816
|
+
|
|
331817
|
+
`);
|
|
331818
|
+
} catch {
|
|
331819
|
+
}
|
|
331820
|
+
}
|
|
331821
|
+
async function pollRunnerChannel(ch) {
|
|
331822
|
+
if (ch.clients.size === 0 || ch.polling) return;
|
|
331823
|
+
ch.polling = true;
|
|
331824
|
+
try {
|
|
331825
|
+
validateSessionId(ch.sessionId);
|
|
331826
|
+
const [screenshot, source] = await Promise.all([
|
|
331827
|
+
getScreenshot(`/session/${ch.sessionId}`),
|
|
331828
|
+
getPageSource(`/session/${ch.sessionId}`)
|
|
331829
|
+
]);
|
|
331830
|
+
if (!screenshot && !source) {
|
|
331831
|
+
ch.nullTicks++;
|
|
331832
|
+
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
331833
|
+
const alive = await isSessionAlive(ch.sessionId);
|
|
331834
|
+
if (!alive) {
|
|
331835
|
+
closeRunnerChannel(ch);
|
|
331836
|
+
return;
|
|
331837
|
+
}
|
|
331838
|
+
ch.nullTicks = 0;
|
|
331839
|
+
}
|
|
331840
|
+
} else {
|
|
331841
|
+
ch.nullTicks = 0;
|
|
331842
|
+
}
|
|
331843
|
+
const frame = { timestamp: Date.now(), screenshot: screenshot ?? null, source: source ?? null };
|
|
331844
|
+
ch.lastFrame = frame;
|
|
331845
|
+
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
331846
|
+
} catch {
|
|
331847
|
+
} finally {
|
|
331848
|
+
ch.polling = false;
|
|
331849
|
+
}
|
|
331850
|
+
}
|
|
331851
|
+
function ensureChannelPolling(ch) {
|
|
331852
|
+
if (ch.pollTimer) return;
|
|
331853
|
+
ch.pollTimer = setInterval(() => {
|
|
331854
|
+
void pollRunnerChannel(ch);
|
|
331855
|
+
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
331856
|
+
ch.pollTimer.unref?.();
|
|
331857
|
+
}
|
|
331858
|
+
function stopChannelPollingIfIdle(ch) {
|
|
331859
|
+
if (ch.clients.size === 0 && ch.pollTimer) {
|
|
331860
|
+
clearInterval(ch.pollTimer);
|
|
331861
|
+
ch.pollTimer = null;
|
|
331862
|
+
}
|
|
331863
|
+
}
|
|
331864
|
+
function closeRunnerChannel(ch) {
|
|
331865
|
+
if (ch.pollTimer) {
|
|
331866
|
+
clearInterval(ch.pollTimer);
|
|
331867
|
+
ch.pollTimer = null;
|
|
331868
|
+
}
|
|
331869
|
+
for (const client of ch.clients) {
|
|
331870
|
+
try {
|
|
331871
|
+
client.end();
|
|
331872
|
+
} catch {
|
|
331873
|
+
}
|
|
331874
|
+
}
|
|
331875
|
+
ch.clients.clear();
|
|
331876
|
+
ch.lastFrame = null;
|
|
331877
|
+
runnerChannels.delete(ch.deviceId);
|
|
331878
|
+
}
|
|
331879
|
+
function closeAllRunnerChannels() {
|
|
331880
|
+
for (const ch of [...runnerChannels.values()]) closeRunnerChannel(ch);
|
|
331881
|
+
}
|
|
331882
|
+
async function serveRunnerMirror(req, res, ch) {
|
|
331883
|
+
res.writeHead(200, {
|
|
331884
|
+
"Content-Type": "text/event-stream",
|
|
331885
|
+
"Cache-Control": "no-cache",
|
|
331886
|
+
"Connection": "keep-alive",
|
|
331887
|
+
"Access-Control-Allow-Origin": getAllowedOrigin(req),
|
|
331888
|
+
"X-Content-Type-Options": "nosniff"
|
|
331889
|
+
});
|
|
331890
|
+
ch.clients.add(res);
|
|
331891
|
+
ensureChannelPolling(ch);
|
|
331892
|
+
if (ch.lastFrame) {
|
|
331893
|
+
writeFrameToChannelClient(ch, res, ch.lastFrame);
|
|
331894
|
+
} else {
|
|
331895
|
+
await pollRunnerChannel(ch);
|
|
331896
|
+
}
|
|
331897
|
+
req.on("close", () => {
|
|
331898
|
+
ch.clients.delete(res);
|
|
331899
|
+
stopChannelPollingIfIdle(ch);
|
|
331900
|
+
});
|
|
331901
|
+
}
|
|
331384
331902
|
async function handleMirror(req, res) {
|
|
331903
|
+
const deviceId = readDeviceIdParam(req);
|
|
331904
|
+
if (deviceId) {
|
|
331905
|
+
const ch = runnerChannels.get(deviceId);
|
|
331906
|
+
if (ch) {
|
|
331907
|
+
await serveRunnerMirror(req, res, ch);
|
|
331908
|
+
return;
|
|
331909
|
+
}
|
|
331910
|
+
sendJSON(res, 404, { error: `No live mirror for device ${deviceId}.` });
|
|
331911
|
+
return;
|
|
331912
|
+
}
|
|
331913
|
+
if (!state.appiumSessionId && runnerChannels.size === 1) {
|
|
331914
|
+
const soleChannel = runnerChannels.values().next().value;
|
|
331915
|
+
if (soleChannel) {
|
|
331916
|
+
await serveRunnerMirror(req, res, soleChannel);
|
|
331917
|
+
return;
|
|
331918
|
+
}
|
|
331919
|
+
}
|
|
331385
331920
|
if (!state.appiumSessionId) {
|
|
331386
331921
|
sendJSON(res, 400, { error: "No active Appium session. Call /bridge/session/start first." });
|
|
331387
331922
|
return;
|
|
@@ -331828,6 +332363,7 @@ async function startMobileBridge(options) {
|
|
|
331828
332363
|
}
|
|
331829
332364
|
async function stopMobileBridge() {
|
|
331830
332365
|
stopAllMirrorClients();
|
|
332366
|
+
closeAllRunnerChannels();
|
|
331831
332367
|
stopSessionReaper();
|
|
331832
332368
|
clearBridgePidFile();
|
|
331833
332369
|
if (state.appiumSessionId) {
|
|
@@ -331876,8 +332412,31 @@ function getBridgeState() {
|
|
|
331876
332412
|
function setExecutorBusy(busy) {
|
|
331877
332413
|
executorBusyCount = busy ? executorBusyCount + 1 : Math.max(0, executorBusyCount - 1);
|
|
331878
332414
|
}
|
|
331879
|
-
function attachMirrorSession(sessionId, platform3) {
|
|
332415
|
+
function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
331880
332416
|
validateSessionId(sessionId);
|
|
332417
|
+
if (deviceId) {
|
|
332418
|
+
const existing = runnerChannels.get(deviceId);
|
|
332419
|
+
if (existing && existing.sessionId !== sessionId) closeRunnerChannel(existing);
|
|
332420
|
+
const ch = runnerChannels.get(deviceId) ?? {
|
|
332421
|
+
deviceId,
|
|
332422
|
+
sessionId,
|
|
332423
|
+
platform: platform3,
|
|
332424
|
+
clients: /* @__PURE__ */ new Set(),
|
|
332425
|
+
lastFrame: null,
|
|
332426
|
+
pollTimer: null,
|
|
332427
|
+
nullTicks: 0,
|
|
332428
|
+
polling: false
|
|
332429
|
+
};
|
|
332430
|
+
ch.sessionId = sessionId;
|
|
332431
|
+
ch.platform = platform3;
|
|
332432
|
+
ch.nullTicks = 0;
|
|
332433
|
+
runnerChannels.set(deviceId, ch);
|
|
332434
|
+
if (ch.clients.size > 0) {
|
|
332435
|
+
ensureChannelPolling(ch);
|
|
332436
|
+
void pollRunnerChannel(ch);
|
|
332437
|
+
}
|
|
332438
|
+
return;
|
|
332439
|
+
}
|
|
331881
332440
|
if (state.appiumSessionId && state.appiumSessionId !== sessionId) {
|
|
331882
332441
|
stopAllMirrorClients();
|
|
331883
332442
|
}
|
|
@@ -331892,8 +332451,12 @@ function attachMirrorSession(sessionId, platform3) {
|
|
|
331892
332451
|
void pollAndBroadcastFrame();
|
|
331893
332452
|
}
|
|
331894
332453
|
}
|
|
331895
|
-
function detachMirrorSession(
|
|
331896
|
-
if (
|
|
332454
|
+
function detachMirrorSession(deviceId) {
|
|
332455
|
+
if (deviceId) {
|
|
332456
|
+
const ch = runnerChannels.get(deviceId);
|
|
332457
|
+
if (ch) closeRunnerChannel(ch);
|
|
332458
|
+
return;
|
|
332459
|
+
}
|
|
331897
332460
|
state.appiumSessionId = null;
|
|
331898
332461
|
state.platform = null;
|
|
331899
332462
|
mirrorSessionOwner = null;
|
|
@@ -333760,6 +334323,102 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
333760
334323
|
log.fail("ERROR - No discovery context");
|
|
333761
334324
|
return;
|
|
333762
334325
|
}
|
|
334326
|
+
const locatorCtx = ctx;
|
|
334327
|
+
if (locatorCtx.mode === "locator-discovery") {
|
|
334328
|
+
const harvestTarget = locatorCtx.target;
|
|
334329
|
+
const targetTestCaseId = locatorCtx.targetTestCaseId;
|
|
334330
|
+
const platform3 = locatorCtx.platform ?? harvestTarget?.platform;
|
|
334331
|
+
if (!platform3 || !harvestTarget?.appId || !targetTestCaseId) {
|
|
334332
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334333
|
+
status: "ERROR",
|
|
334334
|
+
error: "CONFIG_ISSUE: locator-discovery run is missing platform/appId/targetTestCaseId."
|
|
334335
|
+
});
|
|
334336
|
+
log.fail("ERROR - malformed locator-discovery context");
|
|
334337
|
+
return;
|
|
334338
|
+
}
|
|
334339
|
+
if (getBridgeState().hasActiveSession) {
|
|
334340
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334341
|
+
status: "ERROR",
|
|
334342
|
+
error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; retry once it ends."
|
|
334343
|
+
});
|
|
334344
|
+
log.fail("ERROR - device busy (recording) for locator discovery");
|
|
334345
|
+
return;
|
|
334346
|
+
}
|
|
334347
|
+
const leasedName = assignedDeviceId ? getCachedDeviceSnapshot().find((d) => d.id === assignedDeviceId)?.name : void 0;
|
|
334348
|
+
liveBrowserHandle.patch({
|
|
334349
|
+
mode: "discovery",
|
|
334350
|
+
testName: "Cross-platform locator discovery",
|
|
334351
|
+
note: `${platform3} locator discovery`,
|
|
334352
|
+
surface: "mobile",
|
|
334353
|
+
mobilePlatform: platform3,
|
|
334354
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
|
|
334355
|
+
...leasedName ? { deviceName: leasedName } : {}
|
|
334356
|
+
});
|
|
334357
|
+
requestLiveBrowserHeartbeat(true);
|
|
334358
|
+
log.info(`Cross-platform locator discovery \u2014 ${platform3} / ${harvestTarget.appId} for test ${targetTestCaseId}`);
|
|
334359
|
+
setExecutorBusy(true);
|
|
334360
|
+
try {
|
|
334361
|
+
const harvest = await harvestPlatformLocators({
|
|
334362
|
+
steps: locatorCtx.sourceSteps ?? [],
|
|
334363
|
+
target: {
|
|
334364
|
+
appId: harvestTarget.appId,
|
|
334365
|
+
platform: platform3,
|
|
334366
|
+
platformVersion: harvestTarget.platformVersion,
|
|
334367
|
+
recordedDeviceId: harvestTarget.recordedDeviceId,
|
|
334368
|
+
assignedDeviceId,
|
|
334369
|
+
launchMode: harvestTarget.launchMode,
|
|
334370
|
+
deeplink: harvestTarget.deeplink
|
|
334371
|
+
},
|
|
334372
|
+
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
334373
|
+
attachMirrorSession(sessionId, sessionPlatform, deviceId);
|
|
334374
|
+
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
334375
|
+
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
334376
|
+
requestLiveBrowserHeartbeat(true);
|
|
334377
|
+
},
|
|
334378
|
+
onSessionEnding: (deviceId) => {
|
|
334379
|
+
detachMirrorSession(deviceId);
|
|
334380
|
+
}
|
|
334381
|
+
});
|
|
334382
|
+
let queuedRunId = null;
|
|
334383
|
+
let resultPosted = false;
|
|
334384
|
+
try {
|
|
334385
|
+
const res = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/locator-discovery-result`, {
|
|
334386
|
+
method: "POST",
|
|
334387
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
334388
|
+
body: JSON.stringify({
|
|
334389
|
+
bundles: harvest.bundles,
|
|
334390
|
+
divergentOrders: harvest.divergentOrders,
|
|
334391
|
+
reachedOrder: harvest.reachedOrder,
|
|
334392
|
+
logs: harvest.logs,
|
|
334393
|
+
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
334394
|
+
}),
|
|
334395
|
+
signal: AbortSignal.timeout(3e4)
|
|
334396
|
+
});
|
|
334397
|
+
if (res.ok) {
|
|
334398
|
+
resultPosted = true;
|
|
334399
|
+
const data = await res.json().catch(() => null);
|
|
334400
|
+
queuedRunId = data?.queuedRunId ?? null;
|
|
334401
|
+
} else {
|
|
334402
|
+
log.warn(`[locator-discovery] result POST returned ${res.status}`);
|
|
334403
|
+
}
|
|
334404
|
+
} catch (postErr) {
|
|
334405
|
+
log.warn(`[locator-discovery] result POST failed: ${postErr instanceof Error ? postErr.message : String(postErr)}`);
|
|
334406
|
+
}
|
|
334407
|
+
const passed = resultPosted && !harvest.environmentalError && harvest.reachedOrder != null;
|
|
334408
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334409
|
+
status: passed ? "PASSED" : "ERROR",
|
|
334410
|
+
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,
|
|
334411
|
+
logs: harvest.logs,
|
|
334412
|
+
duration: Date.now() - startTime
|
|
334413
|
+
});
|
|
334414
|
+
log[passed ? "ok" : "fail"](
|
|
334415
|
+
`Locator discovery ${passed ? "complete" : "ended"} \u2014 ${Object.keys(harvest.bundles).length} overlays, ${harvest.divergentOrders.length} divergent${queuedRunId ? ` (queued run ${queuedRunId})` : ""}`
|
|
334416
|
+
);
|
|
334417
|
+
} finally {
|
|
334418
|
+
setExecutorBusy(false);
|
|
334419
|
+
}
|
|
334420
|
+
return;
|
|
334421
|
+
}
|
|
333763
334422
|
log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
|
|
333764
334423
|
const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
|
|
333765
334424
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
@@ -334079,7 +334738,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334079
334738
|
testName: ctx.featureName ?? "Mobile discovery",
|
|
334080
334739
|
note: `${platform3} mobile discovery`,
|
|
334081
334740
|
surface: "mobile",
|
|
334082
|
-
mobilePlatform: platform3
|
|
334741
|
+
mobilePlatform: platform3,
|
|
334742
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {}
|
|
334083
334743
|
});
|
|
334084
334744
|
requestLiveBrowserHeartbeat(true);
|
|
334085
334745
|
mobileOnLog(`Mobile discovery (${ctx.mode}) \u2014 ${platform3} / ${mobileTarget.appId}`);
|
|
@@ -334097,6 +334757,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334097
334757
|
deeplink: mobileTarget.deeplink
|
|
334098
334758
|
}, {
|
|
334099
334759
|
onDeviceSelected: async (device) => {
|
|
334760
|
+
liveBrowserHandle.patch({ deviceId: device.deviceId, deviceName: device.name });
|
|
334100
334761
|
try {
|
|
334101
334762
|
capture = await startMobileNetworkCapture({
|
|
334102
334763
|
platform: platform3,
|
|
@@ -334115,7 +334776,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334115
334776
|
}
|
|
334116
334777
|
});
|
|
334117
334778
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
334118
|
-
attachMirrorSession(session.sessionId, platform3);
|
|
334779
|
+
attachMirrorSession(session.sessionId, platform3, session.deviceId);
|
|
334119
334780
|
const driver = new MobileBridgeDriver({
|
|
334120
334781
|
sessionId: session.sessionId,
|
|
334121
334782
|
platform: platform3,
|
|
@@ -334159,7 +334820,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334159
334820
|
}
|
|
334160
334821
|
}
|
|
334161
334822
|
if (session) {
|
|
334162
|
-
detachMirrorSession(session.
|
|
334823
|
+
detachMirrorSession(session.deviceId);
|
|
334163
334824
|
try {
|
|
334164
334825
|
await session.stop();
|
|
334165
334826
|
} catch {
|
|
@@ -334436,6 +335097,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334436
335097
|
return;
|
|
334437
335098
|
}
|
|
334438
335099
|
log.info(`\u{1F4F1} Mobile test - ${target.platform} / ${target.appId}`);
|
|
335100
|
+
const leasedDeviceName = assignedDeviceId ? getCachedDeviceSnapshot().find((d) => d.id === assignedDeviceId)?.name : void 0;
|
|
335101
|
+
liveBrowserHandle.patch({
|
|
335102
|
+
surface: "mobile",
|
|
335103
|
+
mobilePlatform: target.platform,
|
|
335104
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
|
|
335105
|
+
...leasedDeviceName ? { deviceName: leasedDeviceName } : {}
|
|
335106
|
+
});
|
|
335107
|
+
requestLiveBrowserHeartbeat(true);
|
|
334439
335108
|
const mobileSteps = run2.steps ?? [];
|
|
334440
335109
|
if (mobileSteps.length === 0) {
|
|
334441
335110
|
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
@@ -334463,7 +335132,20 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334463
335132
|
recordedDeviceId: target.recordedDeviceId,
|
|
334464
335133
|
assignedDeviceId,
|
|
334465
335134
|
launchMode: target.launchMode,
|
|
334466
|
-
deeplink: target.deeplink
|
|
335135
|
+
deeplink: target.deeplink,
|
|
335136
|
+
// Authoring platform (test's session medium). When it differs from
|
|
335137
|
+
// target.platform this is a cross-platform run: no blind coordinate
|
|
335138
|
+
// taps on foreign geometry, and the run must verify an outcome.
|
|
335139
|
+
authoringPlatform: mobilePlatformFromMedium(run2.authoringMedium) ?? void 0
|
|
335140
|
+
},
|
|
335141
|
+
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
335142
|
+
// leased device id) so N parallel Appium runs each show a live mirror.
|
|
335143
|
+
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
335144
|
+
attachMirrorSession(sessionId, sessionPlatform, deviceId);
|
|
335145
|
+
requestLiveBrowserHeartbeat(true);
|
|
335146
|
+
},
|
|
335147
|
+
onSessionEnding: (deviceId) => {
|
|
335148
|
+
detachMirrorSession(deviceId);
|
|
334467
335149
|
}
|
|
334468
335150
|
});
|
|
334469
335151
|
attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
|
|
@@ -334479,6 +335161,12 @@ ${result2.logs}`);
|
|
|
334479
335161
|
if (attemptLogs.length > 1) {
|
|
334480
335162
|
result2 = { ...result2, logs: attemptLogs.join("\n\n") };
|
|
334481
335163
|
}
|
|
335164
|
+
if (result2.deviceId || result2.deviceName) {
|
|
335165
|
+
liveBrowserHandle.patch({
|
|
335166
|
+
...result2.deviceId ? { deviceId: result2.deviceId } : {},
|
|
335167
|
+
...result2.deviceName ? { deviceName: result2.deviceName } : {}
|
|
335168
|
+
});
|
|
335169
|
+
}
|
|
334482
335170
|
const duration2 = Date.now() - startTime;
|
|
334483
335171
|
const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
334484
335172
|
const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
|