@validate.qa/runner 1.0.16 → 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 +458 -44
- package/dist/cli.mjs +458 -44
- 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();
|
|
@@ -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;
|
|
@@ -329987,6 +330099,11 @@ function boundsFromStep(step) {
|
|
|
329987
330099
|
}
|
|
329988
330100
|
return null;
|
|
329989
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
|
+
}
|
|
329990
330107
|
function locatorStrategyForValue(value) {
|
|
329991
330108
|
const trimmed = value?.trim();
|
|
329992
330109
|
if (!trimmed) {
|
|
@@ -330006,19 +330123,21 @@ function locatorStrategyForValue(value) {
|
|
|
330006
330123
|
{ using: "id", value: trimmed }
|
|
330007
330124
|
];
|
|
330008
330125
|
}
|
|
330009
|
-
function buildLocatorStrategies(target) {
|
|
330126
|
+
function buildLocatorStrategies(target, platform3) {
|
|
330010
330127
|
if (!target) return [];
|
|
330011
330128
|
const strategies = [];
|
|
330012
|
-
const bundle = target
|
|
330129
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330013
330130
|
if (bundle) {
|
|
330014
330131
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330015
330132
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
330016
330133
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330017
330134
|
if (bundle.className) strategies.push({ using: "class name", value: bundle.className });
|
|
330018
330135
|
}
|
|
330019
|
-
|
|
330020
|
-
|
|
330021
|
-
|
|
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
|
+
}
|
|
330022
330141
|
}
|
|
330023
330142
|
return strategies.filter(
|
|
330024
330143
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
@@ -330044,10 +330163,10 @@ function isIdentityStrategy(strategy) {
|
|
|
330044
330163
|
if (strategy.using === "xpath") return strategy.value.includes("@");
|
|
330045
330164
|
return false;
|
|
330046
330165
|
}
|
|
330047
|
-
function buildIdentityLocatorStrategies(target) {
|
|
330166
|
+
function buildIdentityLocatorStrategies(target, platform3) {
|
|
330048
330167
|
if (!target) return [];
|
|
330049
330168
|
const strategies = [];
|
|
330050
|
-
const bundle = target
|
|
330169
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330051
330170
|
if (bundle) {
|
|
330052
330171
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330053
330172
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
@@ -330057,9 +330176,11 @@ function buildIdentityLocatorStrategies(target) {
|
|
|
330057
330176
|
}
|
|
330058
330177
|
if (bundle.xpath && bundle.xpath.includes("@")) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330059
330178
|
}
|
|
330060
|
-
|
|
330061
|
-
for (const
|
|
330062
|
-
|
|
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
|
+
}
|
|
330063
330184
|
}
|
|
330064
330185
|
}
|
|
330065
330186
|
return dedupeStrategies(strategies);
|
|
@@ -330072,7 +330193,7 @@ function readStepTimeout(step) {
|
|
|
330072
330193
|
return void 0;
|
|
330073
330194
|
}
|
|
330074
330195
|
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
330075
|
-
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target) : buildLocatorStrategies(target);
|
|
330196
|
+
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
330076
330197
|
if (strategies.length === 0) {
|
|
330077
330198
|
throw new Error(
|
|
330078
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"
|
|
@@ -330174,18 +330295,18 @@ function shouldAbortAfterStepFailure(action) {
|
|
|
330174
330295
|
return false;
|
|
330175
330296
|
}
|
|
330176
330297
|
}
|
|
330177
|
-
async function executeTap(sessionId, step) {
|
|
330178
|
-
const bounds =
|
|
330298
|
+
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330299
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330179
330300
|
if (step.target) {
|
|
330180
330301
|
try {
|
|
330181
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330302
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330182
330303
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
330183
330304
|
method: "POST",
|
|
330184
330305
|
body: "{}"
|
|
330185
330306
|
});
|
|
330186
330307
|
return;
|
|
330187
330308
|
} catch (error2) {
|
|
330188
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target).length > 0) throw error2;
|
|
330309
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330189
330310
|
}
|
|
330190
330311
|
}
|
|
330191
330312
|
if (!bounds) {
|
|
@@ -330196,7 +330317,7 @@ async function executeTap(sessionId, step) {
|
|
|
330196
330317
|
async function executeType(sessionId, step, context) {
|
|
330197
330318
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
330198
330319
|
const value = String(step.value);
|
|
330199
|
-
const bounds =
|
|
330320
|
+
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
330200
330321
|
let elementId = null;
|
|
330201
330322
|
let lastInputError = null;
|
|
330202
330323
|
let tappedBounds = false;
|
|
@@ -330213,12 +330334,12 @@ async function executeType(sessionId, step, context) {
|
|
|
330213
330334
|
}
|
|
330214
330335
|
if (step.target) {
|
|
330215
330336
|
try {
|
|
330216
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330337
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
330217
330338
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330218
330339
|
if (!lastInputError) return;
|
|
330219
330340
|
elementId = null;
|
|
330220
330341
|
} catch (error2) {
|
|
330221
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target).length > 0) throw error2;
|
|
330342
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
330222
330343
|
lastInputError = error2;
|
|
330223
330344
|
}
|
|
330224
330345
|
}
|
|
@@ -330266,14 +330387,14 @@ async function executeType(sessionId, step, context) {
|
|
|
330266
330387
|
}
|
|
330267
330388
|
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
330268
330389
|
}
|
|
330269
|
-
async function executeClear(sessionId, step) {
|
|
330270
|
-
const bounds =
|
|
330390
|
+
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330391
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330271
330392
|
let elementId = null;
|
|
330272
330393
|
if (step.target) {
|
|
330273
330394
|
try {
|
|
330274
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330395
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330275
330396
|
} catch (error2) {
|
|
330276
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target).length > 0) throw error2;
|
|
330397
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330277
330398
|
}
|
|
330278
330399
|
}
|
|
330279
330400
|
if (!elementId && bounds) {
|
|
@@ -330342,20 +330463,20 @@ async function executeHome(sessionId, platform3) {
|
|
|
330342
330463
|
body: JSON.stringify({ keycode: 3 })
|
|
330343
330464
|
});
|
|
330344
330465
|
}
|
|
330345
|
-
async function executeWaitForElement(sessionId, step) {
|
|
330466
|
+
async function executeWaitForElement(sessionId, step, platform3) {
|
|
330346
330467
|
const timeout = readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS;
|
|
330347
|
-
await findElement(sessionId, step.target, timeout, { identityOnly: true });
|
|
330468
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
330348
330469
|
}
|
|
330349
|
-
async function executeAssertVisible(sessionId, step) {
|
|
330350
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true });
|
|
330470
|
+
async function executeAssertVisible(sessionId, step, platform3) {
|
|
330471
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330351
330472
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
330352
330473
|
if (!result?.value) {
|
|
330353
330474
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
330354
330475
|
}
|
|
330355
330476
|
}
|
|
330356
|
-
async function executeAssertText(sessionId, step) {
|
|
330477
|
+
async function executeAssertText(sessionId, step, platform3) {
|
|
330357
330478
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
330358
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true });
|
|
330479
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330359
330480
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
330360
330481
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
330361
330482
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -330521,12 +330642,14 @@ async function executeMobileTest(options) {
|
|
|
330521
330642
|
}
|
|
330522
330643
|
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
330523
330644
|
let entryDrift = false;
|
|
330645
|
+
const runPlatform = options.target.platform;
|
|
330646
|
+
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
330524
330647
|
const entryStep = options.steps.find(
|
|
330525
|
-
(s) => s.action !== "launchApp" && buildIdentityLocatorStrategies(s.target).length > 0
|
|
330648
|
+
(s) => s.action !== "launchApp" && stepAppliesToPlatform(s, runPlatform) && buildIdentityLocatorStrategies(s.target, runPlatform).length > 0
|
|
330526
330649
|
);
|
|
330527
330650
|
if (entryStep) {
|
|
330528
330651
|
try {
|
|
330529
|
-
await findElement(sessionId, entryStep.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true });
|
|
330652
|
+
await findElement(sessionId, entryStep.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true, platform: runPlatform });
|
|
330530
330653
|
} catch (err) {
|
|
330531
330654
|
if (isTransportError(err)) throw err;
|
|
330532
330655
|
entryDrift = true;
|
|
@@ -330540,6 +330663,12 @@ async function executeMobileTest(options) {
|
|
|
330540
330663
|
let environmental = null;
|
|
330541
330664
|
for (const step of options.steps) {
|
|
330542
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
|
+
}
|
|
330543
330672
|
const stepStart = Date.now();
|
|
330544
330673
|
log2(`Step ${step.order}: ${step.action} - ${step.description}`);
|
|
330545
330674
|
let status = "passed";
|
|
@@ -330551,17 +330680,18 @@ async function executeMobileTest(options) {
|
|
|
330551
330680
|
log2(" App launched (handled by session creation)");
|
|
330552
330681
|
break;
|
|
330553
330682
|
case "tap":
|
|
330554
|
-
await executeTap(sessionId, step);
|
|
330683
|
+
await executeTap(sessionId, step, runPlatform, isCrossPlatform);
|
|
330555
330684
|
break;
|
|
330556
330685
|
case "type":
|
|
330557
330686
|
await executeType(sessionId, step, {
|
|
330558
|
-
platform:
|
|
330687
|
+
platform: runPlatform,
|
|
330559
330688
|
deviceId: selectedDevice.id,
|
|
330560
|
-
log: log2
|
|
330689
|
+
log: log2,
|
|
330690
|
+
isCrossPlatform
|
|
330561
330691
|
});
|
|
330562
330692
|
break;
|
|
330563
330693
|
case "clear":
|
|
330564
|
-
await executeClear(sessionId, step);
|
|
330694
|
+
await executeClear(sessionId, step, runPlatform, isCrossPlatform);
|
|
330565
330695
|
break;
|
|
330566
330696
|
case "swipe":
|
|
330567
330697
|
case "scroll":
|
|
@@ -330571,16 +330701,16 @@ async function executeMobileTest(options) {
|
|
|
330571
330701
|
await executeBack(sessionId);
|
|
330572
330702
|
break;
|
|
330573
330703
|
case "home":
|
|
330574
|
-
await executeHome(sessionId,
|
|
330704
|
+
await executeHome(sessionId, runPlatform);
|
|
330575
330705
|
break;
|
|
330576
330706
|
case "waitForElement":
|
|
330577
|
-
await executeWaitForElement(sessionId, step);
|
|
330707
|
+
await executeWaitForElement(sessionId, step, runPlatform);
|
|
330578
330708
|
break;
|
|
330579
330709
|
case "assertVisible":
|
|
330580
|
-
await executeAssertVisible(sessionId, step);
|
|
330710
|
+
await executeAssertVisible(sessionId, step, runPlatform);
|
|
330581
330711
|
break;
|
|
330582
330712
|
case "assertText":
|
|
330583
|
-
await executeAssertText(sessionId, step);
|
|
330713
|
+
await executeAssertText(sessionId, step, runPlatform);
|
|
330584
330714
|
break;
|
|
330585
330715
|
default: {
|
|
330586
330716
|
const _exhaustive = step.action;
|
|
@@ -330617,8 +330747,14 @@ async function executeMobileTest(options) {
|
|
|
330617
330747
|
break;
|
|
330618
330748
|
}
|
|
330619
330749
|
}
|
|
330620
|
-
const
|
|
330621
|
-
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);
|
|
330622
330758
|
await stopCapture();
|
|
330623
330759
|
await stopRecording();
|
|
330624
330760
|
return {
|
|
@@ -330752,6 +330888,184 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330752
330888
|
}
|
|
330753
330889
|
};
|
|
330754
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
|
+
}
|
|
330755
331069
|
|
|
330756
331070
|
// src/mobile-device-queue.ts
|
|
330757
331071
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
@@ -334009,6 +334323,102 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334009
334323
|
log.fail("ERROR - No discovery context");
|
|
334010
334324
|
return;
|
|
334011
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
|
+
}
|
|
334012
334422
|
log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
|
|
334013
334423
|
const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
|
|
334014
334424
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
@@ -334722,7 +335132,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334722
335132
|
recordedDeviceId: target.recordedDeviceId,
|
|
334723
335133
|
assignedDeviceId,
|
|
334724
335134
|
launchMode: target.launchMode,
|
|
334725
|
-
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
|
|
334726
335140
|
},
|
|
334727
335141
|
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
334728
335142
|
// leased device id) so N parallel Appium runs each show a live mirror.
|