@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.js
CHANGED
|
@@ -1527,7 +1527,7 @@ function buildMobileStep(action, order) {
|
|
|
1527
1527
|
}
|
|
1528
1528
|
return step;
|
|
1529
1529
|
}
|
|
1530
|
-
function buildLaunchStep(target, medium) {
|
|
1530
|
+
function buildLaunchStep(target, medium, appResetOnLaunch = false) {
|
|
1531
1531
|
const screenName = target.deeplink ? "DeepLinkEntry" : `${target.platform === "IOS" ? "IOS" : "Android"}Launch`;
|
|
1532
1532
|
const description = target.launchMode === "DEEP_LINK" ? `Launch ${target.appId} via deep link` : `Launch ${target.appId} on ${medium === "IOS_NATIVE" ? "iOS" : "Android"}`;
|
|
1533
1533
|
return {
|
|
@@ -1544,7 +1544,10 @@ function buildLaunchStep(target, medium) {
|
|
|
1544
1544
|
platform: target.platform,
|
|
1545
1545
|
deeplink: target.deeplink,
|
|
1546
1546
|
deviceId: target.recordedDeviceId,
|
|
1547
|
-
platformVersion: target.platformVersion
|
|
1547
|
+
platformVersion: target.platformVersion,
|
|
1548
|
+
// Recorded from a cleared app-data state: replay must clear before launch
|
|
1549
|
+
// (policy-gated in the executor) to recreate the recording's entry state.
|
|
1550
|
+
...appResetOnLaunch ? { appReset: "cleared" } : {}
|
|
1548
1551
|
}
|
|
1549
1552
|
};
|
|
1550
1553
|
}
|
|
@@ -1795,7 +1798,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1795
1798
|
function generateMobileTestFromSession(input) {
|
|
1796
1799
|
const steps = [];
|
|
1797
1800
|
if (input.target) {
|
|
1798
|
-
steps.push(buildLaunchStep(input.target, input.medium));
|
|
1801
|
+
steps.push(buildLaunchStep(input.target, input.medium, input.appResetOnLaunch === true));
|
|
1799
1802
|
}
|
|
1800
1803
|
for (const action of input.actions) {
|
|
1801
1804
|
const step = buildMobileStep(action, steps.length);
|
|
@@ -67160,7 +67163,7 @@ ${statePacket}` },
|
|
|
67160
67163
|
return out;
|
|
67161
67164
|
}
|
|
67162
67165
|
function assembleTest(args) {
|
|
67163
|
-
const { scenario, recording, ctx, platform: platform3, log: log2 } = args;
|
|
67166
|
+
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
67164
67167
|
const actions = collapseRetryCycles(recording.actions);
|
|
67165
67168
|
if (actions.length < recording.actions.length) {
|
|
67166
67169
|
log2(` collapsed ${recording.actions.length - actions.length} retry-cycle step(s) out of the recorded trace (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
@@ -67177,7 +67180,8 @@ ${statePacket}` },
|
|
|
67177
67180
|
sessionName: recording.finish?.name ?? scenario.name,
|
|
67178
67181
|
medium,
|
|
67179
67182
|
target,
|
|
67180
|
-
actions
|
|
67183
|
+
actions,
|
|
67184
|
+
appResetOnLaunch: appDataCleared
|
|
67181
67185
|
};
|
|
67182
67186
|
let compiled;
|
|
67183
67187
|
try {
|
|
@@ -67283,7 +67287,12 @@ ${statePacket}` },
|
|
|
67283
67287
|
log2(`
|
|
67284
67288
|
-- Generating scenario ${i + 1}/${scenarios.length}: "${scenario.name}" --`);
|
|
67285
67289
|
try {
|
|
67286
|
-
await driver.
|
|
67290
|
+
const appDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
67291
|
+
if (appDataCleared) {
|
|
67292
|
+
log2(" app data cleared \u2014 scenario records from a deterministic cold (logged-out) state.");
|
|
67293
|
+
} else {
|
|
67294
|
+
await driver.relaunch();
|
|
67295
|
+
}
|
|
67287
67296
|
const recording = await runScenarioLoop({
|
|
67288
67297
|
scenario,
|
|
67289
67298
|
driver,
|
|
@@ -67300,7 +67309,7 @@ ${statePacket}` },
|
|
|
67300
67309
|
log: log2
|
|
67301
67310
|
});
|
|
67302
67311
|
consecutiveInfraFailures = 0;
|
|
67303
|
-
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
67312
|
+
const test = assembleTest({ scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 });
|
|
67304
67313
|
if (!test) {
|
|
67305
67314
|
log2(` scenario "${scenario.name}" produced no compilable test \u2014 skipped.`);
|
|
67306
67315
|
continue;
|
|
@@ -313771,6 +313780,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
313771
313780
|
};
|
|
313772
313781
|
}
|
|
313773
313782
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
313783
|
+
const exploreAppDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
313784
|
+
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.");
|
|
313774
313785
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
313775
313786
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
313776
313787
|
const existingScreens = compactExistingScreens(ctx.existingSiteMap);
|
|
@@ -328460,7 +328471,7 @@ var require_package4 = __commonJS({
|
|
|
328460
328471
|
"package.json"(exports2, module2) {
|
|
328461
328472
|
module2.exports = {
|
|
328462
328473
|
name: "@validate.qa/runner",
|
|
328463
|
-
version: "1.0.
|
|
328474
|
+
version: "1.0.20",
|
|
328464
328475
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328465
328476
|
bin: {
|
|
328466
328477
|
"validate-runner": "dist/cli.js"
|
|
@@ -328598,9 +328609,9 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
328598
328609
|
|
|
328599
328610
|
// src/services/appium-executor.ts
|
|
328600
328611
|
init_dist();
|
|
328601
|
-
|
|
328602
|
-
|
|
328603
|
-
|
|
328612
|
+
|
|
328613
|
+
// src/services/mobile/appium-actions.ts
|
|
328614
|
+
init_dist();
|
|
328604
328615
|
|
|
328605
328616
|
// src/services/appium-lifecycle.ts
|
|
328606
328617
|
var import_child_process = require("child_process");
|
|
@@ -328885,6 +328896,234 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
328885
328896
|
};
|
|
328886
328897
|
}
|
|
328887
328898
|
|
|
328899
|
+
// src/services/mobile/appium-actions.ts
|
|
328900
|
+
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
328901
|
+
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
328902
|
+
async function appiumRequest(path, options) {
|
|
328903
|
+
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
328904
|
+
const res = await fetch(url, {
|
|
328905
|
+
...options,
|
|
328906
|
+
signal: options?.signal ?? AbortSignal.timeout(APPIUM_REQUEST_TIMEOUT_MS),
|
|
328907
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
328908
|
+
});
|
|
328909
|
+
if (!res.ok) {
|
|
328910
|
+
const body = await res.text().catch(() => "");
|
|
328911
|
+
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
328912
|
+
}
|
|
328913
|
+
return res.json();
|
|
328914
|
+
}
|
|
328915
|
+
function readValue(result) {
|
|
328916
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
328917
|
+
return result.value;
|
|
328918
|
+
}
|
|
328919
|
+
return void 0;
|
|
328920
|
+
}
|
|
328921
|
+
async function getScreenshot(sessionPath) {
|
|
328922
|
+
try {
|
|
328923
|
+
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
328924
|
+
return typeof value === "string" ? value : null;
|
|
328925
|
+
} catch {
|
|
328926
|
+
return null;
|
|
328927
|
+
}
|
|
328928
|
+
}
|
|
328929
|
+
async function getPageSource(sessionPath) {
|
|
328930
|
+
try {
|
|
328931
|
+
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
328932
|
+
return typeof value === "string" ? value : null;
|
|
328933
|
+
} catch {
|
|
328934
|
+
return null;
|
|
328935
|
+
}
|
|
328936
|
+
}
|
|
328937
|
+
async function getWindowSize(sessionPath) {
|
|
328938
|
+
const retries = 3;
|
|
328939
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
328940
|
+
try {
|
|
328941
|
+
const value = readValue(await appiumRequest(`${sessionPath}/window/rect`));
|
|
328942
|
+
if (value && typeof value === "object") {
|
|
328943
|
+
const w = value.width;
|
|
328944
|
+
const h = value.height;
|
|
328945
|
+
if (typeof w === "number" && typeof h === "number" && w > 0 && h > 0) {
|
|
328946
|
+
return { width: w, height: h };
|
|
328947
|
+
}
|
|
328948
|
+
}
|
|
328949
|
+
} catch {
|
|
328950
|
+
}
|
|
328951
|
+
if (attempt < retries - 1) {
|
|
328952
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
328953
|
+
}
|
|
328954
|
+
}
|
|
328955
|
+
return null;
|
|
328956
|
+
}
|
|
328957
|
+
async function getActiveElementId(sessionPath) {
|
|
328958
|
+
try {
|
|
328959
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/active`));
|
|
328960
|
+
if (!value || typeof value !== "object") return null;
|
|
328961
|
+
const v = value;
|
|
328962
|
+
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
328963
|
+
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
328964
|
+
return null;
|
|
328965
|
+
}
|
|
328966
|
+
return elementId;
|
|
328967
|
+
} catch {
|
|
328968
|
+
return null;
|
|
328969
|
+
}
|
|
328970
|
+
}
|
|
328971
|
+
function normaliseBounds(bounds) {
|
|
328972
|
+
if (!bounds || typeof bounds !== "object" || typeof bounds.x !== "number" || typeof bounds.y !== "number" || !Number.isFinite(bounds.x) || !Number.isFinite(bounds.y)) {
|
|
328973
|
+
return null;
|
|
328974
|
+
}
|
|
328975
|
+
const b = bounds;
|
|
328976
|
+
const width = typeof b.width === "number" && Number.isFinite(b.width) ? b.width : 0;
|
|
328977
|
+
const height = typeof b.height === "number" && Number.isFinite(b.height) ? b.height : 0;
|
|
328978
|
+
return { x: b.x, y: b.y, width, height };
|
|
328979
|
+
}
|
|
328980
|
+
async function tapBoundsCenter(sessionPath, bounds) {
|
|
328981
|
+
const box = normaliseBounds(bounds);
|
|
328982
|
+
if (!box) return false;
|
|
328983
|
+
const tapX = Math.round(box.x + box.width / 2);
|
|
328984
|
+
const tapY = Math.round(box.y + box.height / 2);
|
|
328985
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
328986
|
+
method: "POST",
|
|
328987
|
+
body: JSON.stringify({
|
|
328988
|
+
actions: [{
|
|
328989
|
+
type: "pointer",
|
|
328990
|
+
id: "finger1",
|
|
328991
|
+
parameters: { pointerType: "touch" },
|
|
328992
|
+
actions: [
|
|
328993
|
+
// Note: button: 0 is used because Appium's WebDriver protocol
|
|
328994
|
+
// translates mouse button events to touch events. Standard approach
|
|
328995
|
+
// for touch actions in Appium, despite the mouse button semantics.
|
|
328996
|
+
{ type: "pointerMove", duration: 0, x: tapX, y: tapY },
|
|
328997
|
+
{ type: "pointerDown", button: 0 },
|
|
328998
|
+
{ type: "pause", duration: 100 },
|
|
328999
|
+
{ type: "pointerUp", button: 0 }
|
|
329000
|
+
]
|
|
329001
|
+
}]
|
|
329002
|
+
})
|
|
329003
|
+
});
|
|
329004
|
+
return true;
|
|
329005
|
+
}
|
|
329006
|
+
async function tapAt(sessionPath, bounds) {
|
|
329007
|
+
const ok = await tapBoundsCenter(sessionPath, bounds);
|
|
329008
|
+
if (!ok) {
|
|
329009
|
+
throw new Error("TAP requires usable bounds (finite x/y)");
|
|
329010
|
+
}
|
|
329011
|
+
}
|
|
329012
|
+
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
329013
|
+
if (platform3 !== "ANDROID") return false;
|
|
329014
|
+
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
329015
|
+
if (policy === "never") return false;
|
|
329016
|
+
if (policy === "always") return true;
|
|
329017
|
+
return !!deviceId && deviceId.startsWith("emulator-");
|
|
329018
|
+
}
|
|
329019
|
+
async function getElementRect(sessionPath, elementId) {
|
|
329020
|
+
try {
|
|
329021
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/${elementId}/rect`));
|
|
329022
|
+
if (!value || typeof value !== "object") return null;
|
|
329023
|
+
const r = value;
|
|
329024
|
+
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)) {
|
|
329025
|
+
return null;
|
|
329026
|
+
}
|
|
329027
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
329028
|
+
} catch {
|
|
329029
|
+
return null;
|
|
329030
|
+
}
|
|
329031
|
+
}
|
|
329032
|
+
function rectMatchesIntendedBounds(rect, bounds) {
|
|
329033
|
+
if (bounds.width <= 0 || bounds.height <= 0) return false;
|
|
329034
|
+
const rectCenterX = rect.x + rect.width / 2;
|
|
329035
|
+
const rectCenterY = rect.y + rect.height / 2;
|
|
329036
|
+
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
329037
|
+
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
329038
|
+
const rectCenterInBounds = rectCenterX >= bounds.x && rectCenterX <= bounds.x + bounds.width && rectCenterY >= bounds.y && rectCenterY <= bounds.y + bounds.height;
|
|
329039
|
+
const boundsCenterInRect = rect.width > 0 && rect.height > 0 && boundsCenterX >= rect.x && boundsCenterX <= rect.x + rect.width && boundsCenterY >= rect.y && boundsCenterY <= rect.y + rect.height;
|
|
329040
|
+
return rectCenterInBounds || boundsCenterInRect;
|
|
329041
|
+
}
|
|
329042
|
+
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
329043
|
+
const box = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
329044
|
+
let elementId = await getActiveElementId(sessionPath);
|
|
329045
|
+
if (elementId && box && box.width > 0 && box.height > 0) {
|
|
329046
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
329047
|
+
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
329048
|
+
elementId = null;
|
|
329049
|
+
}
|
|
329050
|
+
}
|
|
329051
|
+
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
329052
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
329053
|
+
elementId = await getActiveElementId(sessionPath);
|
|
329054
|
+
}
|
|
329055
|
+
return elementId;
|
|
329056
|
+
}
|
|
329057
|
+
async function typeText(sessionPath, value, bounds) {
|
|
329058
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329059
|
+
if (!elementId) {
|
|
329060
|
+
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
329061
|
+
}
|
|
329062
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
329063
|
+
method: "POST",
|
|
329064
|
+
body: JSON.stringify({ text: value })
|
|
329065
|
+
});
|
|
329066
|
+
}
|
|
329067
|
+
async function clearField(sessionPath, bounds) {
|
|
329068
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329069
|
+
if (!elementId) {
|
|
329070
|
+
throw new Error("No focused element available for CLEAR. Tap an input field first.");
|
|
329071
|
+
}
|
|
329072
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
329073
|
+
method: "POST",
|
|
329074
|
+
body: JSON.stringify({})
|
|
329075
|
+
});
|
|
329076
|
+
}
|
|
329077
|
+
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
329078
|
+
const dist = clampMobileSwipeDistance(distance);
|
|
329079
|
+
const { startX, startY, endX, endY } = computeMobileSwipePointers(
|
|
329080
|
+
windowSize.width,
|
|
329081
|
+
windowSize.height,
|
|
329082
|
+
direction,
|
|
329083
|
+
dist
|
|
329084
|
+
);
|
|
329085
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
329086
|
+
method: "POST",
|
|
329087
|
+
body: JSON.stringify({
|
|
329088
|
+
actions: [{
|
|
329089
|
+
type: "pointer",
|
|
329090
|
+
id: "finger1",
|
|
329091
|
+
parameters: { pointerType: "touch" },
|
|
329092
|
+
actions: [
|
|
329093
|
+
{ type: "pointerMove", duration: 0, x: startX, y: startY },
|
|
329094
|
+
{ type: "pointerDown", button: 0 },
|
|
329095
|
+
{ type: "pointerMove", duration: 300, x: endX, y: endY },
|
|
329096
|
+
{ type: "pointerUp", button: 0 }
|
|
329097
|
+
]
|
|
329098
|
+
}]
|
|
329099
|
+
})
|
|
329100
|
+
});
|
|
329101
|
+
}
|
|
329102
|
+
async function pressBack(sessionPath) {
|
|
329103
|
+
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
329104
|
+
}
|
|
329105
|
+
async function pressHome(sessionPath, platform3) {
|
|
329106
|
+
if (platform3 === "IOS") {
|
|
329107
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
329108
|
+
method: "POST",
|
|
329109
|
+
body: JSON.stringify({
|
|
329110
|
+
script: "mobile: pressButton",
|
|
329111
|
+
args: [{ name: "home" }]
|
|
329112
|
+
})
|
|
329113
|
+
});
|
|
329114
|
+
return;
|
|
329115
|
+
}
|
|
329116
|
+
await appiumRequest(`${sessionPath}/appium/device/press_keycode`, {
|
|
329117
|
+
method: "POST",
|
|
329118
|
+
body: JSON.stringify({ keycode: 3 })
|
|
329119
|
+
});
|
|
329120
|
+
}
|
|
329121
|
+
|
|
329122
|
+
// src/services/appium-executor.ts
|
|
329123
|
+
var import_node_child_process2 = require("child_process");
|
|
329124
|
+
var import_node_util2 = require("util");
|
|
329125
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
329126
|
+
|
|
328888
329127
|
// src/services/device-discovery.ts
|
|
328889
329128
|
var import_child_process2 = require("child_process");
|
|
328890
329129
|
var import_os2 = require("os");
|
|
@@ -330047,7 +330286,7 @@ async function wdRequest(path, options) {
|
|
|
330047
330286
|
}
|
|
330048
330287
|
return res.json();
|
|
330049
330288
|
}
|
|
330050
|
-
async function
|
|
330289
|
+
async function getWindowSize2(sessionId, retries = 3) {
|
|
330051
330290
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
330052
330291
|
try {
|
|
330053
330292
|
const result = await wdRequest(`/session/${sessionId}/window/rect`);
|
|
@@ -330223,6 +330462,26 @@ async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
|
330223
330462
|
log2(` Fresh launch: activate failed (${err instanceof Error ? err.message : String(err)})`);
|
|
330224
330463
|
}
|
|
330225
330464
|
}
|
|
330465
|
+
async function waitForColdBootContent(sessionId, log2, timeoutMs = 2e4) {
|
|
330466
|
+
const deadline = Date.now() + timeoutMs;
|
|
330467
|
+
const MIN_ELEMENTS = 3;
|
|
330468
|
+
let lastCount = 0;
|
|
330469
|
+
while (Date.now() < deadline) {
|
|
330470
|
+
try {
|
|
330471
|
+
const res = await wdRequest(`/session/${sessionId}/source`);
|
|
330472
|
+
const xml = typeof res?.value === "string" ? res.value : "";
|
|
330473
|
+
const count = (xml.match(/<(android\.|XCUIElementType)/g) ?? []).length;
|
|
330474
|
+
lastCount = count;
|
|
330475
|
+
if (count >= MIN_ELEMENTS) {
|
|
330476
|
+
log2(` Cold-boot ready: UI tree has ${count} node(s).`);
|
|
330477
|
+
return;
|
|
330478
|
+
}
|
|
330479
|
+
} catch {
|
|
330480
|
+
}
|
|
330481
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
330482
|
+
}
|
|
330483
|
+
log2(` Cold-boot wait timed out after ${timeoutMs}ms (last tree had ${lastCount} node(s)) \u2014 proceeding anyway.`);
|
|
330484
|
+
}
|
|
330226
330485
|
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
330227
330486
|
let boundary = null;
|
|
330228
330487
|
try {
|
|
@@ -330470,7 +330729,7 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
330470
330729
|
})
|
|
330471
330730
|
});
|
|
330472
330731
|
}
|
|
330473
|
-
async function
|
|
330732
|
+
async function getActiveElementId2(sessionId) {
|
|
330474
330733
|
try {
|
|
330475
330734
|
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
330476
330735
|
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
@@ -330556,7 +330815,7 @@ async function executeType(sessionId, step, context) {
|
|
|
330556
330815
|
await tapCoordinates(sessionId, bounds);
|
|
330557
330816
|
tappedBounds = true;
|
|
330558
330817
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330559
|
-
elementId = await
|
|
330818
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330560
330819
|
if (elementId) {
|
|
330561
330820
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330562
330821
|
if (!lastInputError) return;
|
|
@@ -330578,10 +330837,10 @@ async function executeType(sessionId, step, context) {
|
|
|
330578
330837
|
await tapCoordinates(sessionId, bounds);
|
|
330579
330838
|
tappedBounds = true;
|
|
330580
330839
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330581
|
-
elementId = await
|
|
330840
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330582
330841
|
}
|
|
330583
330842
|
if (!elementId) {
|
|
330584
|
-
elementId = await
|
|
330843
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330585
330844
|
}
|
|
330586
330845
|
if (elementId) {
|
|
330587
330846
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
@@ -330631,10 +330890,10 @@ async function executeClear(sessionId, step, platform3, isCrossPlatform = false)
|
|
|
330631
330890
|
if (!elementId && bounds) {
|
|
330632
330891
|
await tapCoordinates(sessionId, bounds);
|
|
330633
330892
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330634
|
-
elementId = await
|
|
330893
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330635
330894
|
}
|
|
330636
330895
|
if (!elementId) {
|
|
330637
|
-
elementId = await
|
|
330896
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330638
330897
|
}
|
|
330639
330898
|
if (!elementId) {
|
|
330640
330899
|
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
@@ -330650,7 +330909,7 @@ async function executeSwipe(sessionId, step) {
|
|
|
330650
330909
|
let screenW;
|
|
330651
330910
|
let screenH;
|
|
330652
330911
|
try {
|
|
330653
|
-
const windowSize = await
|
|
330912
|
+
const windowSize = await getWindowSize2(sessionId);
|
|
330654
330913
|
screenW = windowSize.width;
|
|
330655
330914
|
screenH = windowSize.height;
|
|
330656
330915
|
} catch (err) {
|
|
@@ -330865,8 +331124,25 @@ async function executeMobileTest(options) {
|
|
|
330865
331124
|
}
|
|
330866
331125
|
}
|
|
330867
331126
|
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
331127
|
+
const launchStepForReset = runSteps.find((s) => s.action === "launchApp");
|
|
331128
|
+
const recordedFromClearedState = launchStepForReset?.metadata?.appReset === "cleared";
|
|
331129
|
+
if (recordedFromClearedState) {
|
|
331130
|
+
if (shouldResetMobileAppData(options.target.platform, selectedDevice.id)) {
|
|
331131
|
+
try {
|
|
331132
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
331133
|
+
log2("App data cleared before launch (test was recorded from a cold logged-out state).");
|
|
331134
|
+
} catch (err) {
|
|
331135
|
+
log2(`App-data clear failed (continuing with plain fresh launch): ${err instanceof Error ? err.message : String(err)}`);
|
|
331136
|
+
}
|
|
331137
|
+
} else {
|
|
331138
|
+
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.");
|
|
331139
|
+
}
|
|
331140
|
+
}
|
|
330868
331141
|
log2("Opening a fresh app instance for the test...");
|
|
330869
331142
|
await freshLaunchTargetApp(sessionId, options.target.platform, options.target.appId, log2);
|
|
331143
|
+
if (recordedFromClearedState) {
|
|
331144
|
+
await waitForColdBootContent(sessionId, log2);
|
|
331145
|
+
}
|
|
330870
331146
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
330871
331147
|
try {
|
|
330872
331148
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -331381,225 +331657,6 @@ var import_os3 = require("os");
|
|
|
331381
331657
|
var import_path2 = require("path");
|
|
331382
331658
|
var import_fs2 = require("fs");
|
|
331383
331659
|
init_dist();
|
|
331384
|
-
|
|
331385
|
-
// src/services/mobile/appium-actions.ts
|
|
331386
|
-
init_dist();
|
|
331387
|
-
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
331388
|
-
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
331389
|
-
async function appiumRequest(path, options) {
|
|
331390
|
-
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
331391
|
-
const res = await fetch(url, {
|
|
331392
|
-
...options,
|
|
331393
|
-
signal: options?.signal ?? AbortSignal.timeout(APPIUM_REQUEST_TIMEOUT_MS),
|
|
331394
|
-
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
331395
|
-
});
|
|
331396
|
-
if (!res.ok) {
|
|
331397
|
-
const body = await res.text().catch(() => "");
|
|
331398
|
-
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
331399
|
-
}
|
|
331400
|
-
return res.json();
|
|
331401
|
-
}
|
|
331402
|
-
function readValue(result) {
|
|
331403
|
-
if (result && typeof result === "object" && "value" in result) {
|
|
331404
|
-
return result.value;
|
|
331405
|
-
}
|
|
331406
|
-
return void 0;
|
|
331407
|
-
}
|
|
331408
|
-
async function getScreenshot(sessionPath) {
|
|
331409
|
-
try {
|
|
331410
|
-
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
331411
|
-
return typeof value === "string" ? value : null;
|
|
331412
|
-
} catch {
|
|
331413
|
-
return null;
|
|
331414
|
-
}
|
|
331415
|
-
}
|
|
331416
|
-
async function getPageSource(sessionPath) {
|
|
331417
|
-
try {
|
|
331418
|
-
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
331419
|
-
return typeof value === "string" ? value : null;
|
|
331420
|
-
} catch {
|
|
331421
|
-
return null;
|
|
331422
|
-
}
|
|
331423
|
-
}
|
|
331424
|
-
async function getWindowSize2(sessionPath) {
|
|
331425
|
-
const retries = 3;
|
|
331426
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
331427
|
-
try {
|
|
331428
|
-
const value = readValue(await appiumRequest(`${sessionPath}/window/rect`));
|
|
331429
|
-
if (value && typeof value === "object") {
|
|
331430
|
-
const w = value.width;
|
|
331431
|
-
const h = value.height;
|
|
331432
|
-
if (typeof w === "number" && typeof h === "number" && w > 0 && h > 0) {
|
|
331433
|
-
return { width: w, height: h };
|
|
331434
|
-
}
|
|
331435
|
-
}
|
|
331436
|
-
} catch {
|
|
331437
|
-
}
|
|
331438
|
-
if (attempt < retries - 1) {
|
|
331439
|
-
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
331440
|
-
}
|
|
331441
|
-
}
|
|
331442
|
-
return null;
|
|
331443
|
-
}
|
|
331444
|
-
async function getActiveElementId2(sessionPath) {
|
|
331445
|
-
try {
|
|
331446
|
-
const value = readValue(await appiumRequest(`${sessionPath}/element/active`));
|
|
331447
|
-
if (!value || typeof value !== "object") return null;
|
|
331448
|
-
const v = value;
|
|
331449
|
-
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
331450
|
-
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
331451
|
-
return null;
|
|
331452
|
-
}
|
|
331453
|
-
return elementId;
|
|
331454
|
-
} catch {
|
|
331455
|
-
return null;
|
|
331456
|
-
}
|
|
331457
|
-
}
|
|
331458
|
-
function normaliseBounds(bounds) {
|
|
331459
|
-
if (!bounds || typeof bounds !== "object" || typeof bounds.x !== "number" || typeof bounds.y !== "number" || !Number.isFinite(bounds.x) || !Number.isFinite(bounds.y)) {
|
|
331460
|
-
return null;
|
|
331461
|
-
}
|
|
331462
|
-
const b = bounds;
|
|
331463
|
-
const width = typeof b.width === "number" && Number.isFinite(b.width) ? b.width : 0;
|
|
331464
|
-
const height = typeof b.height === "number" && Number.isFinite(b.height) ? b.height : 0;
|
|
331465
|
-
return { x: b.x, y: b.y, width, height };
|
|
331466
|
-
}
|
|
331467
|
-
async function tapBoundsCenter(sessionPath, bounds) {
|
|
331468
|
-
const box = normaliseBounds(bounds);
|
|
331469
|
-
if (!box) return false;
|
|
331470
|
-
const tapX = Math.round(box.x + box.width / 2);
|
|
331471
|
-
const tapY = Math.round(box.y + box.height / 2);
|
|
331472
|
-
await appiumRequest(`${sessionPath}/actions`, {
|
|
331473
|
-
method: "POST",
|
|
331474
|
-
body: JSON.stringify({
|
|
331475
|
-
actions: [{
|
|
331476
|
-
type: "pointer",
|
|
331477
|
-
id: "finger1",
|
|
331478
|
-
parameters: { pointerType: "touch" },
|
|
331479
|
-
actions: [
|
|
331480
|
-
// Note: button: 0 is used because Appium's WebDriver protocol
|
|
331481
|
-
// translates mouse button events to touch events. Standard approach
|
|
331482
|
-
// for touch actions in Appium, despite the mouse button semantics.
|
|
331483
|
-
{ type: "pointerMove", duration: 0, x: tapX, y: tapY },
|
|
331484
|
-
{ type: "pointerDown", button: 0 },
|
|
331485
|
-
{ type: "pause", duration: 100 },
|
|
331486
|
-
{ type: "pointerUp", button: 0 }
|
|
331487
|
-
]
|
|
331488
|
-
}]
|
|
331489
|
-
})
|
|
331490
|
-
});
|
|
331491
|
-
return true;
|
|
331492
|
-
}
|
|
331493
|
-
async function tapAt(sessionPath, bounds) {
|
|
331494
|
-
const ok = await tapBoundsCenter(sessionPath, bounds);
|
|
331495
|
-
if (!ok) {
|
|
331496
|
-
throw new Error("TAP requires usable bounds (finite x/y)");
|
|
331497
|
-
}
|
|
331498
|
-
}
|
|
331499
|
-
async function getElementRect(sessionPath, elementId) {
|
|
331500
|
-
try {
|
|
331501
|
-
const value = readValue(await appiumRequest(`${sessionPath}/element/${elementId}/rect`));
|
|
331502
|
-
if (!value || typeof value !== "object") return null;
|
|
331503
|
-
const r = value;
|
|
331504
|
-
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)) {
|
|
331505
|
-
return null;
|
|
331506
|
-
}
|
|
331507
|
-
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
331508
|
-
} catch {
|
|
331509
|
-
return null;
|
|
331510
|
-
}
|
|
331511
|
-
}
|
|
331512
|
-
function rectMatchesIntendedBounds(rect, bounds) {
|
|
331513
|
-
if (bounds.width <= 0 || bounds.height <= 0) return false;
|
|
331514
|
-
const rectCenterX = rect.x + rect.width / 2;
|
|
331515
|
-
const rectCenterY = rect.y + rect.height / 2;
|
|
331516
|
-
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
331517
|
-
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
331518
|
-
const rectCenterInBounds = rectCenterX >= bounds.x && rectCenterX <= bounds.x + bounds.width && rectCenterY >= bounds.y && rectCenterY <= bounds.y + bounds.height;
|
|
331519
|
-
const boundsCenterInRect = rect.width > 0 && rect.height > 0 && boundsCenterX >= rect.x && boundsCenterX <= rect.x + rect.width && boundsCenterY >= rect.y && boundsCenterY <= rect.y + rect.height;
|
|
331520
|
-
return rectCenterInBounds || boundsCenterInRect;
|
|
331521
|
-
}
|
|
331522
|
-
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
331523
|
-
const box = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
331524
|
-
let elementId = await getActiveElementId2(sessionPath);
|
|
331525
|
-
if (elementId && box && box.width > 0 && box.height > 0) {
|
|
331526
|
-
const rect = await getElementRect(sessionPath, elementId);
|
|
331527
|
-
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
331528
|
-
elementId = null;
|
|
331529
|
-
}
|
|
331530
|
-
}
|
|
331531
|
-
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
331532
|
-
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
331533
|
-
elementId = await getActiveElementId2(sessionPath);
|
|
331534
|
-
}
|
|
331535
|
-
return elementId;
|
|
331536
|
-
}
|
|
331537
|
-
async function typeText(sessionPath, value, bounds) {
|
|
331538
|
-
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
331539
|
-
if (!elementId) {
|
|
331540
|
-
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
331541
|
-
}
|
|
331542
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
331543
|
-
method: "POST",
|
|
331544
|
-
body: JSON.stringify({ text: value })
|
|
331545
|
-
});
|
|
331546
|
-
}
|
|
331547
|
-
async function clearField(sessionPath, bounds) {
|
|
331548
|
-
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
331549
|
-
if (!elementId) {
|
|
331550
|
-
throw new Error("No focused element available for CLEAR. Tap an input field first.");
|
|
331551
|
-
}
|
|
331552
|
-
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
331553
|
-
method: "POST",
|
|
331554
|
-
body: JSON.stringify({})
|
|
331555
|
-
});
|
|
331556
|
-
}
|
|
331557
|
-
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
331558
|
-
const dist = clampMobileSwipeDistance(distance);
|
|
331559
|
-
const { startX, startY, endX, endY } = computeMobileSwipePointers(
|
|
331560
|
-
windowSize.width,
|
|
331561
|
-
windowSize.height,
|
|
331562
|
-
direction,
|
|
331563
|
-
dist
|
|
331564
|
-
);
|
|
331565
|
-
await appiumRequest(`${sessionPath}/actions`, {
|
|
331566
|
-
method: "POST",
|
|
331567
|
-
body: JSON.stringify({
|
|
331568
|
-
actions: [{
|
|
331569
|
-
type: "pointer",
|
|
331570
|
-
id: "finger1",
|
|
331571
|
-
parameters: { pointerType: "touch" },
|
|
331572
|
-
actions: [
|
|
331573
|
-
{ type: "pointerMove", duration: 0, x: startX, y: startY },
|
|
331574
|
-
{ type: "pointerDown", button: 0 },
|
|
331575
|
-
{ type: "pointerMove", duration: 300, x: endX, y: endY },
|
|
331576
|
-
{ type: "pointerUp", button: 0 }
|
|
331577
|
-
]
|
|
331578
|
-
}]
|
|
331579
|
-
})
|
|
331580
|
-
});
|
|
331581
|
-
}
|
|
331582
|
-
async function pressBack(sessionPath) {
|
|
331583
|
-
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
331584
|
-
}
|
|
331585
|
-
async function pressHome(sessionPath, platform3) {
|
|
331586
|
-
if (platform3 === "IOS") {
|
|
331587
|
-
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
331588
|
-
method: "POST",
|
|
331589
|
-
body: JSON.stringify({
|
|
331590
|
-
script: "mobile: pressButton",
|
|
331591
|
-
args: [{ name: "home" }]
|
|
331592
|
-
})
|
|
331593
|
-
});
|
|
331594
|
-
return;
|
|
331595
|
-
}
|
|
331596
|
-
await appiumRequest(`${sessionPath}/appium/device/press_keycode`, {
|
|
331597
|
-
method: "POST",
|
|
331598
|
-
body: JSON.stringify({ keycode: 3 })
|
|
331599
|
-
});
|
|
331600
|
-
}
|
|
331601
|
-
|
|
331602
|
-
// src/services/mobile-bridge.ts
|
|
331603
331660
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
331604
331661
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
331605
331662
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
@@ -331693,7 +331750,7 @@ async function getPageSource2() {
|
|
|
331693
331750
|
async function getWindowSize3() {
|
|
331694
331751
|
if (!state.appiumSessionId) return null;
|
|
331695
331752
|
validateSessionId(state.appiumSessionId);
|
|
331696
|
-
return
|
|
331753
|
+
return getWindowSize(`/session/${state.appiumSessionId}`);
|
|
331697
331754
|
}
|
|
331698
331755
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
331699
331756
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
@@ -332757,6 +332814,7 @@ var MobileBridgeDriver = class {
|
|
|
332757
332814
|
sessionId;
|
|
332758
332815
|
platform;
|
|
332759
332816
|
appId;
|
|
332817
|
+
allowAppReset;
|
|
332760
332818
|
constructor(config) {
|
|
332761
332819
|
if (!APPIUM_SESSION_ID_PATTERN2.test(config.sessionId)) {
|
|
332762
332820
|
throw new Error("MobileBridgeDriver: invalid Appium session id");
|
|
@@ -332770,6 +332828,7 @@ var MobileBridgeDriver = class {
|
|
|
332770
332828
|
this.sessionId = config.sessionId;
|
|
332771
332829
|
this.platform = config.platform;
|
|
332772
332830
|
this.appId = config.appId;
|
|
332831
|
+
this.allowAppReset = config.allowAppReset === true;
|
|
332773
332832
|
}
|
|
332774
332833
|
/** WebDriver session path (`/session/<id>`), the prefix every primitive needs. */
|
|
332775
332834
|
get sessionPath() {
|
|
@@ -332831,7 +332890,7 @@ var MobileBridgeDriver = class {
|
|
|
332831
332890
|
const [xml, screenshot, windowSize] = await Promise.all([
|
|
332832
332891
|
getPageSource(this.sessionPath),
|
|
332833
332892
|
getScreenshot(this.sessionPath),
|
|
332834
|
-
|
|
332893
|
+
getWindowSize(this.sessionPath)
|
|
332835
332894
|
]);
|
|
332836
332895
|
const source = xml ?? "";
|
|
332837
332896
|
const screenSignal = await this.enrichScreenSignal(source);
|
|
@@ -332844,7 +332903,7 @@ var MobileBridgeDriver = class {
|
|
|
332844
332903
|
}
|
|
332845
332904
|
/** Device window size; null when unreadable (swipe geometry degrades gracefully). */
|
|
332846
332905
|
async windowSize() {
|
|
332847
|
-
return
|
|
332906
|
+
return getWindowSize(this.sessionPath);
|
|
332848
332907
|
}
|
|
332849
332908
|
// ── Action result wrapper ──────────────────────────────
|
|
332850
332909
|
/**
|
|
@@ -332886,7 +332945,7 @@ var MobileBridgeDriver = class {
|
|
|
332886
332945
|
* hard error so we never fire an off-screen gesture.
|
|
332887
332946
|
*/
|
|
332888
332947
|
async swipe(direction, distance) {
|
|
332889
|
-
const size = await
|
|
332948
|
+
const size = await getWindowSize(this.sessionPath);
|
|
332890
332949
|
if (!size) {
|
|
332891
332950
|
return { ok: false, error: "Could not determine device screen dimensions for swipe." };
|
|
332892
332951
|
}
|
|
@@ -332911,6 +332970,22 @@ var MobileBridgeDriver = class {
|
|
|
332911
332970
|
await this.executeScript("mobile: terminateApp", [args]);
|
|
332912
332971
|
await this.executeScript("mobile: activateApp", [args]);
|
|
332913
332972
|
}
|
|
332973
|
+
/**
|
|
332974
|
+
* Deterministic state reset: clear app data + relaunch (Android emulators
|
|
332975
|
+
* only, per the runner-computed allowAppReset policy). Returns false when
|
|
332976
|
+
* unsupported/disallowed or when clearApp throws — callers fall back to
|
|
332977
|
+
* relaunch(), preserving today's behavior.
|
|
332978
|
+
*/
|
|
332979
|
+
async resetAppState() {
|
|
332980
|
+
if (!this.allowAppReset || this.platform !== "ANDROID") return false;
|
|
332981
|
+
try {
|
|
332982
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
332983
|
+
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
332984
|
+
return true;
|
|
332985
|
+
} catch {
|
|
332986
|
+
return false;
|
|
332987
|
+
}
|
|
332988
|
+
}
|
|
332914
332989
|
/** (Re)launch the target app (activateApp) from a backgrounded/closed state. */
|
|
332915
332990
|
async launchApp() {
|
|
332916
332991
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
@@ -335052,7 +335127,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
335052
335127
|
const driver = new MobileBridgeDriver({
|
|
335053
335128
|
sessionId: session.sessionId,
|
|
335054
335129
|
platform: platform3,
|
|
335055
|
-
appId: session.appId
|
|
335130
|
+
appId: session.appId,
|
|
335131
|
+
// Deterministic-state authorization: Android emulators only by
|
|
335132
|
+
// default (VALIDATEQA_MOBILE_APP_RESET overrides) — a personal
|
|
335133
|
+
// physical device must never have its app data wiped.
|
|
335134
|
+
allowAppReset: shouldResetMobileAppData(platform3, session.deviceId)
|
|
335056
335135
|
});
|
|
335057
335136
|
discoveryResult = await (0, import_runner_core4.runMobileDiscoveryOnRunner)(ctx, driver, {
|
|
335058
335137
|
onLog: mobileOnLog,
|