@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.mjs
CHANGED
|
@@ -1861,6 +1861,102 @@ var init_mobile_test_generation = __esm({
|
|
|
1861
1861
|
}
|
|
1862
1862
|
});
|
|
1863
1863
|
|
|
1864
|
+
// ../shared/dist/mobile-cross-platform.js
|
|
1865
|
+
function isMobilePlatform(value) {
|
|
1866
|
+
return value === "IOS" || value === "ANDROID";
|
|
1867
|
+
}
|
|
1868
|
+
function mediumForMobilePlatform(platform3) {
|
|
1869
|
+
return platform3 === "IOS" ? "IOS_NATIVE" : "ANDROID_NATIVE";
|
|
1870
|
+
}
|
|
1871
|
+
function mobilePlatformFromMedium(medium) {
|
|
1872
|
+
if (medium === "IOS_NATIVE")
|
|
1873
|
+
return "IOS";
|
|
1874
|
+
if (medium === "ANDROID_NATIVE")
|
|
1875
|
+
return "ANDROID";
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
function otherMobilePlatform(platform3) {
|
|
1879
|
+
return platform3 === "IOS" ? "ANDROID" : "IOS";
|
|
1880
|
+
}
|
|
1881
|
+
function platformTag(platform3) {
|
|
1882
|
+
return platform3 === "IOS" ? "@ios" : "@android";
|
|
1883
|
+
}
|
|
1884
|
+
function resolvePlatformLocatorBundle(target, platform3) {
|
|
1885
|
+
if (!target)
|
|
1886
|
+
return void 0;
|
|
1887
|
+
if (platform3) {
|
|
1888
|
+
const overlay = target.platformLocators?.[platform3];
|
|
1889
|
+
if (overlay)
|
|
1890
|
+
return overlay;
|
|
1891
|
+
}
|
|
1892
|
+
return target.locatorBundle;
|
|
1893
|
+
}
|
|
1894
|
+
function hasPlatformLocator(target, platform3) {
|
|
1895
|
+
return Boolean(target?.platformLocators?.[platform3]);
|
|
1896
|
+
}
|
|
1897
|
+
function hasCrossPlatformIdentity(bundle) {
|
|
1898
|
+
if (!bundle)
|
|
1899
|
+
return false;
|
|
1900
|
+
return Boolean(bundle.text?.trim() || bundle.contentDesc?.trim() || bundle.accessibilityId?.trim());
|
|
1901
|
+
}
|
|
1902
|
+
function stepIsDivergentForPlatform(step, platform3) {
|
|
1903
|
+
return Array.isArray(step.crossPlatformDivergent) && step.crossPlatformDivergent.includes(platform3);
|
|
1904
|
+
}
|
|
1905
|
+
function stepAppliesToPlatform(step, platform3) {
|
|
1906
|
+
const platforms = step.platforms;
|
|
1907
|
+
if (!platforms || platforms.length === 0)
|
|
1908
|
+
return true;
|
|
1909
|
+
if (!platform3)
|
|
1910
|
+
return true;
|
|
1911
|
+
return platforms.includes(platform3);
|
|
1912
|
+
}
|
|
1913
|
+
function stepsMissingPlatformLocators(steps, platform3) {
|
|
1914
|
+
return steps.filter((step) => {
|
|
1915
|
+
if (!LOCATOR_REQUIRING_ACTIONS.has(step.action))
|
|
1916
|
+
return false;
|
|
1917
|
+
if (!stepAppliesToPlatform(step, platform3))
|
|
1918
|
+
return false;
|
|
1919
|
+
if (hasPlatformLocator(step.target, platform3))
|
|
1920
|
+
return false;
|
|
1921
|
+
if (stepIsDivergentForPlatform(step, platform3))
|
|
1922
|
+
return false;
|
|
1923
|
+
return hasCrossPlatformIdentity(step.target?.locatorBundle);
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
function stepsNeedingReviewForPlatform(steps, platform3) {
|
|
1927
|
+
return steps.filter((step) => {
|
|
1928
|
+
if (!LOCATOR_REQUIRING_ACTIONS.has(step.action))
|
|
1929
|
+
return false;
|
|
1930
|
+
if (!stepAppliesToPlatform(step, platform3))
|
|
1931
|
+
return false;
|
|
1932
|
+
if (hasPlatformLocator(step.target, platform3))
|
|
1933
|
+
return false;
|
|
1934
|
+
if (stepIsDivergentForPlatform(step, platform3))
|
|
1935
|
+
return true;
|
|
1936
|
+
return !hasCrossPlatformIdentity(step.target?.locatorBundle);
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
function isTestReadyForPlatform(steps, authoringPlatform, runPlatform) {
|
|
1940
|
+
if (runPlatform === authoringPlatform)
|
|
1941
|
+
return true;
|
|
1942
|
+
return stepsMissingPlatformLocators(steps, runPlatform).length === 0 && stepsNeedingReviewForPlatform(steps, runPlatform).length === 0;
|
|
1943
|
+
}
|
|
1944
|
+
var MOBILE_PLATFORMS, LOCATOR_REQUIRING_ACTIONS;
|
|
1945
|
+
var init_mobile_cross_platform = __esm({
|
|
1946
|
+
"../shared/dist/mobile-cross-platform.js"() {
|
|
1947
|
+
"use strict";
|
|
1948
|
+
MOBILE_PLATFORMS = ["IOS", "ANDROID"];
|
|
1949
|
+
LOCATOR_REQUIRING_ACTIONS = /* @__PURE__ */ new Set([
|
|
1950
|
+
"tap",
|
|
1951
|
+
"type",
|
|
1952
|
+
"clear",
|
|
1953
|
+
"waitForElement",
|
|
1954
|
+
"assertVisible",
|
|
1955
|
+
"assertText"
|
|
1956
|
+
]);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1864
1960
|
// ../shared/dist/mobile-deeplink.js
|
|
1865
1961
|
function isSafeDeepLink(value) {
|
|
1866
1962
|
if (typeof value !== "string")
|
|
@@ -3539,6 +3635,7 @@ __export(dist_exports, {
|
|
|
3539
3635
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
3540
3636
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
3541
3637
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
3638
|
+
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
3542
3639
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
3543
3640
|
MOBILE_SNAPSHOT_MAX_SIZE_BYTES: () => MOBILE_SNAPSHOT_MAX_SIZE_BYTES,
|
|
3544
3641
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
@@ -3592,6 +3689,8 @@ __export(dist_exports, {
|
|
|
3592
3689
|
getPlanDefinition: () => getPlanDefinition,
|
|
3593
3690
|
getPlanLabel: () => getPlanLabel,
|
|
3594
3691
|
getTrialDaysRemaining: () => getTrialDaysRemaining,
|
|
3692
|
+
hasCrossPlatformIdentity: () => hasCrossPlatformIdentity,
|
|
3693
|
+
hasPlatformLocator: () => hasPlatformLocator,
|
|
3595
3694
|
hostOfOrigin: () => hostOfOrigin,
|
|
3596
3695
|
isAddonType: () => isAddonType,
|
|
3597
3696
|
isApiTest: () => isApiTest,
|
|
@@ -3601,23 +3700,34 @@ __export(dist_exports, {
|
|
|
3601
3700
|
isCreditPackAmountCents: () => isCreditPackAmountCents,
|
|
3602
3701
|
isDbPricingPlan: () => isDbPricingPlan,
|
|
3603
3702
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
3703
|
+
isMobilePlatform: () => isMobilePlatform,
|
|
3604
3704
|
isPricingPlan: () => isPricingPlan,
|
|
3605
3705
|
isRunnableStep: () => isRunnableStep,
|
|
3606
3706
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
3607
3707
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
3708
|
+
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
3608
3709
|
isTrialExpired: () => isTrialExpired,
|
|
3609
3710
|
isUIVariant: () => isUIVariant,
|
|
3610
3711
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
3712
|
+
mediumForMobilePlatform: () => mediumForMobilePlatform,
|
|
3713
|
+
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
3611
3714
|
normalizeControlName: () => normalizeControlName,
|
|
3715
|
+
otherMobilePlatform: () => otherMobilePlatform,
|
|
3612
3716
|
parenDepthDelta: () => parenDepthDelta,
|
|
3613
3717
|
planHasBillingPortal: () => planHasBillingPortal,
|
|
3614
3718
|
planRequiresCheckout: () => planRequiresCheckout,
|
|
3719
|
+
platformTag: () => platformTag,
|
|
3615
3720
|
registrableDomain: () => registrableDomain,
|
|
3616
3721
|
resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
|
|
3722
|
+
resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
|
|
3617
3723
|
safeOrigin: () => safeOrigin,
|
|
3618
3724
|
sanitizeTestVariables: () => sanitizeTestVariables,
|
|
3619
3725
|
selectorCandidatesForStep: () => selectorCandidatesForStep,
|
|
3620
3726
|
selectorForValue: () => selectorForValue,
|
|
3727
|
+
stepAppliesToPlatform: () => stepAppliesToPlatform,
|
|
3728
|
+
stepIsDivergentForPlatform: () => stepIsDivergentForPlatform,
|
|
3729
|
+
stepsMissingPlatformLocators: () => stepsMissingPlatformLocators,
|
|
3730
|
+
stepsNeedingReviewForPlatform: () => stepsNeedingReviewForPlatform,
|
|
3621
3731
|
stripEnvironmentalPrefix: () => stripEnvironmentalPrefix,
|
|
3622
3732
|
stripLoginScaffolding: () => stripLoginScaffolding,
|
|
3623
3733
|
summarizeHealth: () => summarizeHealth,
|
|
@@ -3631,6 +3741,7 @@ var init_dist = __esm({
|
|
|
3631
3741
|
init_types();
|
|
3632
3742
|
init_constants();
|
|
3633
3743
|
init_mobile_test_generation();
|
|
3744
|
+
init_mobile_cross_platform();
|
|
3634
3745
|
init_mobile_deeplink();
|
|
3635
3746
|
init_plans();
|
|
3636
3747
|
init_audit_pricing();
|
|
@@ -66885,9 +66996,9 @@ ${statePacket}` },
|
|
|
66885
66996
|
if (claimedVerified && !verified) {
|
|
66886
66997
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
66887
66998
|
}
|
|
66888
|
-
const
|
|
66999
|
+
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
66889
67000
|
const reviewTag = verified ? "@verified" : "@needs-review";
|
|
66890
|
-
const tags = ["@discovered", "@mobile",
|
|
67001
|
+
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
66891
67002
|
const test = {
|
|
66892
67003
|
name: compiled.name,
|
|
66893
67004
|
description: recording.finish?.description ?? compiled.description,
|
|
@@ -328123,7 +328234,7 @@ var require_package4 = __commonJS({
|
|
|
328123
328234
|
"package.json"(exports, module) {
|
|
328124
328235
|
module.exports = {
|
|
328125
328236
|
name: "@validate.qa/runner",
|
|
328126
|
-
version: "1.0.
|
|
328237
|
+
version: "1.0.17",
|
|
328127
328238
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328128
328239
|
bin: {
|
|
328129
328240
|
"validate-runner": "dist/cli.js"
|
|
@@ -329667,6 +329778,7 @@ async function startMobileNetworkCapture(options) {
|
|
|
329667
329778
|
|
|
329668
329779
|
// src/services/appium-executor.ts
|
|
329669
329780
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
329781
|
+
var VERIFICATION_ACTIONS = /* @__PURE__ */ new Set(["assertVisible", "assertText", "waitForElement"]);
|
|
329670
329782
|
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
329671
329783
|
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
329672
329784
|
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
@@ -329995,6 +330107,11 @@ function boundsFromStep(step) {
|
|
|
329995
330107
|
}
|
|
329996
330108
|
return null;
|
|
329997
330109
|
}
|
|
330110
|
+
function boundsForRun(step, platform3, isCrossPlatform) {
|
|
330111
|
+
if (isCrossPlatform) return null;
|
|
330112
|
+
if (platform3 && hasPlatformLocator(step.target, platform3)) return null;
|
|
330113
|
+
return boundsFromStep(step);
|
|
330114
|
+
}
|
|
329998
330115
|
function locatorStrategyForValue(value) {
|
|
329999
330116
|
const trimmed = value?.trim();
|
|
330000
330117
|
if (!trimmed) {
|
|
@@ -330014,19 +330131,21 @@ function locatorStrategyForValue(value) {
|
|
|
330014
330131
|
{ using: "id", value: trimmed }
|
|
330015
330132
|
];
|
|
330016
330133
|
}
|
|
330017
|
-
function buildLocatorStrategies(target) {
|
|
330134
|
+
function buildLocatorStrategies(target, platform3) {
|
|
330018
330135
|
if (!target) return [];
|
|
330019
330136
|
const strategies = [];
|
|
330020
|
-
const bundle = target
|
|
330137
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330021
330138
|
if (bundle) {
|
|
330022
330139
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330023
330140
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
330024
330141
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330025
330142
|
if (bundle.className) strategies.push({ using: "class name", value: bundle.className });
|
|
330026
330143
|
}
|
|
330027
|
-
|
|
330028
|
-
|
|
330029
|
-
|
|
330144
|
+
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
330145
|
+
strategies.push(...locatorStrategyForValue(target.primary));
|
|
330146
|
+
for (const fallback of target.fallbacks ?? []) {
|
|
330147
|
+
strategies.push(...locatorStrategyForValue(fallback));
|
|
330148
|
+
}
|
|
330030
330149
|
}
|
|
330031
330150
|
return strategies.filter(
|
|
330032
330151
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
@@ -330052,10 +330171,10 @@ function isIdentityStrategy(strategy) {
|
|
|
330052
330171
|
if (strategy.using === "xpath") return strategy.value.includes("@");
|
|
330053
330172
|
return false;
|
|
330054
330173
|
}
|
|
330055
|
-
function buildIdentityLocatorStrategies(target) {
|
|
330174
|
+
function buildIdentityLocatorStrategies(target, platform3) {
|
|
330056
330175
|
if (!target) return [];
|
|
330057
330176
|
const strategies = [];
|
|
330058
|
-
const bundle = target
|
|
330177
|
+
const bundle = resolvePlatformLocatorBundle(target, platform3);
|
|
330059
330178
|
if (bundle) {
|
|
330060
330179
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
330061
330180
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
@@ -330065,9 +330184,11 @@ function buildIdentityLocatorStrategies(target) {
|
|
|
330065
330184
|
}
|
|
330066
330185
|
if (bundle.xpath && bundle.xpath.includes("@")) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
330067
330186
|
}
|
|
330068
|
-
|
|
330069
|
-
for (const
|
|
330070
|
-
|
|
330187
|
+
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
330188
|
+
for (const value of [target.primary, ...target.fallbacks ?? []]) {
|
|
330189
|
+
for (const strategy of locatorStrategyForValue(value)) {
|
|
330190
|
+
if (isIdentityStrategy(strategy)) strategies.push(strategy);
|
|
330191
|
+
}
|
|
330071
330192
|
}
|
|
330072
330193
|
}
|
|
330073
330194
|
return dedupeStrategies(strategies);
|
|
@@ -330080,7 +330201,7 @@ function readStepTimeout(step) {
|
|
|
330080
330201
|
return void 0;
|
|
330081
330202
|
}
|
|
330082
330203
|
async function findElement(sessionId, target, timeoutMs = DEFAULT_STEP_TIMEOUT_MS, opts) {
|
|
330083
|
-
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target) : buildLocatorStrategies(target);
|
|
330204
|
+
const strategies = opts?.identityOnly ? buildIdentityLocatorStrategies(target, opts?.platform) : buildLocatorStrategies(target, opts?.platform);
|
|
330084
330205
|
if (strategies.length === 0) {
|
|
330085
330206
|
throw new Error(
|
|
330086
330207
|
opts?.identityOnly ? "No identity locator (accessibility id / resource id / text) available for this step \u2014 cannot verify the screen" : "No locator strategies available for this step"
|
|
@@ -330182,18 +330303,18 @@ function shouldAbortAfterStepFailure(action) {
|
|
|
330182
330303
|
return false;
|
|
330183
330304
|
}
|
|
330184
330305
|
}
|
|
330185
|
-
async function executeTap(sessionId, step) {
|
|
330186
|
-
const bounds =
|
|
330306
|
+
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330307
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330187
330308
|
if (step.target) {
|
|
330188
330309
|
try {
|
|
330189
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330310
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330190
330311
|
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, {
|
|
330191
330312
|
method: "POST",
|
|
330192
330313
|
body: "{}"
|
|
330193
330314
|
});
|
|
330194
330315
|
return;
|
|
330195
330316
|
} catch (error2) {
|
|
330196
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target).length > 0) throw error2;
|
|
330317
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330197
330318
|
}
|
|
330198
330319
|
}
|
|
330199
330320
|
if (!bounds) {
|
|
@@ -330204,7 +330325,7 @@ async function executeTap(sessionId, step) {
|
|
|
330204
330325
|
async function executeType(sessionId, step, context) {
|
|
330205
330326
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
330206
330327
|
const value = String(step.value);
|
|
330207
|
-
const bounds =
|
|
330328
|
+
const bounds = boundsForRun(step, context.platform, context.isCrossPlatform ?? false);
|
|
330208
330329
|
let elementId = null;
|
|
330209
330330
|
let lastInputError = null;
|
|
330210
330331
|
let tappedBounds = false;
|
|
@@ -330221,12 +330342,12 @@ async function executeType(sessionId, step, context) {
|
|
|
330221
330342
|
}
|
|
330222
330343
|
if (step.target) {
|
|
330223
330344
|
try {
|
|
330224
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330345
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: context.platform });
|
|
330225
330346
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330226
330347
|
if (!lastInputError) return;
|
|
330227
330348
|
elementId = null;
|
|
330228
330349
|
} catch (error2) {
|
|
330229
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target).length > 0) throw error2;
|
|
330350
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, context.platform).length > 0) throw error2;
|
|
330230
330351
|
lastInputError = error2;
|
|
330231
330352
|
}
|
|
330232
330353
|
}
|
|
@@ -330274,14 +330395,14 @@ async function executeType(sessionId, step, context) {
|
|
|
330274
330395
|
}
|
|
330275
330396
|
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
330276
330397
|
}
|
|
330277
|
-
async function executeClear(sessionId, step) {
|
|
330278
|
-
const bounds =
|
|
330398
|
+
async function executeClear(sessionId, step, platform3, isCrossPlatform = false) {
|
|
330399
|
+
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
330279
330400
|
let elementId = null;
|
|
330280
330401
|
if (step.target) {
|
|
330281
330402
|
try {
|
|
330282
|
-
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
330403
|
+
elementId = await findElement(sessionId, step.target, readStepTimeout(step), { platform: platform3 });
|
|
330283
330404
|
} catch (error2) {
|
|
330284
|
-
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target).length > 0) throw error2;
|
|
330405
|
+
if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
330285
330406
|
}
|
|
330286
330407
|
}
|
|
330287
330408
|
if (!elementId && bounds) {
|
|
@@ -330350,20 +330471,20 @@ async function executeHome(sessionId, platform3) {
|
|
|
330350
330471
|
body: JSON.stringify({ keycode: 3 })
|
|
330351
330472
|
});
|
|
330352
330473
|
}
|
|
330353
|
-
async function executeWaitForElement(sessionId, step) {
|
|
330474
|
+
async function executeWaitForElement(sessionId, step, platform3) {
|
|
330354
330475
|
const timeout = readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS;
|
|
330355
|
-
await findElement(sessionId, step.target, timeout, { identityOnly: true });
|
|
330476
|
+
await findElement(sessionId, step.target, timeout, { identityOnly: true, platform: platform3 });
|
|
330356
330477
|
}
|
|
330357
|
-
async function executeAssertVisible(sessionId, step) {
|
|
330358
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true });
|
|
330478
|
+
async function executeAssertVisible(sessionId, step, platform3) {
|
|
330479
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330359
330480
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
330360
330481
|
if (!result?.value) {
|
|
330361
330482
|
throw new Error(`Element found but not visible: ${step.target?.primary ?? "unknown"}`);
|
|
330362
330483
|
}
|
|
330363
330484
|
}
|
|
330364
|
-
async function executeAssertText(sessionId, step) {
|
|
330485
|
+
async function executeAssertText(sessionId, step, platform3) {
|
|
330365
330486
|
if (!step.assertion) throw new Error("assertText step requires an assertion value");
|
|
330366
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true });
|
|
330487
|
+
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), { identityOnly: true, platform: platform3 });
|
|
330367
330488
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
330368
330489
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
330369
330490
|
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
@@ -330529,12 +330650,14 @@ async function executeMobileTest(options) {
|
|
|
330529
330650
|
}
|
|
330530
330651
|
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
330531
330652
|
let entryDrift = false;
|
|
330653
|
+
const runPlatform = options.target.platform;
|
|
330654
|
+
const isCrossPlatform = options.target.authoringPlatform != null && options.target.authoringPlatform !== runPlatform;
|
|
330532
330655
|
const entryStep = options.steps.find(
|
|
330533
|
-
(s) => s.action !== "launchApp" && buildIdentityLocatorStrategies(s.target).length > 0
|
|
330656
|
+
(s) => s.action !== "launchApp" && stepAppliesToPlatform(s, runPlatform) && buildIdentityLocatorStrategies(s.target, runPlatform).length > 0
|
|
330534
330657
|
);
|
|
330535
330658
|
if (entryStep) {
|
|
330536
330659
|
try {
|
|
330537
|
-
await findElement(sessionId, entryStep.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true });
|
|
330660
|
+
await findElement(sessionId, entryStep.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true, platform: runPlatform });
|
|
330538
330661
|
} catch (err) {
|
|
330539
330662
|
if (isTransportError(err)) throw err;
|
|
330540
330663
|
entryDrift = true;
|
|
@@ -330548,6 +330671,12 @@ async function executeMobileTest(options) {
|
|
|
330548
330671
|
let environmental = null;
|
|
330549
330672
|
for (const step of options.steps) {
|
|
330550
330673
|
if (entryDrift) break;
|
|
330674
|
+
if (!stepAppliesToPlatform(step, runPlatform)) {
|
|
330675
|
+
log2(`Step ${step.order}: ${step.action} \u2014 skipped (not applicable to ${runPlatform})`);
|
|
330676
|
+
stepResults.push({ stepOrder: step.order, status: "skipped", duration: 0 });
|
|
330677
|
+
screenshots.push("");
|
|
330678
|
+
continue;
|
|
330679
|
+
}
|
|
330551
330680
|
const stepStart = Date.now();
|
|
330552
330681
|
log2(`Step ${step.order}: ${step.action} - ${step.description}`);
|
|
330553
330682
|
let status = "passed";
|
|
@@ -330559,17 +330688,18 @@ async function executeMobileTest(options) {
|
|
|
330559
330688
|
log2(" App launched (handled by session creation)");
|
|
330560
330689
|
break;
|
|
330561
330690
|
case "tap":
|
|
330562
|
-
await executeTap(sessionId, step);
|
|
330691
|
+
await executeTap(sessionId, step, runPlatform, isCrossPlatform);
|
|
330563
330692
|
break;
|
|
330564
330693
|
case "type":
|
|
330565
330694
|
await executeType(sessionId, step, {
|
|
330566
|
-
platform:
|
|
330695
|
+
platform: runPlatform,
|
|
330567
330696
|
deviceId: selectedDevice.id,
|
|
330568
|
-
log: log2
|
|
330697
|
+
log: log2,
|
|
330698
|
+
isCrossPlatform
|
|
330569
330699
|
});
|
|
330570
330700
|
break;
|
|
330571
330701
|
case "clear":
|
|
330572
|
-
await executeClear(sessionId, step);
|
|
330702
|
+
await executeClear(sessionId, step, runPlatform, isCrossPlatform);
|
|
330573
330703
|
break;
|
|
330574
330704
|
case "swipe":
|
|
330575
330705
|
case "scroll":
|
|
@@ -330579,16 +330709,16 @@ async function executeMobileTest(options) {
|
|
|
330579
330709
|
await executeBack(sessionId);
|
|
330580
330710
|
break;
|
|
330581
330711
|
case "home":
|
|
330582
|
-
await executeHome(sessionId,
|
|
330712
|
+
await executeHome(sessionId, runPlatform);
|
|
330583
330713
|
break;
|
|
330584
330714
|
case "waitForElement":
|
|
330585
|
-
await executeWaitForElement(sessionId, step);
|
|
330715
|
+
await executeWaitForElement(sessionId, step, runPlatform);
|
|
330586
330716
|
break;
|
|
330587
330717
|
case "assertVisible":
|
|
330588
|
-
await executeAssertVisible(sessionId, step);
|
|
330718
|
+
await executeAssertVisible(sessionId, step, runPlatform);
|
|
330589
330719
|
break;
|
|
330590
330720
|
case "assertText":
|
|
330591
|
-
await executeAssertText(sessionId, step);
|
|
330721
|
+
await executeAssertText(sessionId, step, runPlatform);
|
|
330592
330722
|
break;
|
|
330593
330723
|
default: {
|
|
330594
330724
|
const _exhaustive = step.action;
|
|
@@ -330625,8 +330755,14 @@ async function executeMobileTest(options) {
|
|
|
330625
330755
|
break;
|
|
330626
330756
|
}
|
|
330627
330757
|
}
|
|
330628
|
-
const
|
|
330629
|
-
const
|
|
330758
|
+
const executedResults = stepResults.filter((r) => r.status !== "skipped");
|
|
330759
|
+
const verifyOrders = new Set(
|
|
330760
|
+
options.steps.filter((s) => VERIFICATION_ACTIONS.has(s.action)).map((s) => s.order)
|
|
330761
|
+
);
|
|
330762
|
+
const anyVerificationPassed = stepResults.some((r) => r.status === "passed" && verifyOrders.has(r.stepOrder));
|
|
330763
|
+
const crossPlatformUnverified = isCrossPlatform && verifyOrders.size > 0 && !anyVerificationPassed;
|
|
330764
|
+
const allPassed = !environmental && executedResults.length > 0 && !crossPlatformUnverified && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
330765
|
+
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null) ?? (crossPlatformUnverified ? `Cross-platform run on ${runPlatform} verified no outcome \u2014 every assertion/wait step was skipped or unresolved. Author a ${runPlatform} assertion or resolve the divergence.` : null);
|
|
330630
330766
|
await stopCapture();
|
|
330631
330767
|
await stopRecording();
|
|
330632
330768
|
return {
|
|
@@ -330760,6 +330896,184 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
330760
330896
|
}
|
|
330761
330897
|
};
|
|
330762
330898
|
}
|
|
330899
|
+
async function readElementAttribute(sessionId, elementId, name) {
|
|
330900
|
+
try {
|
|
330901
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(name)}`);
|
|
330902
|
+
const value = result?.value;
|
|
330903
|
+
if (typeof value !== "string") return void 0;
|
|
330904
|
+
const trimmed = value.trim();
|
|
330905
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
330906
|
+
} catch (err) {
|
|
330907
|
+
if (isTransportError(err)) throw err;
|
|
330908
|
+
return void 0;
|
|
330909
|
+
}
|
|
330910
|
+
}
|
|
330911
|
+
function harvestFallbackXpath(platform3, attrs) {
|
|
330912
|
+
if (platform3 === "IOS") {
|
|
330913
|
+
if (attrs.name) return `//*[@name=${xpathLiteral(attrs.name)}]`;
|
|
330914
|
+
const text = attrs.label ?? attrs.value;
|
|
330915
|
+
if (text) return identityTextXpath(text);
|
|
330916
|
+
return "//*";
|
|
330917
|
+
}
|
|
330918
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral(attrs.resourceId)}]`;
|
|
330919
|
+
if (attrs.text) return identityTextXpath(attrs.text);
|
|
330920
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral(attrs.contentDesc)}]`;
|
|
330921
|
+
return "//*";
|
|
330922
|
+
}
|
|
330923
|
+
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
330924
|
+
const className = await readElementAttribute(sessionId, elementId, platform3 === "IOS" ? "type" : "class") ?? "";
|
|
330925
|
+
if (platform3 === "IOS") {
|
|
330926
|
+
const name = await readElementAttribute(sessionId, elementId, "name");
|
|
330927
|
+
const label = await readElementAttribute(sessionId, elementId, "label");
|
|
330928
|
+
const value = await readElementAttribute(sessionId, elementId, "value");
|
|
330929
|
+
const text2 = label ?? value;
|
|
330930
|
+
if (!name && !text2) return null;
|
|
330931
|
+
const bundle2 = {
|
|
330932
|
+
xpath: harvestFallbackXpath("IOS", { name, label, value }),
|
|
330933
|
+
className
|
|
330934
|
+
};
|
|
330935
|
+
if (name) bundle2.accessibilityId = name;
|
|
330936
|
+
if (text2) bundle2.text = text2;
|
|
330937
|
+
return bundle2;
|
|
330938
|
+
}
|
|
330939
|
+
const resourceId = await readElementAttribute(sessionId, elementId, "resource-id");
|
|
330940
|
+
const contentDesc = await readElementAttribute(sessionId, elementId, "content-desc") ?? await readElementAttribute(sessionId, elementId, "contentDescription");
|
|
330941
|
+
const text = await readElementAttribute(sessionId, elementId, "text");
|
|
330942
|
+
if (!resourceId && !contentDesc && !text) return null;
|
|
330943
|
+
const bundle = {
|
|
330944
|
+
xpath: harvestFallbackXpath("ANDROID", { text, contentDesc, resourceId }),
|
|
330945
|
+
className
|
|
330946
|
+
};
|
|
330947
|
+
if (contentDesc) bundle.accessibilityId = contentDesc;
|
|
330948
|
+
if (resourceId) bundle.resourceId = resourceId;
|
|
330949
|
+
if (text) bundle.text = text;
|
|
330950
|
+
if (contentDesc) bundle.contentDesc = contentDesc;
|
|
330951
|
+
return bundle;
|
|
330952
|
+
}
|
|
330953
|
+
function isStateAdvancingAction(action) {
|
|
330954
|
+
return action === "tap" || action === "type" || action === "clear" || action === "swipe" || action === "scroll" || action === "back" || action === "home";
|
|
330955
|
+
}
|
|
330956
|
+
function stepNeedsLocator(action) {
|
|
330957
|
+
return action === "tap" || action === "type" || action === "clear" || action === "waitForElement" || action === "assertVisible" || action === "assertText";
|
|
330958
|
+
}
|
|
330959
|
+
async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
330960
|
+
switch (step.action) {
|
|
330961
|
+
case "tap":
|
|
330962
|
+
if (!elementId) return false;
|
|
330963
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
330964
|
+
return true;
|
|
330965
|
+
case "type": {
|
|
330966
|
+
if (step.value === void 0 || step.value === null || step.value === "") return true;
|
|
330967
|
+
if (!elementId) return false;
|
|
330968
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/click`, { method: "POST", body: "{}" });
|
|
330969
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330970
|
+
const err = await trySendElementValue(sessionId, elementId, String(step.value));
|
|
330971
|
+
return !err;
|
|
330972
|
+
}
|
|
330973
|
+
case "clear":
|
|
330974
|
+
if (!elementId) return false;
|
|
330975
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/clear`, { method: "POST", body: "{}" });
|
|
330976
|
+
return true;
|
|
330977
|
+
case "swipe":
|
|
330978
|
+
case "scroll":
|
|
330979
|
+
await executeSwipe(sessionId, step);
|
|
330980
|
+
return true;
|
|
330981
|
+
case "back":
|
|
330982
|
+
await executeBack(sessionId);
|
|
330983
|
+
return true;
|
|
330984
|
+
case "home":
|
|
330985
|
+
await executeHome(sessionId, platform3);
|
|
330986
|
+
return true;
|
|
330987
|
+
case "launchApp":
|
|
330988
|
+
case "waitForElement":
|
|
330989
|
+
case "assertVisible":
|
|
330990
|
+
case "assertText":
|
|
330991
|
+
return true;
|
|
330992
|
+
}
|
|
330993
|
+
}
|
|
330994
|
+
async function harvestPlatformLocators(options) {
|
|
330995
|
+
const platform3 = options.target.platform;
|
|
330996
|
+
const logLines = [];
|
|
330997
|
+
const log2 = (msg) => {
|
|
330998
|
+
logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
330999
|
+
};
|
|
331000
|
+
const bundles = {};
|
|
331001
|
+
const divergentOrders = [];
|
|
331002
|
+
let reachedOrder = null;
|
|
331003
|
+
let environmentalError;
|
|
331004
|
+
log2(`Cross-platform locator discovery \u2014 resolving ${platform3} locators for ${options.steps.length} steps`);
|
|
331005
|
+
const session = await createMobileDiscoverySession({
|
|
331006
|
+
appId: options.target.appId,
|
|
331007
|
+
platform: platform3,
|
|
331008
|
+
platformVersion: options.target.platformVersion,
|
|
331009
|
+
recordedDeviceId: options.target.recordedDeviceId,
|
|
331010
|
+
assignedDeviceId: options.target.assignedDeviceId,
|
|
331011
|
+
launchMode: "LAUNCH_APP"
|
|
331012
|
+
});
|
|
331013
|
+
options.onSessionReady?.(session.sessionId, session.deviceId, platform3);
|
|
331014
|
+
try {
|
|
331015
|
+
await freshLaunchTargetApp(session.sessionId, platform3, options.target.appId, log2);
|
|
331016
|
+
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
331017
|
+
try {
|
|
331018
|
+
await openDeepLink(session.sessionId, platform3, options.target.deeplink, options.target.appId);
|
|
331019
|
+
} catch (err) {
|
|
331020
|
+
log2(` Deep link failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
331021
|
+
}
|
|
331022
|
+
}
|
|
331023
|
+
await restoreTargetAppIfExternal(session.sessionId, { appId: options.target.appId, platform: platform3 }, log2, "harvest start", false);
|
|
331024
|
+
for (const step of options.steps) {
|
|
331025
|
+
if (step.action === "launchApp") continue;
|
|
331026
|
+
if (!stepAppliesToPlatform(step, platform3)) continue;
|
|
331027
|
+
let elementId = null;
|
|
331028
|
+
const identityStrategies = buildIdentityLocatorStrategies(step.target);
|
|
331029
|
+
if (identityStrategies.length > 0 && step.target) {
|
|
331030
|
+
try {
|
|
331031
|
+
elementId = await findElement(session.sessionId, step.target, DEFAULT_STEP_TIMEOUT_MS, { identityOnly: true });
|
|
331032
|
+
} catch (err) {
|
|
331033
|
+
if (isTransportError(err)) throw err;
|
|
331034
|
+
elementId = null;
|
|
331035
|
+
}
|
|
331036
|
+
}
|
|
331037
|
+
if (elementId) {
|
|
331038
|
+
const bundle = await readNativeElementBundle(session.sessionId, elementId, platform3);
|
|
331039
|
+
if (bundle) {
|
|
331040
|
+
bundles[step.order] = bundle;
|
|
331041
|
+
log2(` \u2713 Step ${step.order} (${step.action}) resolved on ${platform3}`);
|
|
331042
|
+
} else {
|
|
331043
|
+
divergentOrders.push(step.order);
|
|
331044
|
+
log2(` \u26A0 Step ${step.order} (${step.action}) resolved but has no durable identity \u2014 flagged divergent`);
|
|
331045
|
+
}
|
|
331046
|
+
} else if (stepNeedsLocator(step.action)) {
|
|
331047
|
+
divergentOrders.push(step.order);
|
|
331048
|
+
log2(` \u26A0 Step ${step.order} (${step.action}) could not be resolved on ${platform3} \u2014 needs manual authoring`);
|
|
331049
|
+
}
|
|
331050
|
+
const advanced = await driveHarvestStep(session.sessionId, step, elementId, platform3);
|
|
331051
|
+
if (!advanced && isStateAdvancingAction(step.action)) {
|
|
331052
|
+
log2(` \u2717 Could not advance step ${step.order} (${step.action}) on ${platform3}; stopping harvest early`);
|
|
331053
|
+
break;
|
|
331054
|
+
}
|
|
331055
|
+
reachedOrder = step.order;
|
|
331056
|
+
await restoreTargetAppIfExternal(session.sessionId, { appId: options.target.appId, platform: platform3 }, log2, `harvest step ${step.order}`, false);
|
|
331057
|
+
}
|
|
331058
|
+
} catch (err) {
|
|
331059
|
+
environmentalError = err instanceof Error ? err.message : String(err);
|
|
331060
|
+
log2(` \u2717 Harvest aborted: ${environmentalError}`);
|
|
331061
|
+
} finally {
|
|
331062
|
+
options.onSessionEnding?.(session.deviceId);
|
|
331063
|
+
await session.stop().catch(() => {
|
|
331064
|
+
});
|
|
331065
|
+
}
|
|
331066
|
+
return {
|
|
331067
|
+
platform: platform3,
|
|
331068
|
+
bundles,
|
|
331069
|
+
divergentOrders,
|
|
331070
|
+
reachedOrder,
|
|
331071
|
+
deviceId: session.deviceId,
|
|
331072
|
+
deviceName: session.deviceName,
|
|
331073
|
+
logs: logLines.join("\n"),
|
|
331074
|
+
...environmentalError ? { environmentalError } : {}
|
|
331075
|
+
};
|
|
331076
|
+
}
|
|
330763
331077
|
|
|
330764
331078
|
// src/mobile-device-queue.ts
|
|
330765
331079
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
@@ -334017,6 +334331,102 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334017
334331
|
log.fail("ERROR - No discovery context");
|
|
334018
334332
|
return;
|
|
334019
334333
|
}
|
|
334334
|
+
const locatorCtx = ctx;
|
|
334335
|
+
if (locatorCtx.mode === "locator-discovery") {
|
|
334336
|
+
const harvestTarget = locatorCtx.target;
|
|
334337
|
+
const targetTestCaseId = locatorCtx.targetTestCaseId;
|
|
334338
|
+
const platform3 = locatorCtx.platform ?? harvestTarget?.platform;
|
|
334339
|
+
if (!platform3 || !harvestTarget?.appId || !targetTestCaseId) {
|
|
334340
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334341
|
+
status: "ERROR",
|
|
334342
|
+
error: "CONFIG_ISSUE: locator-discovery run is missing platform/appId/targetTestCaseId."
|
|
334343
|
+
});
|
|
334344
|
+
log.fail("ERROR - malformed locator-discovery context");
|
|
334345
|
+
return;
|
|
334346
|
+
}
|
|
334347
|
+
if (getBridgeState().hasActiveSession) {
|
|
334348
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334349
|
+
status: "ERROR",
|
|
334350
|
+
error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; retry once it ends."
|
|
334351
|
+
});
|
|
334352
|
+
log.fail("ERROR - device busy (recording) for locator discovery");
|
|
334353
|
+
return;
|
|
334354
|
+
}
|
|
334355
|
+
const leasedName = assignedDeviceId ? getCachedDeviceSnapshot().find((d) => d.id === assignedDeviceId)?.name : void 0;
|
|
334356
|
+
liveBrowserHandle.patch({
|
|
334357
|
+
mode: "discovery",
|
|
334358
|
+
testName: "Cross-platform locator discovery",
|
|
334359
|
+
note: `${platform3} locator discovery`,
|
|
334360
|
+
surface: "mobile",
|
|
334361
|
+
mobilePlatform: platform3,
|
|
334362
|
+
...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
|
|
334363
|
+
...leasedName ? { deviceName: leasedName } : {}
|
|
334364
|
+
});
|
|
334365
|
+
requestLiveBrowserHeartbeat(true);
|
|
334366
|
+
log.info(`Cross-platform locator discovery \u2014 ${platform3} / ${harvestTarget.appId} for test ${targetTestCaseId}`);
|
|
334367
|
+
setExecutorBusy(true);
|
|
334368
|
+
try {
|
|
334369
|
+
const harvest = await harvestPlatformLocators({
|
|
334370
|
+
steps: locatorCtx.sourceSteps ?? [],
|
|
334371
|
+
target: {
|
|
334372
|
+
appId: harvestTarget.appId,
|
|
334373
|
+
platform: platform3,
|
|
334374
|
+
platformVersion: harvestTarget.platformVersion,
|
|
334375
|
+
recordedDeviceId: harvestTarget.recordedDeviceId,
|
|
334376
|
+
assignedDeviceId,
|
|
334377
|
+
launchMode: harvestTarget.launchMode,
|
|
334378
|
+
deeplink: harvestTarget.deeplink
|
|
334379
|
+
},
|
|
334380
|
+
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
334381
|
+
attachMirrorSession(sessionId, sessionPlatform, deviceId);
|
|
334382
|
+
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
334383
|
+
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
334384
|
+
requestLiveBrowserHeartbeat(true);
|
|
334385
|
+
},
|
|
334386
|
+
onSessionEnding: (deviceId) => {
|
|
334387
|
+
detachMirrorSession(deviceId);
|
|
334388
|
+
}
|
|
334389
|
+
});
|
|
334390
|
+
let queuedRunId = null;
|
|
334391
|
+
let resultPosted = false;
|
|
334392
|
+
try {
|
|
334393
|
+
const res = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/locator-discovery-result`, {
|
|
334394
|
+
method: "POST",
|
|
334395
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
334396
|
+
body: JSON.stringify({
|
|
334397
|
+
bundles: harvest.bundles,
|
|
334398
|
+
divergentOrders: harvest.divergentOrders,
|
|
334399
|
+
reachedOrder: harvest.reachedOrder,
|
|
334400
|
+
logs: harvest.logs,
|
|
334401
|
+
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
334402
|
+
}),
|
|
334403
|
+
signal: AbortSignal.timeout(3e4)
|
|
334404
|
+
});
|
|
334405
|
+
if (res.ok) {
|
|
334406
|
+
resultPosted = true;
|
|
334407
|
+
const data = await res.json().catch(() => null);
|
|
334408
|
+
queuedRunId = data?.queuedRunId ?? null;
|
|
334409
|
+
} else {
|
|
334410
|
+
log.warn(`[locator-discovery] result POST returned ${res.status}`);
|
|
334411
|
+
}
|
|
334412
|
+
} catch (postErr) {
|
|
334413
|
+
log.warn(`[locator-discovery] result POST failed: ${postErr instanceof Error ? postErr.message : String(postErr)}`);
|
|
334414
|
+
}
|
|
334415
|
+
const passed = resultPosted && !harvest.environmentalError && harvest.reachedOrder != null;
|
|
334416
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
334417
|
+
status: passed ? "PASSED" : "ERROR",
|
|
334418
|
+
error: !resultPosted ? "CONFIG_ISSUE: failed to persist harvested cross-platform locators (result POST did not succeed); retriable." : harvest.environmentalError ? `CONFIG_ISSUE: ${harvest.environmentalError}` : harvest.reachedOrder == null ? "CONFIG_ISSUE: locator discovery could not reach any step on the target platform." : void 0,
|
|
334419
|
+
logs: harvest.logs,
|
|
334420
|
+
duration: Date.now() - startTime
|
|
334421
|
+
});
|
|
334422
|
+
log[passed ? "ok" : "fail"](
|
|
334423
|
+
`Locator discovery ${passed ? "complete" : "ended"} \u2014 ${Object.keys(harvest.bundles).length} overlays, ${harvest.divergentOrders.length} divergent${queuedRunId ? ` (queued run ${queuedRunId})` : ""}`
|
|
334424
|
+
);
|
|
334425
|
+
} finally {
|
|
334426
|
+
setExecutorBusy(false);
|
|
334427
|
+
}
|
|
334428
|
+
return;
|
|
334429
|
+
}
|
|
334020
334430
|
log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
|
|
334021
334431
|
const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
|
|
334022
334432
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
@@ -334730,7 +335140,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
334730
335140
|
recordedDeviceId: target.recordedDeviceId,
|
|
334731
335141
|
assignedDeviceId,
|
|
334732
335142
|
launchMode: target.launchMode,
|
|
334733
|
-
deeplink: target.deeplink
|
|
335143
|
+
deeplink: target.deeplink,
|
|
335144
|
+
// Authoring platform (test's session medium). When it differs from
|
|
335145
|
+
// target.platform this is a cross-platform run: no blind coordinate
|
|
335146
|
+
// taps on foreign geometry, and the run must verify an outcome.
|
|
335147
|
+
authoringPlatform: mobilePlatformFromMedium(run2.authoringMedium) ?? void 0
|
|
334734
335148
|
},
|
|
334735
335149
|
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
334736
335150
|
// leased device id) so N parallel Appium runs each show a live mirror.
|