@validate.qa/runner 1.0.19 → 1.0.20
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 +322 -243
- package/dist/cli.mjs +322 -243
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1532,7 +1532,7 @@ function buildMobileStep(action, order) {
|
|
|
1532
1532
|
}
|
|
1533
1533
|
return step;
|
|
1534
1534
|
}
|
|
1535
|
-
function buildLaunchStep(target, medium) {
|
|
1535
|
+
function buildLaunchStep(target, medium, appResetOnLaunch = false) {
|
|
1536
1536
|
const screenName = target.deeplink ? "DeepLinkEntry" : `${target.platform === "IOS" ? "IOS" : "Android"}Launch`;
|
|
1537
1537
|
const description = target.launchMode === "DEEP_LINK" ? `Launch ${target.appId} via deep link` : `Launch ${target.appId} on ${medium === "IOS_NATIVE" ? "iOS" : "Android"}`;
|
|
1538
1538
|
return {
|
|
@@ -1549,7 +1549,10 @@ function buildLaunchStep(target, medium) {
|
|
|
1549
1549
|
platform: target.platform,
|
|
1550
1550
|
deeplink: target.deeplink,
|
|
1551
1551
|
deviceId: target.recordedDeviceId,
|
|
1552
|
-
platformVersion: target.platformVersion
|
|
1552
|
+
platformVersion: target.platformVersion,
|
|
1553
|
+
// Recorded from a cleared app-data state: replay must clear before launch
|
|
1554
|
+
// (policy-gated in the executor) to recreate the recording's entry state.
|
|
1555
|
+
...appResetOnLaunch ? { appReset: "cleared" } : {}
|
|
1553
1556
|
}
|
|
1554
1557
|
};
|
|
1555
1558
|
}
|
|
@@ -1800,7 +1803,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1800
1803
|
function generateMobileTestFromSession(input) {
|
|
1801
1804
|
const steps = [];
|
|
1802
1805
|
if (input.target) {
|
|
1803
|
-
steps.push(buildLaunchStep(input.target, input.medium));
|
|
1806
|
+
steps.push(buildLaunchStep(input.target, input.medium, input.appResetOnLaunch === true));
|
|
1804
1807
|
}
|
|
1805
1808
|
for (const action of input.actions) {
|
|
1806
1809
|
const step = buildMobileStep(action, steps.length);
|
|
@@ -67165,7 +67168,7 @@ ${statePacket}` },
|
|
|
67165
67168
|
return out;
|
|
67166
67169
|
}
|
|
67167
67170
|
function assembleTest(args) {
|
|
67168
|
-
const { scenario, recording, ctx, platform: platform3, log: log2 } = args;
|
|
67171
|
+
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
67169
67172
|
const actions = collapseRetryCycles(recording.actions);
|
|
67170
67173
|
if (actions.length < recording.actions.length) {
|
|
67171
67174
|
log2(` collapsed ${recording.actions.length - actions.length} retry-cycle step(s) out of the recorded trace (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
@@ -67182,7 +67185,8 @@ ${statePacket}` },
|
|
|
67182
67185
|
sessionName: recording.finish?.name ?? scenario.name,
|
|
67183
67186
|
medium,
|
|
67184
67187
|
target,
|
|
67185
|
-
actions
|
|
67188
|
+
actions,
|
|
67189
|
+
appResetOnLaunch: appDataCleared
|
|
67186
67190
|
};
|
|
67187
67191
|
let compiled;
|
|
67188
67192
|
try {
|
|
@@ -67288,7 +67292,12 @@ ${statePacket}` },
|
|
|
67288
67292
|
log2(`
|
|
67289
67293
|
-- Generating scenario ${i + 1}/${scenarios.length}: "${scenario.name}" --`);
|
|
67290
67294
|
try {
|
|
67291
|
-
await driver.
|
|
67295
|
+
const appDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
67296
|
+
if (appDataCleared) {
|
|
67297
|
+
log2(" app data cleared \u2014 scenario records from a deterministic cold (logged-out) state.");
|
|
67298
|
+
} else {
|
|
67299
|
+
await driver.relaunch();
|
|
67300
|
+
}
|
|
67292
67301
|
const recording = await runScenarioLoop({
|
|
67293
67302
|
scenario,
|
|
67294
67303
|
driver,
|
|
@@ -67305,7 +67314,7 @@ ${statePacket}` },
|
|
|
67305
67314
|
log: log2
|
|
67306
67315
|
});
|
|
67307
67316
|
consecutiveInfraFailures = 0;
|
|
67308
|
-
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
67317
|
+
const test = assembleTest({ scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 });
|
|
67309
67318
|
if (!test) {
|
|
67310
67319
|
log2(` scenario "${scenario.name}" produced no compilable test \u2014 skipped.`);
|
|
67311
67320
|
continue;
|
|
@@ -313776,6 +313785,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
313776
313785
|
};
|
|
313777
313786
|
}
|
|
313778
313787
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
313788
|
+
const exploreAppDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
313789
|
+
log2(exploreAppDataCleared ? "App data cleared \u2014 exploring from a deterministic cold (logged-out) state." : "App data reset unavailable (platform/device policy) \u2014 exploring the app in its current state.");
|
|
313779
313790
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
313780
313791
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
313781
313792
|
const existingScreens = compactExistingScreens(ctx.existingSiteMap);
|
|
@@ -328465,7 +328476,7 @@ var require_package4 = __commonJS({
|
|
|
328465
328476
|
"package.json"(exports, module) {
|
|
328466
328477
|
module.exports = {
|
|
328467
328478
|
name: "@validate.qa/runner",
|
|
328468
|
-
version: "1.0.
|
|
328479
|
+
version: "1.0.20",
|
|
328469
328480
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328470
328481
|
bin: {
|
|
328471
328482
|
"validate-runner": "dist/cli.js"
|
|
@@ -328603,9 +328614,9 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
328603
328614
|
|
|
328604
328615
|
// src/services/appium-executor.ts
|
|
328605
328616
|
init_dist();
|
|
328606
|
-
|
|
328607
|
-
|
|
328608
|
-
|
|
328617
|
+
|
|
328618
|
+
// src/services/mobile/appium-actions.ts
|
|
328619
|
+
init_dist();
|
|
328609
328620
|
|
|
328610
328621
|
// src/services/appium-lifecycle.ts
|
|
328611
328622
|
import { spawn, execFileSync } from "child_process";
|
|
@@ -328890,6 +328901,234 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
328890
328901
|
};
|
|
328891
328902
|
}
|
|
328892
328903
|
|
|
328904
|
+
// src/services/mobile/appium-actions.ts
|
|
328905
|
+
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
328906
|
+
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
328907
|
+
async function appiumRequest(path, options) {
|
|
328908
|
+
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
328909
|
+
const res = await fetch(url, {
|
|
328910
|
+
...options,
|
|
328911
|
+
signal: options?.signal ?? AbortSignal.timeout(APPIUM_REQUEST_TIMEOUT_MS),
|
|
328912
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
328913
|
+
});
|
|
328914
|
+
if (!res.ok) {
|
|
328915
|
+
const body = await res.text().catch(() => "");
|
|
328916
|
+
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
328917
|
+
}
|
|
328918
|
+
return res.json();
|
|
328919
|
+
}
|
|
328920
|
+
function readValue(result) {
|
|
328921
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
328922
|
+
return result.value;
|
|
328923
|
+
}
|
|
328924
|
+
return void 0;
|
|
328925
|
+
}
|
|
328926
|
+
async function getScreenshot(sessionPath) {
|
|
328927
|
+
try {
|
|
328928
|
+
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
328929
|
+
return typeof value === "string" ? value : null;
|
|
328930
|
+
} catch {
|
|
328931
|
+
return null;
|
|
328932
|
+
}
|
|
328933
|
+
}
|
|
328934
|
+
async function getPageSource(sessionPath) {
|
|
328935
|
+
try {
|
|
328936
|
+
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
328937
|
+
return typeof value === "string" ? value : null;
|
|
328938
|
+
} catch {
|
|
328939
|
+
return null;
|
|
328940
|
+
}
|
|
328941
|
+
}
|
|
328942
|
+
async function getWindowSize(sessionPath) {
|
|
328943
|
+
const retries = 3;
|
|
328944
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
328945
|
+
try {
|
|
328946
|
+
const value = readValue(await appiumRequest(`${sessionPath}/window/rect`));
|
|
328947
|
+
if (value && typeof value === "object") {
|
|
328948
|
+
const w = value.width;
|
|
328949
|
+
const h = value.height;
|
|
328950
|
+
if (typeof w === "number" && typeof h === "number" && w > 0 && h > 0) {
|
|
328951
|
+
return { width: w, height: h };
|
|
328952
|
+
}
|
|
328953
|
+
}
|
|
328954
|
+
} catch {
|
|
328955
|
+
}
|
|
328956
|
+
if (attempt < retries - 1) {
|
|
328957
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
328958
|
+
}
|
|
328959
|
+
}
|
|
328960
|
+
return null;
|
|
328961
|
+
}
|
|
328962
|
+
async function getActiveElementId(sessionPath) {
|
|
328963
|
+
try {
|
|
328964
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/active`));
|
|
328965
|
+
if (!value || typeof value !== "object") return null;
|
|
328966
|
+
const v = value;
|
|
328967
|
+
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
328968
|
+
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
328969
|
+
return null;
|
|
328970
|
+
}
|
|
328971
|
+
return elementId;
|
|
328972
|
+
} catch {
|
|
328973
|
+
return null;
|
|
328974
|
+
}
|
|
328975
|
+
}
|
|
328976
|
+
function normaliseBounds(bounds) {
|
|
328977
|
+
if (!bounds || typeof bounds !== "object" || typeof bounds.x !== "number" || typeof bounds.y !== "number" || !Number.isFinite(bounds.x) || !Number.isFinite(bounds.y)) {
|
|
328978
|
+
return null;
|
|
328979
|
+
}
|
|
328980
|
+
const b = bounds;
|
|
328981
|
+
const width = typeof b.width === "number" && Number.isFinite(b.width) ? b.width : 0;
|
|
328982
|
+
const height = typeof b.height === "number" && Number.isFinite(b.height) ? b.height : 0;
|
|
328983
|
+
return { x: b.x, y: b.y, width, height };
|
|
328984
|
+
}
|
|
328985
|
+
async function tapBoundsCenter(sessionPath, bounds) {
|
|
328986
|
+
const box = normaliseBounds(bounds);
|
|
328987
|
+
if (!box) return false;
|
|
328988
|
+
const tapX = Math.round(box.x + box.width / 2);
|
|
328989
|
+
const tapY = Math.round(box.y + box.height / 2);
|
|
328990
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
328991
|
+
method: "POST",
|
|
328992
|
+
body: JSON.stringify({
|
|
328993
|
+
actions: [{
|
|
328994
|
+
type: "pointer",
|
|
328995
|
+
id: "finger1",
|
|
328996
|
+
parameters: { pointerType: "touch" },
|
|
328997
|
+
actions: [
|
|
328998
|
+
// Note: button: 0 is used because Appium's WebDriver protocol
|
|
328999
|
+
// translates mouse button events to touch events. Standard approach
|
|
329000
|
+
// for touch actions in Appium, despite the mouse button semantics.
|
|
329001
|
+
{ type: "pointerMove", duration: 0, x: tapX, y: tapY },
|
|
329002
|
+
{ type: "pointerDown", button: 0 },
|
|
329003
|
+
{ type: "pause", duration: 100 },
|
|
329004
|
+
{ type: "pointerUp", button: 0 }
|
|
329005
|
+
]
|
|
329006
|
+
}]
|
|
329007
|
+
})
|
|
329008
|
+
});
|
|
329009
|
+
return true;
|
|
329010
|
+
}
|
|
329011
|
+
async function tapAt(sessionPath, bounds) {
|
|
329012
|
+
const ok = await tapBoundsCenter(sessionPath, bounds);
|
|
329013
|
+
if (!ok) {
|
|
329014
|
+
throw new Error("TAP requires usable bounds (finite x/y)");
|
|
329015
|
+
}
|
|
329016
|
+
}
|
|
329017
|
+
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
329018
|
+
if (platform3 !== "ANDROID") return false;
|
|
329019
|
+
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
329020
|
+
if (policy === "never") return false;
|
|
329021
|
+
if (policy === "always") return true;
|
|
329022
|
+
return !!deviceId && deviceId.startsWith("emulator-");
|
|
329023
|
+
}
|
|
329024
|
+
async function getElementRect(sessionPath, elementId) {
|
|
329025
|
+
try {
|
|
329026
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/${elementId}/rect`));
|
|
329027
|
+
if (!value || typeof value !== "object") return null;
|
|
329028
|
+
const r = value;
|
|
329029
|
+
if (typeof r.x !== "number" || typeof r.y !== "number" || typeof r.width !== "number" || typeof r.height !== "number" || !Number.isFinite(r.x) || !Number.isFinite(r.y)) {
|
|
329030
|
+
return null;
|
|
329031
|
+
}
|
|
329032
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
329033
|
+
} catch {
|
|
329034
|
+
return null;
|
|
329035
|
+
}
|
|
329036
|
+
}
|
|
329037
|
+
function rectMatchesIntendedBounds(rect, bounds) {
|
|
329038
|
+
if (bounds.width <= 0 || bounds.height <= 0) return false;
|
|
329039
|
+
const rectCenterX = rect.x + rect.width / 2;
|
|
329040
|
+
const rectCenterY = rect.y + rect.height / 2;
|
|
329041
|
+
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
329042
|
+
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
329043
|
+
const rectCenterInBounds = rectCenterX >= bounds.x && rectCenterX <= bounds.x + bounds.width && rectCenterY >= bounds.y && rectCenterY <= bounds.y + bounds.height;
|
|
329044
|
+
const boundsCenterInRect = rect.width > 0 && rect.height > 0 && boundsCenterX >= rect.x && boundsCenterX <= rect.x + rect.width && boundsCenterY >= rect.y && boundsCenterY <= rect.y + rect.height;
|
|
329045
|
+
return rectCenterInBounds || boundsCenterInRect;
|
|
329046
|
+
}
|
|
329047
|
+
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
329048
|
+
const box = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
329049
|
+
let elementId = await getActiveElementId(sessionPath);
|
|
329050
|
+
if (elementId && box && box.width > 0 && box.height > 0) {
|
|
329051
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
329052
|
+
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
329053
|
+
elementId = null;
|
|
329054
|
+
}
|
|
329055
|
+
}
|
|
329056
|
+
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
329057
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
329058
|
+
elementId = await getActiveElementId(sessionPath);
|
|
329059
|
+
}
|
|
329060
|
+
return elementId;
|
|
329061
|
+
}
|
|
329062
|
+
async function typeText(sessionPath, value, bounds) {
|
|
329063
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329064
|
+
if (!elementId) {
|
|
329065
|
+
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
329066
|
+
}
|
|
329067
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
329068
|
+
method: "POST",
|
|
329069
|
+
body: JSON.stringify({ text: value })
|
|
329070
|
+
});
|
|
329071
|
+
}
|
|
329072
|
+
async function clearField(sessionPath, bounds) {
|
|
329073
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329074
|
+
if (!elementId) {
|
|
329075
|
+
throw new Error("No focused element available for CLEAR. Tap an input field first.");
|
|
329076
|
+
}
|
|
329077
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
329078
|
+
method: "POST",
|
|
329079
|
+
body: JSON.stringify({})
|
|
329080
|
+
});
|
|
329081
|
+
}
|
|
329082
|
+
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
329083
|
+
const dist = clampMobileSwipeDistance(distance);
|
|
329084
|
+
const { startX, startY, endX, endY } = computeMobileSwipePointers(
|
|
329085
|
+
windowSize.width,
|
|
329086
|
+
windowSize.height,
|
|
329087
|
+
direction,
|
|
329088
|
+
dist
|
|
329089
|
+
);
|
|
329090
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
329091
|
+
method: "POST",
|
|
329092
|
+
body: JSON.stringify({
|
|
329093
|
+
actions: [{
|
|
329094
|
+
type: "pointer",
|
|
329095
|
+
id: "finger1",
|
|
329096
|
+
parameters: { pointerType: "touch" },
|
|
329097
|
+
actions: [
|
|
329098
|
+
{ type: "pointerMove", duration: 0, x: startX, y: startY },
|
|
329099
|
+
{ type: "pointerDown", button: 0 },
|
|
329100
|
+
{ type: "pointerMove", duration: 300, x: endX, y: endY },
|
|
329101
|
+
{ type: "pointerUp", button: 0 }
|
|
329102
|
+
]
|
|
329103
|
+
}]
|
|
329104
|
+
})
|
|
329105
|
+
});
|
|
329106
|
+
}
|
|
329107
|
+
async function pressBack(sessionPath) {
|
|
329108
|
+
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
329109
|
+
}
|
|
329110
|
+
async function pressHome(sessionPath, platform3) {
|
|
329111
|
+
if (platform3 === "IOS") {
|
|
329112
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
329113
|
+
method: "POST",
|
|
329114
|
+
body: JSON.stringify({
|
|
329115
|
+
script: "mobile: pressButton",
|
|
329116
|
+
args: [{ name: "home" }]
|
|
329117
|
+
})
|
|
329118
|
+
});
|
|
329119
|
+
return;
|
|
329120
|
+
}
|
|
329121
|
+
await appiumRequest(`${sessionPath}/appium/device/press_keycode`, {
|
|
329122
|
+
method: "POST",
|
|
329123
|
+
body: JSON.stringify({ keycode: 3 })
|
|
329124
|
+
});
|
|
329125
|
+
}
|
|
329126
|
+
|
|
329127
|
+
// src/services/appium-executor.ts
|
|
329128
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
329129
|
+
import { execFile as execFile2 } from "child_process";
|
|
329130
|
+
import { promisify as promisify2 } from "util";
|
|
329131
|
+
|
|
328893
329132
|
// src/services/device-discovery.ts
|
|
328894
329133
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
328895
329134
|
import { platform } from "os";
|
|
@@ -330055,7 +330294,7 @@ async function wdRequest(path, options) {
|
|
|
330055
330294
|
}
|
|
330056
330295
|
return res.json();
|
|
330057
330296
|
}
|
|
330058
|
-
async function
|
|
330297
|
+
async function getWindowSize2(sessionId, retries = 3) {
|
|
330059
330298
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
330060
330299
|
try {
|
|
330061
330300
|
const result = await wdRequest(`/session/${sessionId}/window/rect`);
|
|
@@ -330231,6 +330470,26 @@ async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
|
330231
330470
|
log2(` Fresh launch: activate failed (${err instanceof Error ? err.message : String(err)})`);
|
|
330232
330471
|
}
|
|
330233
330472
|
}
|
|
330473
|
+
async function waitForColdBootContent(sessionId, log2, timeoutMs = 2e4) {
|
|
330474
|
+
const deadline = Date.now() + timeoutMs;
|
|
330475
|
+
const MIN_ELEMENTS = 3;
|
|
330476
|
+
let lastCount = 0;
|
|
330477
|
+
while (Date.now() < deadline) {
|
|
330478
|
+
try {
|
|
330479
|
+
const res = await wdRequest(`/session/${sessionId}/source`);
|
|
330480
|
+
const xml = typeof res?.value === "string" ? res.value : "";
|
|
330481
|
+
const count = (xml.match(/<(android\.|XCUIElementType)/g) ?? []).length;
|
|
330482
|
+
lastCount = count;
|
|
330483
|
+
if (count >= MIN_ELEMENTS) {
|
|
330484
|
+
log2(` Cold-boot ready: UI tree has ${count} node(s).`);
|
|
330485
|
+
return;
|
|
330486
|
+
}
|
|
330487
|
+
} catch {
|
|
330488
|
+
}
|
|
330489
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
330490
|
+
}
|
|
330491
|
+
log2(` Cold-boot wait timed out after ${timeoutMs}ms (last tree had ${lastCount} node(s)) \u2014 proceeding anyway.`);
|
|
330492
|
+
}
|
|
330234
330493
|
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
330235
330494
|
let boundary = null;
|
|
330236
330495
|
try {
|
|
@@ -330478,7 +330737,7 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
330478
330737
|
})
|
|
330479
330738
|
});
|
|
330480
330739
|
}
|
|
330481
|
-
async function
|
|
330740
|
+
async function getActiveElementId2(sessionId) {
|
|
330482
330741
|
try {
|
|
330483
330742
|
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
330484
330743
|
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
@@ -330564,7 +330823,7 @@ async function executeType(sessionId, step, context) {
|
|
|
330564
330823
|
await tapCoordinates(sessionId, bounds);
|
|
330565
330824
|
tappedBounds = true;
|
|
330566
330825
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330567
|
-
elementId = await
|
|
330826
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330568
330827
|
if (elementId) {
|
|
330569
330828
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330570
330829
|
if (!lastInputError) return;
|
|
@@ -330586,10 +330845,10 @@ async function executeType(sessionId, step, context) {
|
|
|
330586
330845
|
await tapCoordinates(sessionId, bounds);
|
|
330587
330846
|
tappedBounds = true;
|
|
330588
330847
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330589
|
-
elementId = await
|
|
330848
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330590
330849
|
}
|
|
330591
330850
|
if (!elementId) {
|
|
330592
|
-
elementId = await
|
|
330851
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330593
330852
|
}
|
|
330594
330853
|
if (elementId) {
|
|
330595
330854
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
@@ -330639,10 +330898,10 @@ async function executeClear(sessionId, step, platform3, isCrossPlatform = false)
|
|
|
330639
330898
|
if (!elementId && bounds) {
|
|
330640
330899
|
await tapCoordinates(sessionId, bounds);
|
|
330641
330900
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330642
|
-
elementId = await
|
|
330901
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330643
330902
|
}
|
|
330644
330903
|
if (!elementId) {
|
|
330645
|
-
elementId = await
|
|
330904
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330646
330905
|
}
|
|
330647
330906
|
if (!elementId) {
|
|
330648
330907
|
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
@@ -330658,7 +330917,7 @@ async function executeSwipe(sessionId, step) {
|
|
|
330658
330917
|
let screenW;
|
|
330659
330918
|
let screenH;
|
|
330660
330919
|
try {
|
|
330661
|
-
const windowSize = await
|
|
330920
|
+
const windowSize = await getWindowSize2(sessionId);
|
|
330662
330921
|
screenW = windowSize.width;
|
|
330663
330922
|
screenH = windowSize.height;
|
|
330664
330923
|
} catch (err) {
|
|
@@ -330873,8 +331132,25 @@ async function executeMobileTest(options) {
|
|
|
330873
331132
|
}
|
|
330874
331133
|
}
|
|
330875
331134
|
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
331135
|
+
const launchStepForReset = runSteps.find((s) => s.action === "launchApp");
|
|
331136
|
+
const recordedFromClearedState = launchStepForReset?.metadata?.appReset === "cleared";
|
|
331137
|
+
if (recordedFromClearedState) {
|
|
331138
|
+
if (shouldResetMobileAppData(options.target.platform, selectedDevice.id)) {
|
|
331139
|
+
try {
|
|
331140
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
331141
|
+
log2("App data cleared before launch (test was recorded from a cold logged-out state).");
|
|
331142
|
+
} catch (err) {
|
|
331143
|
+
log2(`App-data clear failed (continuing with plain fresh launch): ${err instanceof Error ? err.message : String(err)}`);
|
|
331144
|
+
}
|
|
331145
|
+
} else {
|
|
331146
|
+
log2("Test expects a cleared app state, but app-data reset is disabled by policy (physical device / VALIDATEQA_MOBILE_APP_RESET=never) \u2014 replaying against the current app state.");
|
|
331147
|
+
}
|
|
331148
|
+
}
|
|
330876
331149
|
log2("Opening a fresh app instance for the test...");
|
|
330877
331150
|
await freshLaunchTargetApp(sessionId, options.target.platform, options.target.appId, log2);
|
|
331151
|
+
if (recordedFromClearedState) {
|
|
331152
|
+
await waitForColdBootContent(sessionId, log2);
|
|
331153
|
+
}
|
|
330878
331154
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
330879
331155
|
try {
|
|
330880
331156
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -331389,225 +331665,6 @@ import { homedir as homedir2 } from "os";
|
|
|
331389
331665
|
import { join as join3, dirname } from "path";
|
|
331390
331666
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
331391
331667
|
init_dist();
|
|
331392
|
-
|
|
331393
|
-
// src/services/mobile/appium-actions.ts
|
|
331394
|
-
init_dist();
|
|
331395
|
-
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
331396
|
-
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
331397
|
-
async function appiumRequest(path, options) {
|
|
331398
|
-
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
331399
|
-
const res = await fetch(url, {
|
|
331400
|
-
...options,
|
|
331401
|
-
signal: options?.signal ?? AbortSignal.timeout(APPIUM_REQUEST_TIMEOUT_MS),
|
|
331402
|
-
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
331403
|
-
});
|
|
331404
|
-
if (!res.ok) {
|
|
331405
|
-
const body = await res.text().catch(() => "");
|
|
331406
|
-
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
331407
|
-
}
|
|
331408
|
-
return res.json();
|
|
331409
|
-
}
|
|
331410
|
-
function readValue(result) {
|
|
331411
|
-
if (result && typeof result === "object" && "value" in result) {
|
|
331412
|
-
return result.value;
|
|
331413
|
-
}
|
|
331414
|
-
return void 0;
|
|
331415
|
-
}
|
|
331416
|
-
async function getScreenshot(sessionPath) {
|
|
331417
|
-
try {
|
|
331418
|
-
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
331419
|
-
return typeof value === "string" ? value : null;
|
|
331420
|
-
} catch {
|
|
331421
|
-
return null;
|
|
331422
|
-
}
|
|
331423
|
-
}
|
|
331424
|
-
async function getPageSource(sessionPath) {
|
|
331425
|
-
try {
|
|
331426
|
-
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
331427
|
-
return typeof value === "string" ? value : null;
|
|
331428
|
-
} catch {
|
|
331429
|
-
return null;
|
|
331430
|
-
}
|
|
331431
|
-
}
|
|
331432
|
-
async function getWindowSize2(sessionPath) {
|
|
331433
|
-
const retries = 3;
|
|
331434
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
331435
|
-
try {
|
|
331436
|
-
const value = readValue(await appiumRequest(`${sessionPath}/window/rect`));
|
|
331437
|
-
if (value && typeof value === "object") {
|
|
331438
|
-
const w = value.width;
|
|
331439
|
-
const h = value.height;
|
|
331440
|
-
if (typeof w === "number" && typeof h === "number" && w > 0 && h > 0) {
|
|
331441
|
-
return { width: w, height: h };
|
|
331442
|
-
}
|
|
331443
|
-
}
|
|
331444
|
-
} catch {
|
|
331445
|
-
}
|
|
331446
|
-
if (attempt < retries - 1) {
|
|
331447
|
-
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
331448
|
-
}
|
|
331449
|
-
}
|
|
331450
|
-
return null;
|
|
331451
|
-
}
|
|
331452
|
-
async function getActiveElementId2(sessionPath) {
|
|
331453
|
-
try {
|
|
331454
|
-
const value = readValue(await appiumRequest(`${sessionPath}/element/active`));
|
|
331455
|
-
if (!value || typeof value !== "object") return null;
|
|
331456
|
-
const v = value;
|
|
331457
|
-
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
331458
|
-
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
331459
|
-
return null;
|
|
331460
|
-
}
|
|
331461
|
-
return elementId;
|
|
331462
|
-
} catch {
|
|
331463
|
-
return null;
|
|
331464
|
-
}
|
|
331465
|
-
}
|
|
331466
|
-
function normaliseBounds(bounds) {
|
|
331467
|
-
if (!bounds || typeof bounds !== "object" || typeof bounds.x !== "number" || typeof bounds.y !== "number" || !Number.isFinite(bounds.x) || !Number.isFinite(bounds.y)) {
|
|
331468
|
-
return null;
|
|
331469
|
-
}
|
|
331470
|
-
const b = bounds;
|
|
331471
|
-
const width = typeof b.width === "number" && Number.isFinite(b.width) ? b.width : 0;
|
|
331472
|
-
const height = typeof b.height === "number" && Number.isFinite(b.height) ? b.height : 0;
|
|
331473
|
-
return { x: b.x, y: b.y, width, height };
|
|
331474
|
-
}
|
|
331475
|
-
async function tapBoundsCenter(sessionPath, bounds) {
|
|
331476
|
-
const box = normaliseBounds(bounds);
|
|
331477
|
-
if (!box) return false;
|
|
331478
|
-
const tapX = Math.round(box.x + box.width / 2);
|
|
331479
|
-
const tapY = Math.round(box.y + box.height / 2);
|
|
331480
|
-
await appiumRequest(`${sessionPath}/actions`, {
|
|
331481
|
-
method: "POST",
|
|
331482
|
-
body: JSON.stringify({
|
|
331483
|
-
actions: [{
|
|
331484
|
-
type: "pointer",
|
|
331485
|
-
id: "finger1",
|
|
331486
|
-
parameters: { pointerType: "touch" },
|
|
331487
|
-
actions: [
|
|
331488
|
-
// Note: button: 0 is used because Appium's WebDriver protocol
|
|
331489
|
-
// translates mouse button events to touch events. Standard approach
|
|
331490
|
-
// for touch actions in Appium, despite the mouse button semantics.
|
|
331491
|
-
{ type: "pointerMove", duration: 0, x: tapX, y: tapY },
|
|
331492
|
-
{ type: "pointerDown", button: 0 },
|
|
331493
|
-
{ type: "pause", duration: 100 },
|
|
331494
|
-
{ type: "pointerUp", button: 0 }
|
|
331495
|
-
]
|
|
331496
|
-
}]
|
|
331497
|
-
})
|
|
331498
|
-
});
|
|
331499
|
-
return true;
|
|
331500
|
-
}
|
|
331501
|
-
async function tapAt(sessionPath, bounds) {
|
|
331502
|
-
const ok = await tapBoundsCenter(sessionPath, bounds);
|
|
331503
|
-
if (!ok) {
|
|
331504
|
-
throw new Error("TAP requires usable bounds (finite x/y)");
|
|
331505
|
-
}
|
|
331506
|
-
}
|
|
331507
|
-
async function getElementRect(sessionPath, elementId) {
|
|
331508
|
-
try {
|
|
331509
|
-
const value = readValue(await appiumRequest(`${sessionPath}/element/${elementId}/rect`));
|
|
331510
|
-
if (!value || typeof value !== "object") return null;
|
|
331511
|
-
const r = value;
|
|
331512
|
-
if (typeof r.x !== "number" || typeof r.y !== "number" || typeof r.width !== "number" || typeof r.height !== "number" || !Number.isFinite(r.x) || !Number.isFinite(r.y)) {
|
|
331513
|
-
return null;
|
|
331514
|
-
}
|
|
331515
|
-
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
331516
|
-
} catch {
|
|
331517
|
-
return null;
|
|
331518
|
-
}
|
|
331519
|
-
}
|
|
331520
|
-
function rectMatchesIntendedBounds(rect, bounds) {
|
|
331521
|
-
if (bounds.width <= 0 || bounds.height <= 0) return false;
|
|
331522
|
-
const rectCenterX = rect.x + rect.width / 2;
|
|
331523
|
-
const rectCenterY = rect.y + rect.height / 2;
|
|
331524
|
-
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
331525
|
-
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
331526
|
-
const rectCenterInBounds = rectCenterX >= bounds.x && rectCenterX <= bounds.x + bounds.width && rectCenterY >= bounds.y && rectCenterY <= bounds.y + bounds.height;
|
|
331527
|
-
const boundsCenterInRect = rect.width > 0 && rect.height > 0 && boundsCenterX >= rect.x && boundsCenterX <= rect.x + rect.width && boundsCenterY >= rect.y && boundsCenterY <= rect.y + rect.height;
|
|
331528
|
-
return rectCenterInBounds || boundsCenterInRect;
|
|
331529
|
-
}
|
|
331530
|
-
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
331531
|
-
const box = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
331532
|
-
let elementId = await getActiveElementId2(sessionPath);
|
|
331533
|
-
if (elementId && box && box.width > 0 && box.height > 0) {
|
|
331534
|
-
const rect = await getElementRect(sessionPath, elementId);
|
|
331535
|
-
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
331536
|
-
elementId = null;
|
|
331537
|
-
}
|
|
331538
|
-
}
|
|
331539
|
-
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
331540
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
331541
|
-
elementId = await getActiveElementId2(sessionPath);
|
|
331542
|
-
}
|
|
331543
|
-
return elementId;
|
|
331544
|
-
}
|
|
331545
|
-
async function typeText(sessionPath, value, bounds) {
|
|
331546
|
-
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
331547
|
-
if (!elementId) {
|
|
331548
|
-
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
331549
|
-
}
|
|
331550
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
331551
|
-
method: "POST",
|
|
331552
|
-
body: JSON.stringify({ text: value })
|
|
331553
|
-
});
|
|
331554
|
-
}
|
|
331555
|
-
async function clearField(sessionPath, bounds) {
|
|
331556
|
-
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
331557
|
-
if (!elementId) {
|
|
331558
|
-
throw new Error("No focused element available for CLEAR. Tap an input field first.");
|
|
331559
|
-
}
|
|
331560
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
331561
|
-
method: "POST",
|
|
331562
|
-
body: JSON.stringify({})
|
|
331563
|
-
});
|
|
331564
|
-
}
|
|
331565
|
-
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
331566
|
-
const dist = clampMobileSwipeDistance(distance);
|
|
331567
|
-
const { startX, startY, endX, endY } = computeMobileSwipePointers(
|
|
331568
|
-
windowSize.width,
|
|
331569
|
-
windowSize.height,
|
|
331570
|
-
direction,
|
|
331571
|
-
dist
|
|
331572
|
-
);
|
|
331573
|
-
await appiumRequest(`${sessionPath}/actions`, {
|
|
331574
|
-
method: "POST",
|
|
331575
|
-
body: JSON.stringify({
|
|
331576
|
-
actions: [{
|
|
331577
|
-
type: "pointer",
|
|
331578
|
-
id: "finger1",
|
|
331579
|
-
parameters: { pointerType: "touch" },
|
|
331580
|
-
actions: [
|
|
331581
|
-
{ type: "pointerMove", duration: 0, x: startX, y: startY },
|
|
331582
|
-
{ type: "pointerDown", button: 0 },
|
|
331583
|
-
{ type: "pointerMove", duration: 300, x: endX, y: endY },
|
|
331584
|
-
{ type: "pointerUp", button: 0 }
|
|
331585
|
-
]
|
|
331586
|
-
}]
|
|
331587
|
-
})
|
|
331588
|
-
});
|
|
331589
|
-
}
|
|
331590
|
-
async function pressBack(sessionPath) {
|
|
331591
|
-
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
331592
|
-
}
|
|
331593
|
-
async function pressHome(sessionPath, platform3) {
|
|
331594
|
-
if (platform3 === "IOS") {
|
|
331595
|
-
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
331596
|
-
method: "POST",
|
|
331597
|
-
body: JSON.stringify({
|
|
331598
|
-
script: "mobile: pressButton",
|
|
331599
|
-
args: [{ name: "home" }]
|
|
331600
|
-
})
|
|
331601
|
-
});
|
|
331602
|
-
return;
|
|
331603
|
-
}
|
|
331604
|
-
await appiumRequest(`${sessionPath}/appium/device/press_keycode`, {
|
|
331605
|
-
method: "POST",
|
|
331606
|
-
body: JSON.stringify({ keycode: 3 })
|
|
331607
|
-
});
|
|
331608
|
-
}
|
|
331609
|
-
|
|
331610
|
-
// src/services/mobile-bridge.ts
|
|
331611
331668
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
331612
331669
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
331613
331670
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
@@ -331701,7 +331758,7 @@ async function getPageSource2() {
|
|
|
331701
331758
|
async function getWindowSize3() {
|
|
331702
331759
|
if (!state.appiumSessionId) return null;
|
|
331703
331760
|
validateSessionId(state.appiumSessionId);
|
|
331704
|
-
return
|
|
331761
|
+
return getWindowSize(`/session/${state.appiumSessionId}`);
|
|
331705
331762
|
}
|
|
331706
331763
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
331707
331764
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
@@ -332765,6 +332822,7 @@ var MobileBridgeDriver = class {
|
|
|
332765
332822
|
sessionId;
|
|
332766
332823
|
platform;
|
|
332767
332824
|
appId;
|
|
332825
|
+
allowAppReset;
|
|
332768
332826
|
constructor(config) {
|
|
332769
332827
|
if (!APPIUM_SESSION_ID_PATTERN2.test(config.sessionId)) {
|
|
332770
332828
|
throw new Error("MobileBridgeDriver: invalid Appium session id");
|
|
@@ -332778,6 +332836,7 @@ var MobileBridgeDriver = class {
|
|
|
332778
332836
|
this.sessionId = config.sessionId;
|
|
332779
332837
|
this.platform = config.platform;
|
|
332780
332838
|
this.appId = config.appId;
|
|
332839
|
+
this.allowAppReset = config.allowAppReset === true;
|
|
332781
332840
|
}
|
|
332782
332841
|
/** WebDriver session path (`/session/<id>`), the prefix every primitive needs. */
|
|
332783
332842
|
get sessionPath() {
|
|
@@ -332839,7 +332898,7 @@ var MobileBridgeDriver = class {
|
|
|
332839
332898
|
const [xml, screenshot, windowSize] = await Promise.all([
|
|
332840
332899
|
getPageSource(this.sessionPath),
|
|
332841
332900
|
getScreenshot(this.sessionPath),
|
|
332842
|
-
|
|
332901
|
+
getWindowSize(this.sessionPath)
|
|
332843
332902
|
]);
|
|
332844
332903
|
const source = xml ?? "";
|
|
332845
332904
|
const screenSignal = await this.enrichScreenSignal(source);
|
|
@@ -332852,7 +332911,7 @@ var MobileBridgeDriver = class {
|
|
|
332852
332911
|
}
|
|
332853
332912
|
/** Device window size; null when unreadable (swipe geometry degrades gracefully). */
|
|
332854
332913
|
async windowSize() {
|
|
332855
|
-
return
|
|
332914
|
+
return getWindowSize(this.sessionPath);
|
|
332856
332915
|
}
|
|
332857
332916
|
// ── Action result wrapper ──────────────────────────────
|
|
332858
332917
|
/**
|
|
@@ -332894,7 +332953,7 @@ var MobileBridgeDriver = class {
|
|
|
332894
332953
|
* hard error so we never fire an off-screen gesture.
|
|
332895
332954
|
*/
|
|
332896
332955
|
async swipe(direction, distance) {
|
|
332897
|
-
const size = await
|
|
332956
|
+
const size = await getWindowSize(this.sessionPath);
|
|
332898
332957
|
if (!size) {
|
|
332899
332958
|
return { ok: false, error: "Could not determine device screen dimensions for swipe." };
|
|
332900
332959
|
}
|
|
@@ -332919,6 +332978,22 @@ var MobileBridgeDriver = class {
|
|
|
332919
332978
|
await this.executeScript("mobile: terminateApp", [args]);
|
|
332920
332979
|
await this.executeScript("mobile: activateApp", [args]);
|
|
332921
332980
|
}
|
|
332981
|
+
/**
|
|
332982
|
+
* Deterministic state reset: clear app data + relaunch (Android emulators
|
|
332983
|
+
* only, per the runner-computed allowAppReset policy). Returns false when
|
|
332984
|
+
* unsupported/disallowed or when clearApp throws — callers fall back to
|
|
332985
|
+
* relaunch(), preserving today's behavior.
|
|
332986
|
+
*/
|
|
332987
|
+
async resetAppState() {
|
|
332988
|
+
if (!this.allowAppReset || this.platform !== "ANDROID") return false;
|
|
332989
|
+
try {
|
|
332990
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
332991
|
+
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
332992
|
+
return true;
|
|
332993
|
+
} catch {
|
|
332994
|
+
return false;
|
|
332995
|
+
}
|
|
332996
|
+
}
|
|
332922
332997
|
/** (Re)launch the target app (activateApp) from a backgrounded/closed state. */
|
|
332923
332998
|
async launchApp() {
|
|
332924
332999
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
@@ -335060,7 +335135,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
335060
335135
|
const driver = new MobileBridgeDriver({
|
|
335061
335136
|
sessionId: session.sessionId,
|
|
335062
335137
|
platform: platform3,
|
|
335063
|
-
appId: session.appId
|
|
335138
|
+
appId: session.appId,
|
|
335139
|
+
// Deterministic-state authorization: Android emulators only by
|
|
335140
|
+
// default (VALIDATEQA_MOBILE_APP_RESET overrides) — a personal
|
|
335141
|
+
// physical device must never have its app data wiped.
|
|
335142
|
+
allowAppReset: shouldResetMobileAppData(platform3, session.deviceId)
|
|
335064
335143
|
});
|
|
335065
335144
|
discoveryResult = await (0, import_runner_core4.runMobileDiscoveryOnRunner)(ctx, driver, {
|
|
335066
335145
|
onLog: mobileOnLog,
|