@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.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);
|
|
@@ -66065,6 +66068,8 @@ ${constraints.join("\n")}`);
|
|
|
66065
66068
|
function buildMobileFallbackScenarios(input) {
|
|
66066
66069
|
if (input.mode === "survey")
|
|
66067
66070
|
return [];
|
|
66071
|
+
if (input.featureName?.trim())
|
|
66072
|
+
return [];
|
|
66068
66073
|
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);
|
|
66069
66074
|
if (orderedScreens.length === 0)
|
|
66070
66075
|
return [];
|
|
@@ -66212,6 +66217,15 @@ ${constraints.join("\n")}`);
|
|
|
66212
66217
|
fallbackReason: result.fallbackReason ?? "Mobile planner produced no scenarios"
|
|
66213
66218
|
};
|
|
66214
66219
|
}
|
|
66220
|
+
if (input.featureName?.trim()) {
|
|
66221
|
+
return {
|
|
66222
|
+
...result,
|
|
66223
|
+
featureGroups: mobileFeatureGroups,
|
|
66224
|
+
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(" | "),
|
|
66225
|
+
fallbackUsed: true,
|
|
66226
|
+
fallbackReason: result.fallbackReason ?? `Planner produced no scenarios for feature "${input.featureName}"`
|
|
66227
|
+
};
|
|
66228
|
+
}
|
|
66215
66229
|
}
|
|
66216
66230
|
if (input.mode === "survey" && result.featureGroups.length === 0 && mobileFeatureGroups.length > 0) {
|
|
66217
66231
|
return {
|
|
@@ -66492,7 +66506,8 @@ Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
|
|
|
66492
66506
|
This app has stored login credentials under these keys:
|
|
66493
66507
|
${keyList}
|
|
66494
66508
|
|
|
66495
|
-
When a scenario needs a signed-in session
|
|
66509
|
+
When a scenario needs a signed-in session, complete the FULL login sequence \u2014 filling the fields alone does NOT log you in:
|
|
66510
|
+
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).
|
|
66496
66511
|
|
|
66497
66512
|
SECURITY RULES:
|
|
66498
66513
|
- 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.
|
|
@@ -67160,7 +67175,7 @@ ${statePacket}` },
|
|
|
67160
67175
|
return out;
|
|
67161
67176
|
}
|
|
67162
67177
|
function assembleTest(args) {
|
|
67163
|
-
const { scenario, recording, ctx, platform: platform3, log: log2 } = args;
|
|
67178
|
+
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
67164
67179
|
const actions = collapseRetryCycles(recording.actions);
|
|
67165
67180
|
if (actions.length < recording.actions.length) {
|
|
67166
67181
|
log2(` collapsed ${recording.actions.length - actions.length} retry-cycle step(s) out of the recorded trace (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
@@ -67177,7 +67192,8 @@ ${statePacket}` },
|
|
|
67177
67192
|
sessionName: recording.finish?.name ?? scenario.name,
|
|
67178
67193
|
medium,
|
|
67179
67194
|
target,
|
|
67180
|
-
actions
|
|
67195
|
+
actions,
|
|
67196
|
+
appResetOnLaunch: appDataCleared
|
|
67181
67197
|
};
|
|
67182
67198
|
let compiled;
|
|
67183
67199
|
try {
|
|
@@ -67283,7 +67299,12 @@ ${statePacket}` },
|
|
|
67283
67299
|
log2(`
|
|
67284
67300
|
-- Generating scenario ${i + 1}/${scenarios.length}: "${scenario.name}" --`);
|
|
67285
67301
|
try {
|
|
67286
|
-
await driver.
|
|
67302
|
+
const appDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
67303
|
+
if (appDataCleared) {
|
|
67304
|
+
log2(" app data cleared \u2014 scenario records from a deterministic cold (logged-out) state.");
|
|
67305
|
+
} else {
|
|
67306
|
+
await driver.relaunch();
|
|
67307
|
+
}
|
|
67287
67308
|
const recording = await runScenarioLoop({
|
|
67288
67309
|
scenario,
|
|
67289
67310
|
driver,
|
|
@@ -67300,7 +67321,7 @@ ${statePacket}` },
|
|
|
67300
67321
|
log: log2
|
|
67301
67322
|
});
|
|
67302
67323
|
consecutiveInfraFailures = 0;
|
|
67303
|
-
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
67324
|
+
const test = assembleTest({ scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 });
|
|
67304
67325
|
if (!test) {
|
|
67305
67326
|
log2(` scenario "${scenario.name}" produced no compilable test \u2014 skipped.`);
|
|
67306
67327
|
continue;
|
|
@@ -313771,6 +313792,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
313771
313792
|
};
|
|
313772
313793
|
}
|
|
313773
313794
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
313795
|
+
const exploreAppDataCleared = await driver.resetAppState?.().catch(() => false) ?? false;
|
|
313796
|
+
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
313797
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
313775
313798
|
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
313776
313799
|
const existingScreens = compactExistingScreens(ctx.existingSiteMap);
|
|
@@ -328460,7 +328483,7 @@ var require_package4 = __commonJS({
|
|
|
328460
328483
|
"package.json"(exports2, module2) {
|
|
328461
328484
|
module2.exports = {
|
|
328462
328485
|
name: "@validate.qa/runner",
|
|
328463
|
-
version: "1.0.
|
|
328486
|
+
version: "1.0.21",
|
|
328464
328487
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
328465
328488
|
bin: {
|
|
328466
328489
|
"validate-runner": "dist/cli.js"
|
|
@@ -328598,9 +328621,9 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
328598
328621
|
|
|
328599
328622
|
// src/services/appium-executor.ts
|
|
328600
328623
|
init_dist();
|
|
328601
|
-
|
|
328602
|
-
|
|
328603
|
-
|
|
328624
|
+
|
|
328625
|
+
// src/services/mobile/appium-actions.ts
|
|
328626
|
+
init_dist();
|
|
328604
328627
|
|
|
328605
328628
|
// src/services/appium-lifecycle.ts
|
|
328606
328629
|
var import_child_process = require("child_process");
|
|
@@ -328885,6 +328908,234 @@ function resetServerState(port = APPIUM_PORT, restartCount = 0) {
|
|
|
328885
328908
|
};
|
|
328886
328909
|
}
|
|
328887
328910
|
|
|
328911
|
+
// src/services/mobile/appium-actions.ts
|
|
328912
|
+
var APPIUM_REQUEST_TIMEOUT_MS = 1e4;
|
|
328913
|
+
var ELEMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
328914
|
+
async function appiumRequest(path, options) {
|
|
328915
|
+
const url = `http://localhost:${getAppiumState().port}${path}`;
|
|
328916
|
+
const res = await fetch(url, {
|
|
328917
|
+
...options,
|
|
328918
|
+
signal: options?.signal ?? AbortSignal.timeout(APPIUM_REQUEST_TIMEOUT_MS),
|
|
328919
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
328920
|
+
});
|
|
328921
|
+
if (!res.ok) {
|
|
328922
|
+
const body = await res.text().catch(() => "");
|
|
328923
|
+
throw new Error(`Appium error ${res.status}: ${body.slice(0, 200)}`);
|
|
328924
|
+
}
|
|
328925
|
+
return res.json();
|
|
328926
|
+
}
|
|
328927
|
+
function readValue(result) {
|
|
328928
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
328929
|
+
return result.value;
|
|
328930
|
+
}
|
|
328931
|
+
return void 0;
|
|
328932
|
+
}
|
|
328933
|
+
async function getScreenshot(sessionPath) {
|
|
328934
|
+
try {
|
|
328935
|
+
const value = readValue(await appiumRequest(`${sessionPath}/screenshot`));
|
|
328936
|
+
return typeof value === "string" ? value : null;
|
|
328937
|
+
} catch {
|
|
328938
|
+
return null;
|
|
328939
|
+
}
|
|
328940
|
+
}
|
|
328941
|
+
async function getPageSource(sessionPath) {
|
|
328942
|
+
try {
|
|
328943
|
+
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
328944
|
+
return typeof value === "string" ? value : null;
|
|
328945
|
+
} catch {
|
|
328946
|
+
return null;
|
|
328947
|
+
}
|
|
328948
|
+
}
|
|
328949
|
+
async function getWindowSize(sessionPath) {
|
|
328950
|
+
const retries = 3;
|
|
328951
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
328952
|
+
try {
|
|
328953
|
+
const value = readValue(await appiumRequest(`${sessionPath}/window/rect`));
|
|
328954
|
+
if (value && typeof value === "object") {
|
|
328955
|
+
const w = value.width;
|
|
328956
|
+
const h = value.height;
|
|
328957
|
+
if (typeof w === "number" && typeof h === "number" && w > 0 && h > 0) {
|
|
328958
|
+
return { width: w, height: h };
|
|
328959
|
+
}
|
|
328960
|
+
}
|
|
328961
|
+
} catch {
|
|
328962
|
+
}
|
|
328963
|
+
if (attempt < retries - 1) {
|
|
328964
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
328965
|
+
}
|
|
328966
|
+
}
|
|
328967
|
+
return null;
|
|
328968
|
+
}
|
|
328969
|
+
async function getActiveElementId(sessionPath) {
|
|
328970
|
+
try {
|
|
328971
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/active`));
|
|
328972
|
+
if (!value || typeof value !== "object") return null;
|
|
328973
|
+
const v = value;
|
|
328974
|
+
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
328975
|
+
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
328976
|
+
return null;
|
|
328977
|
+
}
|
|
328978
|
+
return elementId;
|
|
328979
|
+
} catch {
|
|
328980
|
+
return null;
|
|
328981
|
+
}
|
|
328982
|
+
}
|
|
328983
|
+
function normaliseBounds(bounds) {
|
|
328984
|
+
if (!bounds || typeof bounds !== "object" || typeof bounds.x !== "number" || typeof bounds.y !== "number" || !Number.isFinite(bounds.x) || !Number.isFinite(bounds.y)) {
|
|
328985
|
+
return null;
|
|
328986
|
+
}
|
|
328987
|
+
const b = bounds;
|
|
328988
|
+
const width = typeof b.width === "number" && Number.isFinite(b.width) ? b.width : 0;
|
|
328989
|
+
const height = typeof b.height === "number" && Number.isFinite(b.height) ? b.height : 0;
|
|
328990
|
+
return { x: b.x, y: b.y, width, height };
|
|
328991
|
+
}
|
|
328992
|
+
async function tapBoundsCenter(sessionPath, bounds) {
|
|
328993
|
+
const box = normaliseBounds(bounds);
|
|
328994
|
+
if (!box) return false;
|
|
328995
|
+
const tapX = Math.round(box.x + box.width / 2);
|
|
328996
|
+
const tapY = Math.round(box.y + box.height / 2);
|
|
328997
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
328998
|
+
method: "POST",
|
|
328999
|
+
body: JSON.stringify({
|
|
329000
|
+
actions: [{
|
|
329001
|
+
type: "pointer",
|
|
329002
|
+
id: "finger1",
|
|
329003
|
+
parameters: { pointerType: "touch" },
|
|
329004
|
+
actions: [
|
|
329005
|
+
// Note: button: 0 is used because Appium's WebDriver protocol
|
|
329006
|
+
// translates mouse button events to touch events. Standard approach
|
|
329007
|
+
// for touch actions in Appium, despite the mouse button semantics.
|
|
329008
|
+
{ type: "pointerMove", duration: 0, x: tapX, y: tapY },
|
|
329009
|
+
{ type: "pointerDown", button: 0 },
|
|
329010
|
+
{ type: "pause", duration: 100 },
|
|
329011
|
+
{ type: "pointerUp", button: 0 }
|
|
329012
|
+
]
|
|
329013
|
+
}]
|
|
329014
|
+
})
|
|
329015
|
+
});
|
|
329016
|
+
return true;
|
|
329017
|
+
}
|
|
329018
|
+
async function tapAt(sessionPath, bounds) {
|
|
329019
|
+
const ok = await tapBoundsCenter(sessionPath, bounds);
|
|
329020
|
+
if (!ok) {
|
|
329021
|
+
throw new Error("TAP requires usable bounds (finite x/y)");
|
|
329022
|
+
}
|
|
329023
|
+
}
|
|
329024
|
+
function shouldResetMobileAppData(platform3, deviceId, env = process.env) {
|
|
329025
|
+
if (platform3 !== "ANDROID") return false;
|
|
329026
|
+
const policy = (env.VALIDATEQA_MOBILE_APP_RESET ?? "").trim().toLowerCase();
|
|
329027
|
+
if (policy === "never") return false;
|
|
329028
|
+
if (policy === "always") return true;
|
|
329029
|
+
return !!deviceId && deviceId.startsWith("emulator-");
|
|
329030
|
+
}
|
|
329031
|
+
async function getElementRect(sessionPath, elementId) {
|
|
329032
|
+
try {
|
|
329033
|
+
const value = readValue(await appiumRequest(`${sessionPath}/element/${elementId}/rect`));
|
|
329034
|
+
if (!value || typeof value !== "object") return null;
|
|
329035
|
+
const r = value;
|
|
329036
|
+
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)) {
|
|
329037
|
+
return null;
|
|
329038
|
+
}
|
|
329039
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
329040
|
+
} catch {
|
|
329041
|
+
return null;
|
|
329042
|
+
}
|
|
329043
|
+
}
|
|
329044
|
+
function rectMatchesIntendedBounds(rect, bounds) {
|
|
329045
|
+
if (bounds.width <= 0 || bounds.height <= 0) return false;
|
|
329046
|
+
const rectCenterX = rect.x + rect.width / 2;
|
|
329047
|
+
const rectCenterY = rect.y + rect.height / 2;
|
|
329048
|
+
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
329049
|
+
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
329050
|
+
const rectCenterInBounds = rectCenterX >= bounds.x && rectCenterX <= bounds.x + bounds.width && rectCenterY >= bounds.y && rectCenterY <= bounds.y + bounds.height;
|
|
329051
|
+
const boundsCenterInRect = rect.width > 0 && rect.height > 0 && boundsCenterX >= rect.x && boundsCenterX <= rect.x + rect.width && boundsCenterY >= rect.y && boundsCenterY <= rect.y + rect.height;
|
|
329052
|
+
return rectCenterInBounds || boundsCenterInRect;
|
|
329053
|
+
}
|
|
329054
|
+
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
329055
|
+
const box = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
329056
|
+
let elementId = await getActiveElementId(sessionPath);
|
|
329057
|
+
if (elementId && box && box.width > 0 && box.height > 0) {
|
|
329058
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
329059
|
+
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
329060
|
+
elementId = null;
|
|
329061
|
+
}
|
|
329062
|
+
}
|
|
329063
|
+
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
329064
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
329065
|
+
elementId = await getActiveElementId(sessionPath);
|
|
329066
|
+
}
|
|
329067
|
+
return elementId;
|
|
329068
|
+
}
|
|
329069
|
+
async function typeText(sessionPath, value, bounds) {
|
|
329070
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329071
|
+
if (!elementId) {
|
|
329072
|
+
throw new Error("No focused element available for TYPE. Tap an input field first.");
|
|
329073
|
+
}
|
|
329074
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/value`, {
|
|
329075
|
+
method: "POST",
|
|
329076
|
+
body: JSON.stringify({ text: value })
|
|
329077
|
+
});
|
|
329078
|
+
}
|
|
329079
|
+
async function clearField(sessionPath, bounds) {
|
|
329080
|
+
const elementId = await resolveTypeTargetElementId(sessionPath, bounds);
|
|
329081
|
+
if (!elementId) {
|
|
329082
|
+
throw new Error("No focused element available for CLEAR. Tap an input field first.");
|
|
329083
|
+
}
|
|
329084
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/clear`, {
|
|
329085
|
+
method: "POST",
|
|
329086
|
+
body: JSON.stringify({})
|
|
329087
|
+
});
|
|
329088
|
+
}
|
|
329089
|
+
async function swipe(sessionPath, direction, distance, windowSize) {
|
|
329090
|
+
const dist = clampMobileSwipeDistance(distance);
|
|
329091
|
+
const { startX, startY, endX, endY } = computeMobileSwipePointers(
|
|
329092
|
+
windowSize.width,
|
|
329093
|
+
windowSize.height,
|
|
329094
|
+
direction,
|
|
329095
|
+
dist
|
|
329096
|
+
);
|
|
329097
|
+
await appiumRequest(`${sessionPath}/actions`, {
|
|
329098
|
+
method: "POST",
|
|
329099
|
+
body: JSON.stringify({
|
|
329100
|
+
actions: [{
|
|
329101
|
+
type: "pointer",
|
|
329102
|
+
id: "finger1",
|
|
329103
|
+
parameters: { pointerType: "touch" },
|
|
329104
|
+
actions: [
|
|
329105
|
+
{ type: "pointerMove", duration: 0, x: startX, y: startY },
|
|
329106
|
+
{ type: "pointerDown", button: 0 },
|
|
329107
|
+
{ type: "pointerMove", duration: 300, x: endX, y: endY },
|
|
329108
|
+
{ type: "pointerUp", button: 0 }
|
|
329109
|
+
]
|
|
329110
|
+
}]
|
|
329111
|
+
})
|
|
329112
|
+
});
|
|
329113
|
+
}
|
|
329114
|
+
async function pressBack(sessionPath) {
|
|
329115
|
+
await appiumRequest(`${sessionPath}/back`, { method: "POST", body: "{}" });
|
|
329116
|
+
}
|
|
329117
|
+
async function pressHome(sessionPath, platform3) {
|
|
329118
|
+
if (platform3 === "IOS") {
|
|
329119
|
+
await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
329120
|
+
method: "POST",
|
|
329121
|
+
body: JSON.stringify({
|
|
329122
|
+
script: "mobile: pressButton",
|
|
329123
|
+
args: [{ name: "home" }]
|
|
329124
|
+
})
|
|
329125
|
+
});
|
|
329126
|
+
return;
|
|
329127
|
+
}
|
|
329128
|
+
await appiumRequest(`${sessionPath}/appium/device/press_keycode`, {
|
|
329129
|
+
method: "POST",
|
|
329130
|
+
body: JSON.stringify({ keycode: 3 })
|
|
329131
|
+
});
|
|
329132
|
+
}
|
|
329133
|
+
|
|
329134
|
+
// src/services/appium-executor.ts
|
|
329135
|
+
var import_node_child_process2 = require("child_process");
|
|
329136
|
+
var import_node_util2 = require("util");
|
|
329137
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
329138
|
+
|
|
328888
329139
|
// src/services/device-discovery.ts
|
|
328889
329140
|
var import_child_process2 = require("child_process");
|
|
328890
329141
|
var import_os2 = require("os");
|
|
@@ -330047,7 +330298,7 @@ async function wdRequest(path, options) {
|
|
|
330047
330298
|
}
|
|
330048
330299
|
return res.json();
|
|
330049
330300
|
}
|
|
330050
|
-
async function
|
|
330301
|
+
async function getWindowSize2(sessionId, retries = 3) {
|
|
330051
330302
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
330052
330303
|
try {
|
|
330053
330304
|
const result = await wdRequest(`/session/${sessionId}/window/rect`);
|
|
@@ -330223,6 +330474,26 @@ async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
|
330223
330474
|
log2(` Fresh launch: activate failed (${err instanceof Error ? err.message : String(err)})`);
|
|
330224
330475
|
}
|
|
330225
330476
|
}
|
|
330477
|
+
async function waitForColdBootContent(sessionId, log2, timeoutMs = 2e4) {
|
|
330478
|
+
const deadline = Date.now() + timeoutMs;
|
|
330479
|
+
const MIN_ELEMENTS = 3;
|
|
330480
|
+
let lastCount = 0;
|
|
330481
|
+
while (Date.now() < deadline) {
|
|
330482
|
+
try {
|
|
330483
|
+
const res = await wdRequest(`/session/${sessionId}/source`);
|
|
330484
|
+
const xml = typeof res?.value === "string" ? res.value : "";
|
|
330485
|
+
const count = (xml.match(/<(android\.|XCUIElementType)/g) ?? []).length;
|
|
330486
|
+
lastCount = count;
|
|
330487
|
+
if (count >= MIN_ELEMENTS) {
|
|
330488
|
+
log2(` Cold-boot ready: UI tree has ${count} node(s).`);
|
|
330489
|
+
return;
|
|
330490
|
+
}
|
|
330491
|
+
} catch {
|
|
330492
|
+
}
|
|
330493
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
330494
|
+
}
|
|
330495
|
+
log2(` Cold-boot wait timed out after ${timeoutMs}ms (last tree had ${lastCount} node(s)) \u2014 proceeding anyway.`);
|
|
330496
|
+
}
|
|
330226
330497
|
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
330227
330498
|
let boundary = null;
|
|
330228
330499
|
try {
|
|
@@ -330470,7 +330741,7 @@ async function tapCoordinates(sessionId, bounds) {
|
|
|
330470
330741
|
})
|
|
330471
330742
|
});
|
|
330472
330743
|
}
|
|
330473
|
-
async function
|
|
330744
|
+
async function getActiveElementId2(sessionId) {
|
|
330474
330745
|
try {
|
|
330475
330746
|
const activeEl = await wdRequest(`/session/${sessionId}/element/active`);
|
|
330476
330747
|
return activeEl?.value?.ELEMENT || activeEl?.value?.["element-6066-11e4-a52e-4f735466cecf"] || null;
|
|
@@ -330556,7 +330827,7 @@ async function executeType(sessionId, step, context) {
|
|
|
330556
330827
|
await tapCoordinates(sessionId, bounds);
|
|
330557
330828
|
tappedBounds = true;
|
|
330558
330829
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330559
|
-
elementId = await
|
|
330830
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330560
330831
|
if (elementId) {
|
|
330561
330832
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
330562
330833
|
if (!lastInputError) return;
|
|
@@ -330578,10 +330849,10 @@ async function executeType(sessionId, step, context) {
|
|
|
330578
330849
|
await tapCoordinates(sessionId, bounds);
|
|
330579
330850
|
tappedBounds = true;
|
|
330580
330851
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330581
|
-
elementId = await
|
|
330852
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330582
330853
|
}
|
|
330583
330854
|
if (!elementId) {
|
|
330584
|
-
elementId = await
|
|
330855
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330585
330856
|
}
|
|
330586
330857
|
if (elementId) {
|
|
330587
330858
|
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
@@ -330631,10 +330902,10 @@ async function executeClear(sessionId, step, platform3, isCrossPlatform = false)
|
|
|
330631
330902
|
if (!elementId && bounds) {
|
|
330632
330903
|
await tapCoordinates(sessionId, bounds);
|
|
330633
330904
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
330634
|
-
elementId = await
|
|
330905
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330635
330906
|
}
|
|
330636
330907
|
if (!elementId) {
|
|
330637
|
-
elementId = await
|
|
330908
|
+
elementId = await getActiveElementId2(sessionId);
|
|
330638
330909
|
}
|
|
330639
330910
|
if (!elementId) {
|
|
330640
330911
|
throw new Error("CLEAR step could not resolve a target or focused element");
|
|
@@ -330650,7 +330921,7 @@ async function executeSwipe(sessionId, step) {
|
|
|
330650
330921
|
let screenW;
|
|
330651
330922
|
let screenH;
|
|
330652
330923
|
try {
|
|
330653
|
-
const windowSize = await
|
|
330924
|
+
const windowSize = await getWindowSize2(sessionId);
|
|
330654
330925
|
screenW = windowSize.width;
|
|
330655
330926
|
screenH = windowSize.height;
|
|
330656
330927
|
} catch (err) {
|
|
@@ -330865,8 +331136,25 @@ async function executeMobileTest(options) {
|
|
|
330865
331136
|
}
|
|
330866
331137
|
}
|
|
330867
331138
|
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
331139
|
+
const launchStepForReset = runSteps.find((s) => s.action === "launchApp");
|
|
331140
|
+
const recordedFromClearedState = launchStepForReset?.metadata?.appReset === "cleared";
|
|
331141
|
+
if (recordedFromClearedState) {
|
|
331142
|
+
if (shouldResetMobileAppData(options.target.platform, selectedDevice.id)) {
|
|
331143
|
+
try {
|
|
331144
|
+
await executeMobileScript(sessionId, "mobile: clearApp", [{ appId: options.target.appId }]);
|
|
331145
|
+
log2("App data cleared before launch (test was recorded from a cold logged-out state).");
|
|
331146
|
+
} catch (err) {
|
|
331147
|
+
log2(`App-data clear failed (continuing with plain fresh launch): ${err instanceof Error ? err.message : String(err)}`);
|
|
331148
|
+
}
|
|
331149
|
+
} else {
|
|
331150
|
+
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.");
|
|
331151
|
+
}
|
|
331152
|
+
}
|
|
330868
331153
|
log2("Opening a fresh app instance for the test...");
|
|
330869
331154
|
await freshLaunchTargetApp(sessionId, options.target.platform, options.target.appId, log2);
|
|
331155
|
+
if (recordedFromClearedState) {
|
|
331156
|
+
await waitForColdBootContent(sessionId, log2);
|
|
331157
|
+
}
|
|
330870
331158
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
330871
331159
|
try {
|
|
330872
331160
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -331381,225 +331669,6 @@ var import_os3 = require("os");
|
|
|
331381
331669
|
var import_path2 = require("path");
|
|
331382
331670
|
var import_fs2 = require("fs");
|
|
331383
331671
|
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
331672
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
331604
331673
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
331605
331674
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
@@ -331693,7 +331762,7 @@ async function getPageSource2() {
|
|
|
331693
331762
|
async function getWindowSize3() {
|
|
331694
331763
|
if (!state.appiumSessionId) return null;
|
|
331695
331764
|
validateSessionId(state.appiumSessionId);
|
|
331696
|
-
return
|
|
331765
|
+
return getWindowSize(`/session/${state.appiumSessionId}`);
|
|
331697
331766
|
}
|
|
331698
331767
|
var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
|
|
331699
331768
|
var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
|
|
@@ -332757,6 +332826,7 @@ var MobileBridgeDriver = class {
|
|
|
332757
332826
|
sessionId;
|
|
332758
332827
|
platform;
|
|
332759
332828
|
appId;
|
|
332829
|
+
allowAppReset;
|
|
332760
332830
|
constructor(config) {
|
|
332761
332831
|
if (!APPIUM_SESSION_ID_PATTERN2.test(config.sessionId)) {
|
|
332762
332832
|
throw new Error("MobileBridgeDriver: invalid Appium session id");
|
|
@@ -332770,6 +332840,7 @@ var MobileBridgeDriver = class {
|
|
|
332770
332840
|
this.sessionId = config.sessionId;
|
|
332771
332841
|
this.platform = config.platform;
|
|
332772
332842
|
this.appId = config.appId;
|
|
332843
|
+
this.allowAppReset = config.allowAppReset === true;
|
|
332773
332844
|
}
|
|
332774
332845
|
/** WebDriver session path (`/session/<id>`), the prefix every primitive needs. */
|
|
332775
332846
|
get sessionPath() {
|
|
@@ -332831,7 +332902,7 @@ var MobileBridgeDriver = class {
|
|
|
332831
332902
|
const [xml, screenshot, windowSize] = await Promise.all([
|
|
332832
332903
|
getPageSource(this.sessionPath),
|
|
332833
332904
|
getScreenshot(this.sessionPath),
|
|
332834
|
-
|
|
332905
|
+
getWindowSize(this.sessionPath)
|
|
332835
332906
|
]);
|
|
332836
332907
|
const source = xml ?? "";
|
|
332837
332908
|
const screenSignal = await this.enrichScreenSignal(source);
|
|
@@ -332844,7 +332915,7 @@ var MobileBridgeDriver = class {
|
|
|
332844
332915
|
}
|
|
332845
332916
|
/** Device window size; null when unreadable (swipe geometry degrades gracefully). */
|
|
332846
332917
|
async windowSize() {
|
|
332847
|
-
return
|
|
332918
|
+
return getWindowSize(this.sessionPath);
|
|
332848
332919
|
}
|
|
332849
332920
|
// ── Action result wrapper ──────────────────────────────
|
|
332850
332921
|
/**
|
|
@@ -332886,7 +332957,7 @@ var MobileBridgeDriver = class {
|
|
|
332886
332957
|
* hard error so we never fire an off-screen gesture.
|
|
332887
332958
|
*/
|
|
332888
332959
|
async swipe(direction, distance) {
|
|
332889
|
-
const size = await
|
|
332960
|
+
const size = await getWindowSize(this.sessionPath);
|
|
332890
332961
|
if (!size) {
|
|
332891
332962
|
return { ok: false, error: "Could not determine device screen dimensions for swipe." };
|
|
332892
332963
|
}
|
|
@@ -332911,6 +332982,22 @@ var MobileBridgeDriver = class {
|
|
|
332911
332982
|
await this.executeScript("mobile: terminateApp", [args]);
|
|
332912
332983
|
await this.executeScript("mobile: activateApp", [args]);
|
|
332913
332984
|
}
|
|
332985
|
+
/**
|
|
332986
|
+
* Deterministic state reset: clear app data + relaunch (Android emulators
|
|
332987
|
+
* only, per the runner-computed allowAppReset policy). Returns false when
|
|
332988
|
+
* unsupported/disallowed or when clearApp throws — callers fall back to
|
|
332989
|
+
* relaunch(), preserving today's behavior.
|
|
332990
|
+
*/
|
|
332991
|
+
async resetAppState() {
|
|
332992
|
+
if (!this.allowAppReset || this.platform !== "ANDROID") return false;
|
|
332993
|
+
try {
|
|
332994
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
332995
|
+
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
332996
|
+
return true;
|
|
332997
|
+
} catch {
|
|
332998
|
+
return false;
|
|
332999
|
+
}
|
|
333000
|
+
}
|
|
332914
333001
|
/** (Re)launch the target app (activateApp) from a backgrounded/closed state. */
|
|
332915
333002
|
async launchApp() {
|
|
332916
333003
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
@@ -335052,7 +335139,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
335052
335139
|
const driver = new MobileBridgeDriver({
|
|
335053
335140
|
sessionId: session.sessionId,
|
|
335054
335141
|
platform: platform3,
|
|
335055
|
-
appId: session.appId
|
|
335142
|
+
appId: session.appId,
|
|
335143
|
+
// Deterministic-state authorization: Android emulators only by
|
|
335144
|
+
// default (VALIDATEQA_MOBILE_APP_RESET overrides) — a personal
|
|
335145
|
+
// physical device must never have its app data wiped.
|
|
335146
|
+
allowAppReset: shouldResetMobileAppData(platform3, session.deviceId)
|
|
335056
335147
|
});
|
|
335057
335148
|
discoveryResult = await (0, import_runner_core4.runMobileDiscoveryOnRunner)(ctx, driver, {
|
|
335058
335149
|
onLog: mobileOnLog,
|