@validate.qa/runner 1.0.19 → 1.0.21
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 +335 -244
- package/dist/cli.mjs +335 -244
- 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);
|
|
@@ -66070,6 +66073,8 @@ ${constraints.join("\n")}`);
|
|
|
66070
66073
|
function buildMobileFallbackScenarios(input) {
|
|
66071
66074
|
if (input.mode === "survey")
|
|
66072
66075
|
return [];
|
|
66076
|
+
if (input.featureName?.trim())
|
|
66077
|
+
return [];
|
|
66073
66078
|
const orderedScreens = targetAppScreens(input.graph, input.appId).filter((state2) => state2.screenType?.toLowerCase() !== "error").filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
66074
66079
|
if (orderedScreens.length === 0)
|
|
66075
66080
|
return [];
|
|
@@ -66217,6 +66222,15 @@ ${constraints.join("\n")}`);
|
|
|
66217
66222
|
fallbackReason: result.fallbackReason ?? "Mobile planner produced no scenarios"
|
|
66218
66223
|
};
|
|
66219
66224
|
}
|
|
66225
|
+
if (input.featureName?.trim()) {
|
|
66226
|
+
return {
|
|
66227
|
+
...result,
|
|
66228
|
+
featureGroups: mobileFeatureGroups,
|
|
66229
|
+
warning: [result.warning, `Deep discovery could not plan any scenarios for "${input.featureName}" \u2014 the explorer did not reach that feature's screens this run. If the feature requires a signed-in session, verify TEST_USER_EMAIL / TEST_USER_PASSWORD are valid for this app, then re-run.`].filter(Boolean).join(" | "),
|
|
66230
|
+
fallbackUsed: true,
|
|
66231
|
+
fallbackReason: result.fallbackReason ?? `Planner produced no scenarios for feature "${input.featureName}"`
|
|
66232
|
+
};
|
|
66233
|
+
}
|
|
66220
66234
|
}
|
|
66221
66235
|
if (input.mode === "survey" && result.featureGroups.length === 0 && mobileFeatureGroups.length > 0) {
|
|
66222
66236
|
return {
|
|
@@ -66497,7 +66511,8 @@ Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
|
|
|
66497
66511
|
This app has stored login credentials under these keys:
|
|
66498
66512
|
${keyList}
|
|
66499
66513
|
|
|
66500
|
-
When a scenario needs a signed-in session
|
|
66514
|
+
When a scenario needs a signed-in session, complete the FULL login sequence \u2014 filling the fields alone does NOT log you in:
|
|
66515
|
+
1. Reach the email/password form. 2. Fill each credential field with \`mobile_fill_credential\` (pass the field ref and the KEY, e.g. \`{ ref: "e5", credentialKey: "TEST_USER_EMAIL" }\`) \u2014 the runner types the real value on the device and the generated test stores a \`{{KEY}}\` placeholder that replay resolves; you never see the secret. 3. TAP THE SUBMIT button (Sign In / Log In / Continue). 4. Re-snapshot and VERIFY an authenticated screen appeared. If login fails twice, do NOT keep retrying or wander: call finish_generate with verified:false and say the login could not be completed (the credentials may be invalid for this app).
|
|
66501
66516
|
|
|
66502
66517
|
SECURITY RULES:
|
|
66503
66518
|
- NEVER call \`mobile_type\` with a real email, password, or token value \u2014 the runner will REJECT the call and you lose an iteration. Use \`mobile_fill_credential\` for every credential field.
|
|
@@ -67165,7 +67180,7 @@ ${statePacket}` },
|
|
|
67165
67180
|
return out;
|
|
67166
67181
|
}
|
|
67167
67182
|
function assembleTest(args) {
|
|
67168
|
-
const { scenario, recording, ctx, platform: platform3, log: log2 } = args;
|
|
67183
|
+
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
67169
67184
|
const actions = collapseRetryCycles(recording.actions);
|
|
67170
67185
|
if (actions.length < recording.actions.length) {
|
|
67171
67186
|
log2(` collapsed ${recording.actions.length - actions.length} retry-cycle step(s) out of the recorded trace (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
@@ -67182,7 +67197,8 @@ ${statePacket}` },
|
|
|
67182
67197
|
sessionName: recording.finish?.name ?? scenario.name,
|
|
67183
67198
|
medium,
|
|
67184
67199
|
target,
|
|
67185
|
-
actions
|
|
67200
|
+
actions,
|
|
67201
|
+
appResetOnLaunch: appDataCleared
|
|
67186
67202
|
};
|
|
67187
67203
|
let compiled;
|
|
67188
67204
|
try {
|
|
@@ -67288,7 +67304,12 @@ ${statePacket}` },
|
|
|
67288
67304
|
log2(`
|
|
67289
67305
|
-- Generating scenario ${i + 1}/${scenarios.length}: "${scenario.name}" --`);
|
|
67290
67306
|
try {
|
|
67291
|
-
await driver.
|
|
67307
|
+
const appDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
67308
|
+
if (appDataCleared) {
|
|
67309
|
+
log2(" app data cleared \u2014 scenario records from a deterministic cold (logged-out) state.");
|
|
67310
|
+
} else {
|
|
67311
|
+
await driver.relaunch();
|
|
67312
|
+
}
|
|
67292
67313
|
const recording = await runScenarioLoop({
|
|
67293
67314
|
scenario,
|
|
67294
67315
|
driver,
|
|
@@ -67305,7 +67326,7 @@ ${statePacket}` },
|
|
|
67305
67326
|
log: log2
|
|
67306
67327
|
});
|
|
67307
67328
|
consecutiveInfraFailures = 0;
|
|
67308
|
-
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
67329
|
+
const test = assembleTest({ scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 });
|
|
67309
67330
|
if (!test) {
|
|
67310
67331
|
log2(` scenario "${scenario.name}" produced no compilable test \u2014 skipped.`);
|
|
67311
67332
|
continue;
|
|
@@ -313776,6 +313797,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
313776
313797
|
};
|
|
313777
313798
|
}
|
|
313778
313799
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
313800
|
+
const exploreAppDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
313801
|
+
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
313802
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
313780
313803
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
313781
313804
|
const existingScreens = compactExistingScreens(ctx.existingSiteMap);
|
|
@@ -328465,7 +328488,7 @@ var require_package4 = __commonJS({
|
|
|
328465
328488
|
"package.json"(exports, module) {
|
|
328466
328489
|
module.exports = {
|
|
328467
328490
|
name: "@validate.qa/runner",
|
|
328468
|
-
version: "1.0.
|
|
328491
|
+
version: "1.0.21",
|
|
328469
328492
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328470
328493
|
bin: {
|
|
328471
328494
|
"validate-runner": "dist/cli.js"
|
|
@@ -328603,9 +328626,9 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
328603
328626
|
|
|
328604
328627
|
// src/services/appium-executor.ts
|
|
328605
328628
|
init_dist();
|
|
328606
|
-
|
|
328607
|
-
|
|
328608
|
-
|
|
328629
|
+
|
|
328630
|
+
// src/services/mobile/appium-actions.ts
|
|
328631
|
+
init_dist();
|
|
328609
328632
|
|
|
328610
328633
|
// src/services/appium-lifecycle.ts
|
|
328611
328634
|
import { spawn, execFileSync } from "child_process";
|
|
@@ -328890,6 +328913,234 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
328890
328913
|
};
|
|
328891
328914
|
}
|
|
328892
328915
|
|
|
328916
|
+
// src/services/mobile/appium-actions.ts
|
|
328917
|
+
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
328918
|
+
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
328919
|
+
async function appiumRequest(path, options) {
|
|
328920
|
+
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
328921
|
+
const res = await fetch(url, {
|
|
328922
|
+
...options,
|
|
328923
|
+
signal: options?.signal ?? AbortSignal.timeout(APPIUM_REQUEST_TIMEOUT_MS),
|
|
328924
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
328925
|
+
});
|
|
328926
|
+
if (!res.ok) {
|
|
328927
|
+
const body = await res.text().catch(() => "");
|
|
328928
|
+
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
328929
|
+
}
|
|
328930
|
+
return res.json();
|
|
328931
|
+
}
|
|
328932
|
+
function readValue(result) {
|
|
328933
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
328934
|
+
return result.value;
|
|
328935
|
+
}
|
|
328936
|
+
return void 0;
|
|
328937
|
+
}
|
|
328938
|
+
async function getScreenshot(sessionPath) {
|
|
328939
|
+
try {
|
|
328940
|
+
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
328941
|
+
return typeof value === "string" ? value : null;
|
|
328942
|
+
} catch {
|
|
328943
|
+
return null;
|
|
328944
|
+
}
|
|
328945
|
+
}
|
|
328946
|
+
async function getPageSource(sessionPath) {
|
|
328947
|
+
try {
|
|
328948
|
+
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
328949
|
+
return typeof value === "string" ? value : null;
|
|
328950
|
+
} catch {
|
|
328951
|
+
return null;
|
|
328952
|
+
}
|
|
328953
|
+
}
|
|
328954
|
+
async function getWindowSize(sessionPath) {
|
|
328955
|
+
const retries = 3;
|
|
328956
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
328957
|
+
try {
|
|
328958
|
+
const value = readValue(await appiumRequest(`${sessionPath}/window/rect`));
|
|
328959
|
+
if (value && typeof value === "object") {
|
|
328960
|
+
const w = value.width;
|
|
328961
|
+
const h = value.height;
|
|
328962
|
+
if (typeof w === "number" && typeof h === "number" && w > 0 && h > 0) {
|
|
328963
|
+
return { width: w, height: h };
|
|
328964
|
+
}
|
|
328965
|
+
}
|
|
328966
|
+
} catch {
|
|
328967
|
+
}
|
|
328968
|
+
if (attempt < retries - 1) {
|
|
328969
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
328970
|
+
}
|
|
328971
|
+
}
|
|
328972
|
+
return null;
|
|
328973
|
+
}
|
|
328974
|
+
async function getActiveElementId(sessionPath) {
|
|
328975
|
+
try {
|
|
328976
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/active`));
|
|
328977
|
+
if (!value || typeof value !== "object") return null;
|
|
328978
|
+
const v = value;
|
|
328979
|
+
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
328980
|
+
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
328981
|
+
return null;
|
|
328982
|
+
}
|
|
328983
|
+
return elementId;
|
|
328984
|
+
} catch {
|
|
328985
|
+
return null;
|
|
328986
|
+
}
|
|
328987
|
+
}
|
|
328988
|
+
function normaliseBounds(bounds) {
|
|
328989
|
+
if (!bounds || typeof bounds !== "object" || typeof bounds.x !== "number" || typeof bounds.y !== "number" || !Number.isFinite(bounds.x) || !Number.isFinite(bounds.y)) {
|
|
328990
|
+
return null;
|
|
328991
|
+
}
|
|
328992
|
+
const b = bounds;
|
|
328993
|
+
const width = typeof b.width === "number" && Number.isFinite(b.width) ? b.width : 0;
|
|
328994
|
+
const height = typeof b.height === "number" && Number.isFinite(b.height) ? b.height : 0;
|
|
328995
|
+
return { x: b.x, y: b.y, width, height };
|
|
328996
|
+
}
|
|
328997
|
+
async function tapBoundsCenter(sessionPath, bounds) {
|
|
328998
|
+
const box = normaliseBounds(bounds);
|
|
328999
|
+
if (!box) return false;
|
|
329000
|
+
const tapX = Math.round(box.x + box.width / 2);
|
|
329001
|
+
const tapY = Math.round(box.y + box.height / 2);
|
|
329002
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
329003
|
+
method: "POST",
|
|
329004
|
+
body: JSON.stringify({
|
|
329005
|
+
actions: [{
|
|
329006
|
+
type: "pointer",
|
|
329007
|
+
id: "finger1",
|
|
329008
|
+
parameters: { pointerType: "touch" },
|
|
329009
|
+
actions: [
|
|
329010
|
+
// Note: button: 0 is used because Appium's WebDriver protocol
|
|
329011
|
+
// translates mouse button events to touch events. Standard approach
|
|
329012
|
+
// for touch actions in Appium, despite the mouse button semantics.
|
|
329013
|
+
{ type: "pointerMove", duration: 0, x: tapX, y: tapY },
|
|
329014
|
+
{ type: "pointerDown", button: 0 },
|
|
329015
|
+
{ type: "pause", duration: 100 },
|
|
329016
|
+
{ type: "pointerUp", button: 0 }
|
|
329017
|
+
]
|
|
329018
|
+
}]
|
|
329019
|
+
})
|
|
329020
|
+
});
|
|
329021
|
+
return true;
|
|
329022
|
+
}
|
|
329023
|
+
async function tapAt(sessionPath, bounds) {
|
|
329024
|
+
const ok = await tapBoundsCenter(sessionPath, bounds);
|
|
329025
|
+
if (!ok) {
|
|
329026
|
+
throw new Error("TAP requires usable bounds (finite x/y)");
|
|
329027
|
+
}
|
|
329028
|
+
}
|
|
329029
|
+
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
329030
|
+
if (platform3 !== "ANDROID") return false;
|
|
329031
|
+
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
329032
|
+
if (policy === "never") return false;
|
|
329033
|
+
if (policy === "always") return true;
|
|
329034
|
+
return !!deviceId && deviceId.startsWith("emulator-");
|
|
329035
|
+
}
|
|
329036
|
+
async function getElementRect(sessionPath, elementId) {
|
|
329037
|
+
try {
|
|
329038
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/${elementId}/rect`));
|
|
329039
|
+
if (!value || typeof value !== "object") return null;
|
|
329040
|
+
const r = value;
|
|
329041
|
+
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)) {
|
|
329042
|
+
return null;
|
|
329043
|
+
}
|
|
329044
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
329045
|
+
} catch {
|
|
329046
|
+
return null;
|
|
329047
|
+
}
|
|
329048
|
+
}
|
|
329049
|
+
function rectMatchesIntendedBounds(rect, bounds) {
|
|
329050
|
+
if (bounds.width <= 0 || bounds.height <= 0) return false;
|
|
329051
|
+
const rectCenterX = rect.x + rect.width / 2;
|
|
329052
|
+
const rectCenterY = rect.y + rect.height / 2;
|
|
329053
|
+
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
329054
|
+
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
329055
|
+
const rectCenterInBounds = rectCenterX >= bounds.x && rectCenterX <= bounds.x + bounds.width && rectCenterY >= bounds.y && rectCenterY <= bounds.y + bounds.height;
|
|
329056
|
+
const boundsCenterInRect = rect.width > 0 && rect.height > 0 && boundsCenterX >= rect.x && boundsCenterX <= rect.x + rect.width && boundsCenterY >= rect.y && boundsCenterY <= rect.y + rect.height;
|
|
329057
|
+
return rectCenterInBounds || boundsCenterInRect;
|
|
329058
|
+
}
|
|
329059
|
+
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
329060
|
+
const box = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
329061
|
+
let elementId = await getActiveElementId(sessionPath);
|
|
329062
|
+
if (elementId && box && box.width > 0 && box.height > 0) {
|
|
329063
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
329064
|
+
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
329065
|
+
elementId = null;
|
|
329066
|
+
}
|
|
329067
|
+
}
|
|
329068
|
+
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
329069
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
329070
|
+
elementId = await getActiveElementId(sessionPath);
|
|
329071
|
+
}
|
|
329072
|
+
return elementId;
|
|
329073
|
+
}
|
|
329074
|
+
async function typeText(sessionPath, value, bounds) {
|
|
329075
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329076
|
+
if (!elementId) {
|
|
329077
|
+
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
329078
|
+
}
|
|
329079
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
329080
|
+
method: "POST",
|
|
329081
|
+
body: JSON.stringify({ text: value })
|
|
329082
|
+
});
|
|
329083
|
+
}
|
|
329084
|
+
async function clearField(sessionPath, bounds) {
|
|
329085
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329086
|
+
if (!elementId) {
|
|
329087
|
+
throw new Error("No focused element available for CLEAR. Tap an input field first.");
|
|
329088
|
+
}
|
|
329089
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
329090
|
+
method: "POST",
|
|
329091
|
+
body: JSON.stringify({})
|
|
329092
|
+
});
|
|
329093
|
+
}
|
|
329094
|
+
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
329095
|
+
const dist = clampMobileSwipeDistance(distance);
|
|
329096
|
+
const { startX, startY, endX, endY } = computeMobileSwipePointers(
|
|
329097
|
+
windowSize.width,
|
|
329098
|
+
windowSize.height,
|
|
329099
|
+
direction,
|
|
329100
|
+
dist
|
|
329101
|
+
);
|
|
329102
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
329103
|
+
method: "POST",
|
|
329104
|
+
body: JSON.stringify({
|
|
329105
|
+
actions: [{
|
|
329106
|
+
type: "pointer",
|
|
329107
|
+
id: "finger1",
|
|
329108
|
+
parameters: { pointerType: "touch" },
|
|
329109
|
+
actions: [
|
|
329110
|
+
{ type: "pointerMove", duration: 0, x: startX, y: startY },
|
|
329111
|
+
{ type: "pointerDown", button: 0 },
|
|
329112
|
+
{ type: "pointerMove", duration: 300, x: endX, y: endY },
|
|
329113
|
+
{ type: "pointerUp", button: 0 }
|
|
329114
|
+
]
|
|
329115
|
+
}]
|
|
329116
|
+
})
|
|
329117
|
+
});
|
|
329118
|
+
}
|
|
329119
|
+
async function pressBack(sessionPath) {
|
|
329120
|
+
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
329121
|
+
}
|
|
329122
|
+
async function pressHome(sessionPath, platform3) {
|
|
329123
|
+
if (platform3 === "IOS") {
|
|
329124
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
329125
|
+
method: "POST",
|
|
329126
|
+
body: JSON.stringify({
|
|
329127
|
+
script: "mobile: pressButton",
|
|
329128
|
+
args: [{ name: "home" }]
|
|
329129
|
+
})
|
|
329130
|
+
});
|
|
329131
|
+
return;
|
|
329132
|
+
}
|
|
329133
|
+
await appiumRequest(`${sessionPath}/appium/device/press_keycode`, {
|
|
329134
|
+
method: "POST",
|
|
329135
|
+
body: JSON.stringify({ keycode: 3 })
|
|
329136
|
+
});
|
|
329137
|
+
}
|
|
329138
|
+
|
|
329139
|
+
// src/services/appium-executor.ts
|
|
329140
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
329141
|
+
import { execFile as execFile2 } from "child_process";
|
|
329142
|
+
import { promisify as promisify2 } from "util";
|
|
329143
|
+
|
|
328893
329144
|
// src/services/device-discovery.ts
|
|
328894
329145
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
328895
329146
|
import { platform } from "os";
|
|
@@ -330055,7 +330306,7 @@ async function wdRequest(path, options) {
|
|
|
330055
330306
|
}
|
|
330056
330307
|
return res.json();
|
|
330057
330308
|
}
|
|
330058
|
-
async function
|
|
330309
|
+
async function getWindowSize2(sessionId, retries = 3) {
|
|
330059
330310
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
330060
330311
|
try {
|
|
330061
330312
|
const result = await wdRequest(`/session/${sessionId}/window/rect`);
|
|
@@ -330231,6 +330482,26 @@ async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
|
330231
330482
|
log2(` Fresh launch: activate failed (${err instanceof Error ? err.message : String(err)})`);
|
|
330232
330483
|
}
|
|
330233
330484
|
}
|
|
330485
|
+
async function waitForColdBootContent(sessionId, log2, timeoutMs = 2e4) {
|
|
330486
|
+
const deadline = Date.now() + timeoutMs;
|
|
330487
|
+
const MIN_ELEMENTS = 3;
|
|
330488
|
+
let lastCount = 0;
|
|
330489
|
+
while (Date.now() < deadline) {
|
|
330490
|
+
try {
|
|
330491
|
+
const res = await wdRequest(`/session/${sessionId}/source`);
|
|
330492
|
+
const xml = typeof res?.value === "string" ? res.value : "";
|
|
330493
|
+
const count = (xml.match(/<(android\.|XCUIElementType)/g) ?? []).length;
|
|
330494
|
+
lastCount = count;
|
|
330495
|
+
if (count >= MIN_ELEMENTS) {
|
|
330496
|
+
log2(` Cold-boot ready: UI tree has ${count} node(s).`);
|
|
330497
|
+
return;
|
|
330498
|
+
}
|
|
330499
|
+
} catch {
|
|
330500
|
+
}
|
|
330501
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
330502
|
+
}
|
|
330503
|
+
log2(` Cold-boot wait timed out after ${timeoutMs}ms (last tree had ${lastCount} node(s)) \u2014 proceeding anyway.`);
|
|
330504
|
+
}
|
|
330234
330505
|
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
330235
330506
|
let boundary = null;
|
|
330236
330507
|
try {
|
|
@@ -330478,7 +330749,7 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
330478
330749
|
})
|
|
330479
330750
|
});
|
|
330480
330751
|
}
|
|
330481
|
-
async function
|
|
330752
|
+
async function getActiveElementId2(sessionId) {
|
|
330482
330753
|
try {
|
|
330483
330754
|
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
330484
330755
|
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
@@ -330564,7 +330835,7 @@ async function executeType(sessionId, step, context) {
|
|
|
330564
330835
|
await tapCoordinates(sessionId, bounds);
|
|
330565
330836
|
tappedBounds = true;
|
|
330566
330837
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330567
|
-
elementId = await
|
|
330838
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330568
330839
|
if (elementId) {
|
|
330569
330840
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330570
330841
|
if (!lastInputError) return;
|
|
@@ -330586,10 +330857,10 @@ async function executeType(sessionId, step, context) {
|
|
|
330586
330857
|
await tapCoordinates(sessionId, bounds);
|
|
330587
330858
|
tappedBounds = true;
|
|
330588
330859
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330589
|
-
elementId = await
|
|
330860
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330590
330861
|
}
|
|
330591
330862
|
if (!elementId) {
|
|
330592
|
-
elementId = await
|
|
330863
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330593
330864
|
}
|
|
330594
330865
|
if (elementId) {
|
|
330595
330866
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
@@ -330639,10 +330910,10 @@ async function executeClear(sessionId, step, platform3, isCrossPlatform = false)
|
|
|
330639
330910
|
if (!elementId && bounds) {
|
|
330640
330911
|
await tapCoordinates(sessionId, bounds);
|
|
330641
330912
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330642
|
-
elementId = await
|
|
330913
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330643
330914
|
}
|
|
330644
330915
|
if (!elementId) {
|
|
330645
|
-
elementId = await
|
|
330916
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330646
330917
|
}
|
|
330647
330918
|
if (!elementId) {
|
|
330648
330919
|
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
@@ -330658,7 +330929,7 @@ async function executeSwipe(sessionId, step) {
|
|
|
330658
330929
|
let screenW;
|
|
330659
330930
|
let screenH;
|
|
330660
330931
|
try {
|
|
330661
|
-
const windowSize = await
|
|
330932
|
+
const windowSize = await getWindowSize2(sessionId);
|
|
330662
330933
|
screenW = windowSize.width;
|
|
330663
330934
|
screenH = windowSize.height;
|
|
330664
330935
|
} catch (err) {
|
|
@@ -330873,8 +331144,25 @@ async function executeMobileTest(options) {
|
|
|
330873
331144
|
}
|
|
330874
331145
|
}
|
|
330875
331146
|
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
331147
|
+
const launchStepForReset = runSteps.find((s) => s.action === "launchApp");
|
|
331148
|
+
const recordedFromClearedState = launchStepForReset?.metadata?.appReset === "cleared";
|
|
331149
|
+
if (recordedFromClearedState) {
|
|
331150
|
+
if (shouldResetMobileAppData(options.target.platform, selectedDevice.id)) {
|
|
331151
|
+
try {
|
|
331152
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
331153
|
+
log2("App data cleared before launch (test was recorded from a cold logged-out state).");
|
|
331154
|
+
} catch (err) {
|
|
331155
|
+
log2(`App-data clear failed (continuing with plain fresh launch): ${err instanceof Error ? err.message : String(err)}`);
|
|
331156
|
+
}
|
|
331157
|
+
} else {
|
|
331158
|
+
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.");
|
|
331159
|
+
}
|
|
331160
|
+
}
|
|
330876
331161
|
log2("Opening a fresh app instance for the test...");
|
|
330877
331162
|
await freshLaunchTargetApp(sessionId, options.target.platform, options.target.appId, log2);
|
|
331163
|
+
if (recordedFromClearedState) {
|
|
331164
|
+
await waitForColdBootContent(sessionId, log2);
|
|
331165
|
+
}
|
|
330878
331166
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
330879
331167
|
try {
|
|
330880
331168
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -331389,225 +331677,6 @@ import { homedir as homedir2 } from "os";
|
|
|
331389
331677
|
import { join as join3, dirname } from "path";
|
|
331390
331678
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
331391
331679
|
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
331680
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
331612
331681
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
331613
331682
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
@@ -331701,7 +331770,7 @@ async function getPageSource2() {
|
|
|
331701
331770
|
async function getWindowSize3() {
|
|
331702
331771
|
if (!state.appiumSessionId) return null;
|
|
331703
331772
|
validateSessionId(state.appiumSessionId);
|
|
331704
|
-
return
|
|
331773
|
+
return getWindowSize(`/session/${state.appiumSessionId}`);
|
|
331705
331774
|
}
|
|
331706
331775
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
331707
331776
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
@@ -332765,6 +332834,7 @@ var MobileBridgeDriver = class {
|
|
|
332765
332834
|
sessionId;
|
|
332766
332835
|
platform;
|
|
332767
332836
|
appId;
|
|
332837
|
+
allowAppReset;
|
|
332768
332838
|
constructor(config) {
|
|
332769
332839
|
if (!APPIUM_SESSION_ID_PATTERN2.test(config.sessionId)) {
|
|
332770
332840
|
throw new Error("MobileBridgeDriver: invalid Appium session id");
|
|
@@ -332778,6 +332848,7 @@ var MobileBridgeDriver = class {
|
|
|
332778
332848
|
this.sessionId = config.sessionId;
|
|
332779
332849
|
this.platform = config.platform;
|
|
332780
332850
|
this.appId = config.appId;
|
|
332851
|
+
this.allowAppReset = config.allowAppReset === true;
|
|
332781
332852
|
}
|
|
332782
332853
|
/** WebDriver session path (`/session/<id>`), the prefix every primitive needs. */
|
|
332783
332854
|
get sessionPath() {
|
|
@@ -332839,7 +332910,7 @@ var MobileBridgeDriver = class {
|
|
|
332839
332910
|
const [xml, screenshot, windowSize] = await Promise.all([
|
|
332840
332911
|
getPageSource(this.sessionPath),
|
|
332841
332912
|
getScreenshot(this.sessionPath),
|
|
332842
|
-
|
|
332913
|
+
getWindowSize(this.sessionPath)
|
|
332843
332914
|
]);
|
|
332844
332915
|
const source = xml ?? "";
|
|
332845
332916
|
const screenSignal = await this.enrichScreenSignal(source);
|
|
@@ -332852,7 +332923,7 @@ var MobileBridgeDriver = class {
|
|
|
332852
332923
|
}
|
|
332853
332924
|
/** Device window size; null when unreadable (swipe geometry degrades gracefully). */
|
|
332854
332925
|
async windowSize() {
|
|
332855
|
-
return
|
|
332926
|
+
return getWindowSize(this.sessionPath);
|
|
332856
332927
|
}
|
|
332857
332928
|
// ── Action result wrapper ──────────────────────────────
|
|
332858
332929
|
/**
|
|
@@ -332894,7 +332965,7 @@ var MobileBridgeDriver = class {
|
|
|
332894
332965
|
* hard error so we never fire an off-screen gesture.
|
|
332895
332966
|
*/
|
|
332896
332967
|
async swipe(direction, distance) {
|
|
332897
|
-
const size = await
|
|
332968
|
+
const size = await getWindowSize(this.sessionPath);
|
|
332898
332969
|
if (!size) {
|
|
332899
332970
|
return { ok: false, error: "Could not determine device screen dimensions for swipe." };
|
|
332900
332971
|
}
|
|
@@ -332919,6 +332990,22 @@ var MobileBridgeDriver = class {
|
|
|
332919
332990
|
await this.executeScript("mobile: terminateApp", [args]);
|
|
332920
332991
|
await this.executeScript("mobile: activateApp", [args]);
|
|
332921
332992
|
}
|
|
332993
|
+
/**
|
|
332994
|
+
* Deterministic state reset: clear app data + relaunch (Android emulators
|
|
332995
|
+
* only, per the runner-computed allowAppReset policy). Returns false when
|
|
332996
|
+
* unsupported/disallowed or when clearApp throws — callers fall back to
|
|
332997
|
+
* relaunch(), preserving today's behavior.
|
|
332998
|
+
*/
|
|
332999
|
+
async resetAppState() {
|
|
333000
|
+
if (!this.allowAppReset || this.platform !== "ANDROID") return false;
|
|
333001
|
+
try {
|
|
333002
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
333003
|
+
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
333004
|
+
return true;
|
|
333005
|
+
} catch {
|
|
333006
|
+
return false;
|
|
333007
|
+
}
|
|
333008
|
+
}
|
|
332922
333009
|
/** (Re)launch the target app (activateApp) from a backgrounded/closed state. */
|
|
332923
333010
|
async launchApp() {
|
|
332924
333011
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
@@ -335060,7 +335147,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
335060
335147
|
const driver = new MobileBridgeDriver({
|
|
335061
335148
|
sessionId: session.sessionId,
|
|
335062
335149
|
platform: platform3,
|
|
335063
|
-
appId: session.appId
|
|
335150
|
+
appId: session.appId,
|
|
335151
|
+
// Deterministic-state authorization: Android emulators only by
|
|
335152
|
+
// default (VALIDATEQA_MOBILE_APP_RESET overrides) — a personal
|
|
335153
|
+
// physical device must never have its app data wiped.
|
|
335154
|
+
allowAppReset: shouldResetMobileAppData(platform3, session.deviceId)
|
|
335064
335155
|
});
|
|
335065
335156
|
discoveryResult = await (0, import_runner_core4.runMobileDiscoveryOnRunner)(ctx, driver, {
|
|
335066
335157
|
onLog: mobileOnLog,
|