@validate.qa/runner 1.0.5 → 1.0.6
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 +462 -147
- package/dist/cli.mjs +462 -147
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1042,6 +1042,65 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1042
1042
|
}
|
|
1043
1043
|
});
|
|
1044
1044
|
|
|
1045
|
+
// ../runner-core/dist/services/mobile/mobile-boundary.js
|
|
1046
|
+
var require_mobile_boundary = __commonJS({
|
|
1047
|
+
"../runner-core/dist/services/mobile/mobile-boundary.js"(exports) {
|
|
1048
|
+
"use strict";
|
|
1049
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1050
|
+
exports.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
|
|
1051
|
+
exports.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
|
|
1052
|
+
exports.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
|
|
1053
|
+
exports.isTargetLikeMobileBoundary = isTargetLikeMobileBoundary2;
|
|
1054
|
+
exports.mobileBoundaryNote = mobileBoundaryNote;
|
|
1055
|
+
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1056
|
+
"com.android.permissioncontroller",
|
|
1057
|
+
"com.google.android.permissioncontroller",
|
|
1058
|
+
"com.android.packageinstaller",
|
|
1059
|
+
"com.google.android.packageinstaller"
|
|
1060
|
+
]);
|
|
1061
|
+
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1062
|
+
"com.apple.springboard"
|
|
1063
|
+
]);
|
|
1064
|
+
function mobileOwnerFromScreenSignal(signal, targetAppId) {
|
|
1065
|
+
const bundleId = signal.bundleId?.trim();
|
|
1066
|
+
if (bundleId)
|
|
1067
|
+
return bundleId;
|
|
1068
|
+
const activity = signal.activity?.trim();
|
|
1069
|
+
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
1070
|
+
return targetAppId;
|
|
1071
|
+
}
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
function isTransientMobileSystemOwner(owner, platform3) {
|
|
1075
|
+
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
1076
|
+
}
|
|
1077
|
+
function classifyMobileScreenBoundary2(signal, targetAppId, platform3) {
|
|
1078
|
+
if (!targetAppId)
|
|
1079
|
+
return { kind: "target" };
|
|
1080
|
+
const owner = mobileOwnerFromScreenSignal(signal, targetAppId);
|
|
1081
|
+
if (!owner)
|
|
1082
|
+
return { kind: "unknown" };
|
|
1083
|
+
if (owner === targetAppId)
|
|
1084
|
+
return { kind: "target" };
|
|
1085
|
+
if (isTransientMobileSystemOwner(owner, platform3)) {
|
|
1086
|
+
return { kind: "transient_external", owner, targetAppId };
|
|
1087
|
+
}
|
|
1088
|
+
return { kind: "external", owner, targetAppId };
|
|
1089
|
+
}
|
|
1090
|
+
function isTargetLikeMobileBoundary2(boundary) {
|
|
1091
|
+
return boundary.kind === "target" || boundary.kind === "unknown";
|
|
1092
|
+
}
|
|
1093
|
+
function mobileBoundaryNote(boundary) {
|
|
1094
|
+
return [
|
|
1095
|
+
"",
|
|
1096
|
+
"## App Boundary",
|
|
1097
|
+
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
1098
|
+
boundary.kind === "transient_external" ? "This looks like a system overlay. Handle or dismiss it if needed, but do not capture it as an app screen." : "The runner excluded this external app screen from the target app evidence and relaunched the target app."
|
|
1099
|
+
].join("\n");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1045
1104
|
// ../shared/dist/types.js
|
|
1046
1105
|
function isUIVariant(value) {
|
|
1047
1106
|
return typeof value === "string" && UI_VARIANTS.includes(value);
|
|
@@ -1477,6 +1536,18 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1477
1536
|
` }`,
|
|
1478
1537
|
` throw new Error('No locator resolved. Tried: ' + JSON.stringify(selectors) + (__lastError ? ' (last error: ' + String(__lastError) + ')' : ''));`,
|
|
1479
1538
|
`}`,
|
|
1539
|
+
``,
|
|
1540
|
+
`async function __tapAt(driver: WebdriverIO.Browser, x: number, y: number) {`,
|
|
1541
|
+
` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x, y }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`,
|
|
1542
|
+
`}`,
|
|
1543
|
+
``,
|
|
1544
|
+
`async function __typeIntoFocused(driver: WebdriverIO.Browser, text: string) {`,
|
|
1545
|
+
` try {`,
|
|
1546
|
+
` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).addValue(text);`,
|
|
1547
|
+
` } catch {`,
|
|
1548
|
+
` await driver.keys(text);`,
|
|
1549
|
+
` }`,
|
|
1550
|
+
`}`,
|
|
1480
1551
|
...hasSwipe ? [
|
|
1481
1552
|
``,
|
|
1482
1553
|
`// Read the device window with a few retries (matches the runtime executor).`,
|
|
@@ -1535,13 +1606,25 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1535
1606
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1536
1607
|
lines.push(` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x: ${x}, y: ${y} }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`);
|
|
1537
1608
|
} else if (step.action === "type" && hasSelector) {
|
|
1538
|
-
|
|
1609
|
+
if (bounds) {
|
|
1610
|
+
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1611
|
+
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1612
|
+
lines.push(` try {`);
|
|
1613
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1614
|
+
lines.push(` } catch {`);
|
|
1615
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1616
|
+
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1617
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1618
|
+
lines.push(` }`);
|
|
1619
|
+
} else {
|
|
1620
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1621
|
+
}
|
|
1539
1622
|
} else if (step.action === "type" && bounds) {
|
|
1540
1623
|
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1541
1624
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1542
|
-
lines.push(` await driver
|
|
1625
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1543
1626
|
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1544
|
-
lines.push(` await (
|
|
1627
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1545
1628
|
} else if (step.action === "clear" && hasSelector) {
|
|
1546
1629
|
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
|
|
1547
1630
|
} else if (step.action === "assertVisible" && hasSelector) {
|
|
@@ -4438,21 +4521,13 @@ var require_mobile_explorer = __commonJS({
|
|
|
4438
4521
|
var ai_retry_js_1 = require_ai_retry();
|
|
4439
4522
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
4440
4523
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
4524
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
4441
4525
|
var MAX_MOBILE_ITERATIONS_SURVEY = 40;
|
|
4442
4526
|
var MAX_MOBILE_ITERATIONS_DEEP = 60;
|
|
4443
4527
|
var MAX_SCREENSHOTS = 15;
|
|
4444
4528
|
var MAX_RECENT_EXCHANGES = 24;
|
|
4445
4529
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
4446
4530
|
var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
|
|
4447
|
-
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4448
|
-
"com.android.permissioncontroller",
|
|
4449
|
-
"com.google.android.permissioncontroller",
|
|
4450
|
-
"com.android.packageinstaller",
|
|
4451
|
-
"com.google.android.packageinstaller"
|
|
4452
|
-
]);
|
|
4453
|
-
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4454
|
-
"com.apple.springboard"
|
|
4455
|
-
]);
|
|
4456
4531
|
var MOBILE_SNAPSHOT_TOOL = {
|
|
4457
4532
|
type: "function",
|
|
4458
4533
|
function: {
|
|
@@ -4675,40 +4750,6 @@ var require_mobile_explorer = __commonJS({
|
|
|
4675
4750
|
return "timeout";
|
|
4676
4751
|
return "ai_error";
|
|
4677
4752
|
}
|
|
4678
|
-
function ownerFromScreenSignal(signal, targetAppId) {
|
|
4679
|
-
const bundleId = signal.bundleId?.trim();
|
|
4680
|
-
if (bundleId)
|
|
4681
|
-
return bundleId;
|
|
4682
|
-
const activity = signal.activity?.trim();
|
|
4683
|
-
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
4684
|
-
return targetAppId;
|
|
4685
|
-
}
|
|
4686
|
-
return null;
|
|
4687
|
-
}
|
|
4688
|
-
function isTransientSystemOwner(owner, platform3) {
|
|
4689
|
-
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
4690
|
-
}
|
|
4691
|
-
function classifyMobileScreenBoundary(signal, targetAppId, platform3) {
|
|
4692
|
-
if (!targetAppId)
|
|
4693
|
-
return { kind: "target" };
|
|
4694
|
-
const owner = ownerFromScreenSignal(signal, targetAppId);
|
|
4695
|
-
if (!owner)
|
|
4696
|
-
return { kind: "unknown" };
|
|
4697
|
-
if (owner === targetAppId)
|
|
4698
|
-
return { kind: "target" };
|
|
4699
|
-
if (isTransientSystemOwner(owner, platform3)) {
|
|
4700
|
-
return { kind: "transient_external", owner, targetAppId };
|
|
4701
|
-
}
|
|
4702
|
-
return { kind: "external", owner, targetAppId };
|
|
4703
|
-
}
|
|
4704
|
-
function boundaryNote(boundary) {
|
|
4705
|
-
return [
|
|
4706
|
-
"",
|
|
4707
|
-
"## App Boundary",
|
|
4708
|
-
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
4709
|
-
boundary.kind === "transient_external" ? "This looks like a system overlay. Handle or dismiss it if needed, but do not capture it as an app screen." : "The runner excluded this external app screen from the survey evidence."
|
|
4710
|
-
].join("\n");
|
|
4711
|
-
}
|
|
4712
4753
|
function resolveSnapshot(xml, windowSize, driverSignal, platform3) {
|
|
4713
4754
|
const parsed = (0, mobile_source_parser_js_1.parseMobileSource)(xml, platform3);
|
|
4714
4755
|
const signal = {
|
|
@@ -4906,10 +4947,10 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4906
4947
|
};
|
|
4907
4948
|
const currentBoundary = () => {
|
|
4908
4949
|
const current = getLatest();
|
|
4909
|
-
return current ? classifyMobileScreenBoundary(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4950
|
+
return current ? (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4910
4951
|
};
|
|
4911
4952
|
const absorbResolvedObservation = async (resolved, screenshot, iterationForObservation, source) => {
|
|
4912
|
-
const boundary = classifyMobileScreenBoundary(resolved.signal, targetAppId, platform3);
|
|
4953
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
4913
4954
|
if (boundary.kind === "target" || boundary.kind === "unknown") {
|
|
4914
4955
|
const screenId = ingestSnapshot(resolved, iterationForObservation);
|
|
4915
4956
|
captureScreenshot(screenshot, screenId);
|
|
@@ -4920,7 +4961,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4920
4961
|
if (boundary.kind === "transient_external") {
|
|
4921
4962
|
log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
|
|
4922
4963
|
return {
|
|
4923
|
-
observation: `${resolved.observation}${
|
|
4964
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
4924
4965
|
screenId: null,
|
|
4925
4966
|
transientExternal: true,
|
|
4926
4967
|
externalOwner: boundary.owner
|
|
@@ -4939,7 +4980,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4939
4980
|
externalOwner: boundary.owner
|
|
4940
4981
|
};
|
|
4941
4982
|
}
|
|
4942
|
-
const recoveredBoundary = classifyMobileScreenBoundary(recovered.signal, targetAppId, platform3);
|
|
4983
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
4943
4984
|
latest = recovered;
|
|
4944
4985
|
lastSnapshotIteration = iterationForObservation;
|
|
4945
4986
|
if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
|
|
@@ -4955,7 +4996,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4955
4996
|
if (recoveredBoundary.kind === "transient_external") {
|
|
4956
4997
|
log2(` Relaunch surfaced system overlay ${recoveredBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
4957
4998
|
return {
|
|
4958
|
-
observation: `${recovered.observation}${
|
|
4999
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
4959
5000
|
screenId: null,
|
|
4960
5001
|
externalBlocked: true,
|
|
4961
5002
|
transientExternal: true,
|
|
@@ -8599,6 +8640,7 @@ var require_mobile_generator = __commonJS({
|
|
|
8599
8640
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
8600
8641
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
8601
8642
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
8643
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
8602
8644
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
8603
8645
|
var MAX_RECENT_EXCHANGES = 24;
|
|
8604
8646
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
@@ -8784,8 +8826,9 @@ var require_mobile_generator = __commonJS({
|
|
|
8784
8826
|
function hasStableId(element) {
|
|
8785
8827
|
return !!nonEmpty2(element.accessibilityId) || !!nonEmpty2(element.resourceId);
|
|
8786
8828
|
}
|
|
8787
|
-
function buildSystemPrompt(platform3) {
|
|
8829
|
+
function buildSystemPrompt(platform3, targetAppId) {
|
|
8788
8830
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
8831
|
+
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
8789
8832
|
return `You are an expert mobile QA agent generating ONE automated test by driving a real ${platformLabel} app on a connected device. The app has just been relaunched into a clean state.
|
|
8790
8833
|
|
|
8791
8834
|
## How the loop works
|
|
@@ -8805,7 +8848,8 @@ var require_mobile_generator = __commonJS({
|
|
|
8805
8848
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
8806
8849
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
8807
8850
|
- Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
|
|
8808
|
-
- Stay inside this app.
|
|
8851
|
+
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
8852
|
+
- Finish promptly once the outcome is proven.
|
|
8809
8853
|
|
|
8810
8854
|
Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
|
|
8811
8855
|
}
|
|
@@ -8878,7 +8922,7 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8878
8922
|
}
|
|
8879
8923
|
}
|
|
8880
8924
|
async function runScenarioLoop(args) {
|
|
8881
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, log: log2 } = args;
|
|
8925
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, log: log2 } = args;
|
|
8882
8926
|
const actions = [];
|
|
8883
8927
|
let assertCount = 0;
|
|
8884
8928
|
let finish = null;
|
|
@@ -8892,14 +8936,98 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8892
8936
|
const fromActivity = signal.activity ? signal.activity.split(".").pop() : void 0;
|
|
8893
8937
|
return nonEmpty2(fromActivity) ?? nonEmpty2(signal.bundleId) ?? `screen-${signal.screenHash.slice(0, 8)}`;
|
|
8894
8938
|
};
|
|
8895
|
-
const
|
|
8896
|
-
if (!xml)
|
|
8897
|
-
return null;
|
|
8898
|
-
const resolved = resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8939
|
+
const setLatest = (resolved) => {
|
|
8899
8940
|
latest = resolved;
|
|
8900
8941
|
lastScreenName = screenNameFor(resolved);
|
|
8901
8942
|
return resolved;
|
|
8902
8943
|
};
|
|
8944
|
+
const buildResolved = (xml, driverSignal, windowSize) => {
|
|
8945
|
+
if (!xml)
|
|
8946
|
+
return null;
|
|
8947
|
+
return resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8948
|
+
};
|
|
8949
|
+
const emitScreenshot = (base64) => {
|
|
8950
|
+
if (!base64)
|
|
8951
|
+
return;
|
|
8952
|
+
try {
|
|
8953
|
+
onScreenshot?.(base64);
|
|
8954
|
+
} catch {
|
|
8955
|
+
}
|
|
8956
|
+
};
|
|
8957
|
+
const absorbResolvedObservation = async (resolved, screenshot, source) => {
|
|
8958
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
8959
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(boundary)) {
|
|
8960
|
+
setLatest(resolved);
|
|
8961
|
+
emitScreenshot(screenshot);
|
|
8962
|
+
return { resolved, observation: resolved.observation };
|
|
8963
|
+
}
|
|
8964
|
+
setLatest(resolved);
|
|
8965
|
+
if (boundary.kind === "transient_external") {
|
|
8966
|
+
log2(` system overlay detected from ${source}: ${boundary.owner}; not recording it as a target-app step.`);
|
|
8967
|
+
return {
|
|
8968
|
+
resolved,
|
|
8969
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
8970
|
+
transientExternal: true,
|
|
8971
|
+
externalOwner: boundary.owner
|
|
8972
|
+
};
|
|
8973
|
+
}
|
|
8974
|
+
log2(` external app detected from ${source}: ${boundary.owner}; rejecting the action and relaunching ${boundary.targetAppId}.`);
|
|
8975
|
+
try {
|
|
8976
|
+
await driver.relaunch();
|
|
8977
|
+
const snap = await driver.snapshot();
|
|
8978
|
+
const recovered = buildResolved(snap.xml, snap.screenSignal, snap.windowSize);
|
|
8979
|
+
if (!recovered) {
|
|
8980
|
+
return {
|
|
8981
|
+
resolved: null,
|
|
8982
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunched target app, but no UI tree was returned)`,
|
|
8983
|
+
externalBlocked: true,
|
|
8984
|
+
externalOwner: boundary.owner
|
|
8985
|
+
};
|
|
8986
|
+
}
|
|
8987
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
8988
|
+
setLatest(recovered);
|
|
8989
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(recoveredBoundary)) {
|
|
8990
|
+
emitScreenshot(snap.screenshot);
|
|
8991
|
+
return {
|
|
8992
|
+
resolved: recovered,
|
|
8993
|
+
observation: recovered.observation,
|
|
8994
|
+
externalBlocked: true,
|
|
8995
|
+
externalOwner: boundary.owner
|
|
8996
|
+
};
|
|
8997
|
+
}
|
|
8998
|
+
if (recoveredBoundary.kind === "transient_external") {
|
|
8999
|
+
log2(` relaunch surfaced system overlay ${recoveredBoundary.owner}; not recording it as a target-app step.`);
|
|
9000
|
+
return {
|
|
9001
|
+
resolved: recovered,
|
|
9002
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
9003
|
+
externalBlocked: true,
|
|
9004
|
+
transientExternal: true,
|
|
9005
|
+
externalOwner: boundary.owner
|
|
9006
|
+
};
|
|
9007
|
+
}
|
|
9008
|
+
log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
|
|
9009
|
+
return {
|
|
9010
|
+
resolved: recovered,
|
|
9011
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
|
|
9012
|
+
externalBlocked: true,
|
|
9013
|
+
externalOwner: boundary.owner
|
|
9014
|
+
};
|
|
9015
|
+
} catch (err) {
|
|
9016
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9017
|
+
return {
|
|
9018
|
+
resolved: null,
|
|
9019
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; failed to relaunch target app: ${message})`,
|
|
9020
|
+
externalBlocked: true,
|
|
9021
|
+
externalOwner: boundary.owner
|
|
9022
|
+
};
|
|
9023
|
+
}
|
|
9024
|
+
};
|
|
9025
|
+
const absorbObservation = async (xml, driverSignal, windowSize, screenshot, source) => {
|
|
9026
|
+
const resolved = buildResolved(xml, driverSignal, windowSize);
|
|
9027
|
+
if (!resolved)
|
|
9028
|
+
return { resolved: null, observation: null };
|
|
9029
|
+
return absorbResolvedObservation(resolved, screenshot, source);
|
|
9030
|
+
};
|
|
8903
9031
|
const resolveRef = (ref) => {
|
|
8904
9032
|
if (typeof ref !== "string" || !latest)
|
|
8905
9033
|
return null;
|
|
@@ -8907,11 +9035,11 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8907
9035
|
};
|
|
8908
9036
|
try {
|
|
8909
9037
|
const seed = await driver.snapshot();
|
|
8910
|
-
|
|
9038
|
+
await absorbObservation(seed.xml, seed.screenSignal, seed.windowSize, seed.screenshot, "seed snapshot");
|
|
8911
9039
|
} catch (err) {
|
|
8912
9040
|
log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
8913
9041
|
}
|
|
8914
|
-
const systemPrompt = buildSystemPrompt(platform3);
|
|
9042
|
+
const systemPrompt = buildSystemPrompt(platform3, targetAppId);
|
|
8915
9043
|
const kickoff = buildScenarioKickoff(scenario);
|
|
8916
9044
|
const recentExchanges = [];
|
|
8917
9045
|
let iteration = 0;
|
|
@@ -9022,7 +9150,7 @@ ${statePacket}` },
|
|
|
9022
9150
|
driver,
|
|
9023
9151
|
platform: platform3,
|
|
9024
9152
|
latest: getLatest,
|
|
9025
|
-
|
|
9153
|
+
absorbObservation,
|
|
9026
9154
|
resolveRef,
|
|
9027
9155
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
9028
9156
|
recordAction: (action) => {
|
|
@@ -9045,60 +9173,85 @@ ${statePacket}` },
|
|
|
9045
9173
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
9046
9174
|
}
|
|
9047
9175
|
async function dispatchGenerateTool(args) {
|
|
9048
|
-
const { toolName, toolArgs, driver, platform: platform3, latest,
|
|
9176
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, screenName, recordAction, log: log2 } = args;
|
|
9049
9177
|
const unknownRef = (ref) => ({
|
|
9050
9178
|
ok: false,
|
|
9051
9179
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
9052
9180
|
});
|
|
9181
|
+
const actionPayload = (result, absorbed) => {
|
|
9182
|
+
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
9183
|
+
return {
|
|
9184
|
+
ok: result.ok && !blocked,
|
|
9185
|
+
...result.error ? { error: result.error } : {},
|
|
9186
|
+
...blocked ? { error: "Action left the target app and was not recorded. Continue from the relaunched target app." } : {},
|
|
9187
|
+
observation: absorbed.observation ?? void 0,
|
|
9188
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9189
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9190
|
+
};
|
|
9191
|
+
};
|
|
9192
|
+
const shouldRecordAction = (result, absorbed) => result.ok && !absorbed.externalBlocked && !absorbed.transientExternal;
|
|
9053
9193
|
switch (toolName) {
|
|
9054
9194
|
case "mobile_snapshot": {
|
|
9055
9195
|
const snap = await driver.snapshot();
|
|
9056
|
-
const
|
|
9057
|
-
if (!resolved)
|
|
9196
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_snapshot");
|
|
9197
|
+
if (!absorbed.resolved)
|
|
9058
9198
|
return { ok: false, error: "snapshot returned no UI tree" };
|
|
9059
|
-
log2(` snapshot: ${resolved.screen.elements.length} elements`);
|
|
9060
|
-
return {
|
|
9199
|
+
log2(` snapshot: ${absorbed.resolved.screen.elements.length} elements`);
|
|
9200
|
+
return {
|
|
9201
|
+
observation: absorbed.observation,
|
|
9202
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9203
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9204
|
+
};
|
|
9061
9205
|
}
|
|
9062
9206
|
case "mobile_tap": {
|
|
9063
9207
|
const element = resolveRef(toolArgs.ref);
|
|
9064
9208
|
if (!element)
|
|
9065
9209
|
return unknownRef(toolArgs.ref);
|
|
9210
|
+
const beforeScreenName = screenName();
|
|
9066
9211
|
const result = await driver.tap(element.bounds);
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9212
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
9213
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9214
|
+
recordAction({
|
|
9215
|
+
type: "TAP",
|
|
9216
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9217
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9218
|
+
});
|
|
9219
|
+
}
|
|
9220
|
+
return actionPayload(result, absorbed);
|
|
9074
9221
|
}
|
|
9075
9222
|
case "mobile_type": {
|
|
9076
9223
|
const element = resolveRef(toolArgs.ref);
|
|
9077
9224
|
if (!element)
|
|
9078
9225
|
return unknownRef(toolArgs.ref);
|
|
9079
9226
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
9227
|
+
const beforeScreenName = screenName();
|
|
9080
9228
|
const result = await driver.type(value, element.bounds);
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9229
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_type");
|
|
9230
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9231
|
+
recordAction({
|
|
9232
|
+
type: "TYPE",
|
|
9233
|
+
value,
|
|
9234
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9235
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9236
|
+
});
|
|
9237
|
+
}
|
|
9238
|
+
return actionPayload(result, absorbed);
|
|
9089
9239
|
}
|
|
9090
9240
|
case "mobile_clear": {
|
|
9091
9241
|
const element = resolveRef(toolArgs.ref);
|
|
9092
9242
|
if (!element)
|
|
9093
9243
|
return unknownRef(toolArgs.ref);
|
|
9244
|
+
const beforeScreenName = screenName();
|
|
9094
9245
|
const result = await driver.clear(element.bounds);
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9246
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_clear");
|
|
9247
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9248
|
+
recordAction({
|
|
9249
|
+
type: "CLEAR",
|
|
9250
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9251
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9252
|
+
});
|
|
9253
|
+
}
|
|
9254
|
+
return actionPayload(result, absorbed);
|
|
9102
9255
|
}
|
|
9103
9256
|
case "mobile_swipe": {
|
|
9104
9257
|
const direction = toolArgs.direction;
|
|
@@ -9106,30 +9259,42 @@ ${statePacket}` },
|
|
|
9106
9259
|
return { ok: false, error: "direction must be one of up|down|left|right" };
|
|
9107
9260
|
}
|
|
9108
9261
|
const distance = typeof toolArgs.distance === "number" ? toolArgs.distance : void 0;
|
|
9262
|
+
const beforeScreenName = screenName();
|
|
9109
9263
|
const result = await driver.swipe(direction, distance);
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9264
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_swipe");
|
|
9265
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9266
|
+
recordAction({
|
|
9267
|
+
type: "SWIPE",
|
|
9268
|
+
metadata: {
|
|
9269
|
+
screenName: beforeScreenName,
|
|
9270
|
+
direction,
|
|
9271
|
+
...distance !== void 0 ? { distance } : {}
|
|
9272
|
+
}
|
|
9273
|
+
});
|
|
9274
|
+
}
|
|
9275
|
+
return actionPayload(result, absorbed);
|
|
9120
9276
|
}
|
|
9121
9277
|
case "mobile_back": {
|
|
9278
|
+
const beforeScreenName = screenName();
|
|
9122
9279
|
const result = await driver.back();
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9280
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_back");
|
|
9281
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9282
|
+
recordAction({ type: "BACK", metadata: { screenName: beforeScreenName } });
|
|
9283
|
+
}
|
|
9284
|
+
return actionPayload(result, absorbed);
|
|
9126
9285
|
}
|
|
9127
9286
|
case "mobile_relaunch": {
|
|
9128
9287
|
await driver.relaunch();
|
|
9129
9288
|
const snap = await driver.snapshot();
|
|
9130
|
-
const
|
|
9289
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
|
|
9131
9290
|
log2(" relaunched app for hermetic reset.");
|
|
9132
|
-
return {
|
|
9291
|
+
return {
|
|
9292
|
+
ok: true,
|
|
9293
|
+
observation: absorbed.observation,
|
|
9294
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9295
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9296
|
+
note: "App relaunched (clean state). Not recorded as a step."
|
|
9297
|
+
};
|
|
9133
9298
|
}
|
|
9134
9299
|
case "mobile_assert_visible": {
|
|
9135
9300
|
const recorded = recordAssertVisible(toolArgs, { resolveRef, screenName, platform: platform3, recordAction });
|
|
@@ -9267,7 +9432,7 @@ ${statePacket}` },
|
|
|
9267
9432
|
return test;
|
|
9268
9433
|
}
|
|
9269
9434
|
async function runMobileGeneratePhase(args) {
|
|
9270
|
-
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated } = args;
|
|
9435
|
+
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
|
|
9271
9436
|
const log2 = (line) => {
|
|
9272
9437
|
try {
|
|
9273
9438
|
onLog?.(line);
|
|
@@ -9281,6 +9446,7 @@ ${statePacket}` },
|
|
|
9281
9446
|
const provider = (0, ai_provider_js_1.getProviderForModel)(agentModel);
|
|
9282
9447
|
const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
|
|
9283
9448
|
const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
9449
|
+
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
9284
9450
|
log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
|
|
9285
9451
|
const tests = [];
|
|
9286
9452
|
for (let i = 0; i < scenarios.length; i++) {
|
|
@@ -9299,6 +9465,8 @@ ${statePacket}` },
|
|
|
9299
9465
|
aiClient,
|
|
9300
9466
|
auditGroupId,
|
|
9301
9467
|
onAICall,
|
|
9468
|
+
onScreenshot,
|
|
9469
|
+
targetAppId,
|
|
9302
9470
|
log: log2
|
|
9303
9471
|
});
|
|
9304
9472
|
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
@@ -311428,7 +311596,9 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311428
311596
|
return {
|
|
311429
311597
|
collectedPages: explore.collectedPages,
|
|
311430
311598
|
collectedTransitions,
|
|
311431
|
-
|
|
311599
|
+
// Native screenshots are multi-megabyte PNGs. The live mirror streams them
|
|
311600
|
+
// through heartbeat telemetry; discovery checkpoints keep the screen graph
|
|
311601
|
+
// and transitions so large Android surveys cannot exceed server body limits.
|
|
311432
311602
|
exploreStats: explore.exploreStats,
|
|
311433
311603
|
healthObservations: explore.healthObservations
|
|
311434
311604
|
};
|
|
@@ -311586,6 +311756,7 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311586
311756
|
ctx,
|
|
311587
311757
|
onLog: (line) => log2(line),
|
|
311588
311758
|
onAICall,
|
|
311759
|
+
onScreenshot,
|
|
311589
311760
|
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
|
|
311590
311761
|
});
|
|
311591
311762
|
const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -325291,7 +325462,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
325291
325462
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
325292
325463
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
325293
325464
|
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
325294
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "
|
|
325465
|
+
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
325295
325466
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
325296
325467
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
325297
325468
|
var LiveBrowserRegistry = class {
|
|
@@ -325787,6 +325958,7 @@ var require_dist2 = __commonJS({
|
|
|
325787
325958
|
__exportStar(require_ai_provider(), exports);
|
|
325788
325959
|
__exportStar(require_mobile_types(), exports);
|
|
325789
325960
|
__exportStar(require_mobile_source_parser(), exports);
|
|
325961
|
+
__exportStar(require_mobile_boundary(), exports);
|
|
325790
325962
|
var mobile_explorer_js_1 = require_mobile_explorer();
|
|
325791
325963
|
Object.defineProperty(exports, "runMobileExplorePhase", { enumerable: true, get: function() {
|
|
325792
325964
|
return mobile_explorer_js_1.runMobileExplorePhase;
|
|
@@ -326119,7 +326291,7 @@ var require_package4 = __commonJS({
|
|
|
326119
326291
|
"package.json"(exports, module) {
|
|
326120
326292
|
module.exports = {
|
|
326121
326293
|
name: "@validate.qa/runner",
|
|
326122
|
-
version: "1.0.
|
|
326294
|
+
version: "1.0.6",
|
|
326123
326295
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326124
326296
|
bin: {
|
|
326125
326297
|
"validate-runner": "dist/cli.js"
|
|
@@ -326217,7 +326389,7 @@ var import_runner_core = __toESM(require_dist2());
|
|
|
326217
326389
|
var import_runner_core2 = __toESM(require_dist2());
|
|
326218
326390
|
|
|
326219
326391
|
// src/runner.ts
|
|
326220
|
-
var
|
|
326392
|
+
var import_runner_core10 = __toESM(require_dist2());
|
|
326221
326393
|
|
|
326222
326394
|
// src/services/regression-runner.ts
|
|
326223
326395
|
var import_runner_core3 = __toESM(require_dist2());
|
|
@@ -326232,7 +326404,7 @@ var import_runner_core5 = __toESM(require_dist2());
|
|
|
326232
326404
|
var import_runner_core6 = __toESM(require_dist2());
|
|
326233
326405
|
|
|
326234
326406
|
// src/runner.ts
|
|
326235
|
-
var
|
|
326407
|
+
var import_runner_core11 = __toESM(require_dist2());
|
|
326236
326408
|
|
|
326237
326409
|
// src/services/auth-state.ts
|
|
326238
326410
|
var INLINE_LOGIN_TAG = "@inline-login";
|
|
@@ -326252,6 +326424,7 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
326252
326424
|
|
|
326253
326425
|
// src/services/appium-executor.ts
|
|
326254
326426
|
init_dist();
|
|
326427
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
326255
326428
|
|
|
326256
326429
|
// src/services/appium-lifecycle.ts
|
|
326257
326430
|
import { spawn, execFileSync } from "child_process";
|
|
@@ -327543,6 +327716,86 @@ async function openDeepLink(sessionId, platform3, deeplink, appId) {
|
|
|
327543
327716
|
body: JSON.stringify({ script: "mobile: deepLink", args })
|
|
327544
327717
|
});
|
|
327545
327718
|
}
|
|
327719
|
+
function readExecuteStringValue(result) {
|
|
327720
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327721
|
+
const value = result.value;
|
|
327722
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
327723
|
+
}
|
|
327724
|
+
return void 0;
|
|
327725
|
+
}
|
|
327726
|
+
function readActiveBundleId(result) {
|
|
327727
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327728
|
+
const value = result.value;
|
|
327729
|
+
if (value && typeof value === "object" && "bundleId" in value) {
|
|
327730
|
+
const bundleId = value.bundleId;
|
|
327731
|
+
if (typeof bundleId === "string" && bundleId.trim()) return bundleId.trim();
|
|
327732
|
+
}
|
|
327733
|
+
}
|
|
327734
|
+
return void 0;
|
|
327735
|
+
}
|
|
327736
|
+
function normalizeAndroidActivity(activity, pkg2) {
|
|
327737
|
+
const trimmed = activity?.trim();
|
|
327738
|
+
if (!trimmed) return void 0;
|
|
327739
|
+
const packageName = pkg2?.trim();
|
|
327740
|
+
if (!packageName) return trimmed;
|
|
327741
|
+
if (trimmed.startsWith(".")) return `${packageName}${trimmed}`;
|
|
327742
|
+
if (!trimmed.includes(".")) return `${packageName}.${trimmed}`;
|
|
327743
|
+
return trimmed;
|
|
327744
|
+
}
|
|
327745
|
+
async function executeMobileScript(sessionId, script, args) {
|
|
327746
|
+
return wdRequest(`/session/${sessionId}/execute/sync`, {
|
|
327747
|
+
method: "POST",
|
|
327748
|
+
body: JSON.stringify({ script, args })
|
|
327749
|
+
});
|
|
327750
|
+
}
|
|
327751
|
+
async function readForegroundScreenSignal(sessionId, platform3) {
|
|
327752
|
+
if (platform3 === "ANDROID") {
|
|
327753
|
+
const [packageResult, activityResult] = await Promise.all([
|
|
327754
|
+
executeMobileScript(sessionId, "mobile: getCurrentPackage", []),
|
|
327755
|
+
executeMobileScript(sessionId, "mobile: getCurrentActivity", []).catch(() => void 0)
|
|
327756
|
+
]);
|
|
327757
|
+
const pkg2 = readExecuteStringValue(packageResult);
|
|
327758
|
+
if (!pkg2) return null;
|
|
327759
|
+
const activity = normalizeAndroidActivity(readExecuteStringValue(activityResult), pkg2);
|
|
327760
|
+
return {
|
|
327761
|
+
platform: platform3,
|
|
327762
|
+
bundleId: pkg2,
|
|
327763
|
+
...activity ? { activity } : {},
|
|
327764
|
+
screenHash: "foreground"
|
|
327765
|
+
};
|
|
327766
|
+
}
|
|
327767
|
+
const infoResult = await executeMobileScript(sessionId, "mobile: activeAppInfo", []);
|
|
327768
|
+
const bundleId = readActiveBundleId(infoResult);
|
|
327769
|
+
if (!bundleId) return null;
|
|
327770
|
+
return { platform: platform3, bundleId, screenHash: "foreground" };
|
|
327771
|
+
}
|
|
327772
|
+
async function activateTargetApp(sessionId, platform3, appId) {
|
|
327773
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
327774
|
+
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
327775
|
+
}
|
|
327776
|
+
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
327777
|
+
let boundary = null;
|
|
327778
|
+
try {
|
|
327779
|
+
const signal = await readForegroundScreenSignal(sessionId, target.platform);
|
|
327780
|
+
boundary = signal ? (0, import_runner_core8.classifyMobileScreenBoundary)(signal, target.appId, target.platform) : null;
|
|
327781
|
+
} catch (err) {
|
|
327782
|
+
log2(` App boundary check skipped after ${source}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327783
|
+
return;
|
|
327784
|
+
}
|
|
327785
|
+
if (!boundary || (0, import_runner_core8.isTargetLikeMobileBoundary)(boundary) || boundary.kind === "transient_external") {
|
|
327786
|
+
return;
|
|
327787
|
+
}
|
|
327788
|
+
const message = `Foreground app changed to ${boundary.owner}, outside target app ${boundary.targetAppId}`;
|
|
327789
|
+
log2(` ${message}; activating target app.`);
|
|
327790
|
+
try {
|
|
327791
|
+
await activateTargetApp(sessionId, target.platform, target.appId);
|
|
327792
|
+
} catch (err) {
|
|
327793
|
+
log2(` Could not reactivate ${target.appId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327794
|
+
}
|
|
327795
|
+
if (failOnExternal) {
|
|
327796
|
+
throw new Error(`${message}; relaunched target app and failed the step so mobile tests do not continue in another app.`);
|
|
327797
|
+
}
|
|
327798
|
+
}
|
|
327546
327799
|
async function deleteSession(sessionId) {
|
|
327547
327800
|
try {
|
|
327548
327801
|
await wdRequest(`/session/${sessionId}`, { method: "DELETE" });
|
|
@@ -327669,6 +327922,29 @@ async function getActiveElementId(sessionId) {
|
|
|
327669
327922
|
return null;
|
|
327670
327923
|
}
|
|
327671
327924
|
}
|
|
327925
|
+
async function sendKeysToFocusedInput(sessionId, value) {
|
|
327926
|
+
await wdRequest(`/session/${sessionId}/keys`, {
|
|
327927
|
+
method: "POST",
|
|
327928
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
327929
|
+
});
|
|
327930
|
+
}
|
|
327931
|
+
function shouldAbortAfterStepFailure(action) {
|
|
327932
|
+
switch (action) {
|
|
327933
|
+
case "launchApp":
|
|
327934
|
+
case "tap":
|
|
327935
|
+
case "type":
|
|
327936
|
+
case "clear":
|
|
327937
|
+
case "swipe":
|
|
327938
|
+
case "scroll":
|
|
327939
|
+
case "back":
|
|
327940
|
+
case "home":
|
|
327941
|
+
case "waitForElement":
|
|
327942
|
+
return true;
|
|
327943
|
+
case "assertVisible":
|
|
327944
|
+
case "assertText":
|
|
327945
|
+
return false;
|
|
327946
|
+
}
|
|
327947
|
+
}
|
|
327672
327948
|
async function executeTap(sessionId, step) {
|
|
327673
327949
|
const bounds = boundsFromStep(step);
|
|
327674
327950
|
if (step.target) {
|
|
@@ -327690,8 +327966,10 @@ async function executeTap(sessionId, step) {
|
|
|
327690
327966
|
}
|
|
327691
327967
|
async function executeType(sessionId, step) {
|
|
327692
327968
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
327969
|
+
const value = String(step.value);
|
|
327693
327970
|
const bounds = boundsFromStep(step);
|
|
327694
327971
|
let elementId = null;
|
|
327972
|
+
let tappedBounds = false;
|
|
327695
327973
|
if (step.target) {
|
|
327696
327974
|
try {
|
|
327697
327975
|
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
@@ -327701,19 +327979,33 @@ async function executeType(sessionId, step) {
|
|
|
327701
327979
|
}
|
|
327702
327980
|
if (!elementId && bounds) {
|
|
327703
327981
|
await tapCoordinates(sessionId, bounds);
|
|
327982
|
+
tappedBounds = true;
|
|
327704
327983
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327705
327984
|
elementId = await getActiveElementId(sessionId);
|
|
327706
327985
|
}
|
|
327707
327986
|
if (!elementId) {
|
|
327708
327987
|
elementId = await getActiveElementId(sessionId);
|
|
327709
327988
|
}
|
|
327710
|
-
if (
|
|
327711
|
-
|
|
327989
|
+
if (elementId) {
|
|
327990
|
+
try {
|
|
327991
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
|
|
327992
|
+
method: "POST",
|
|
327993
|
+
body: JSON.stringify({ text: value })
|
|
327994
|
+
});
|
|
327995
|
+
return;
|
|
327996
|
+
} catch (error2) {
|
|
327997
|
+
if (!bounds) throw error2;
|
|
327998
|
+
}
|
|
327712
327999
|
}
|
|
327713
|
-
|
|
327714
|
-
|
|
327715
|
-
|
|
327716
|
-
|
|
328000
|
+
if (bounds) {
|
|
328001
|
+
if (!tappedBounds) {
|
|
328002
|
+
await tapCoordinates(sessionId, bounds);
|
|
328003
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328004
|
+
}
|
|
328005
|
+
await sendKeysToFocusedInput(sessionId, value);
|
|
328006
|
+
return;
|
|
328007
|
+
}
|
|
328008
|
+
throw new Error("TYPE step could not resolve a target or focused element");
|
|
327717
328009
|
}
|
|
327718
328010
|
async function executeClear(sessionId, step) {
|
|
327719
328011
|
const bounds = boundsFromStep(step);
|
|
@@ -327917,6 +328209,7 @@ async function executeMobileTest(options) {
|
|
|
327917
328209
|
log2(` \u2717 Deep link failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
327918
328210
|
}
|
|
327919
328211
|
}
|
|
328212
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
327920
328213
|
let environmental = null;
|
|
327921
328214
|
for (const step of options.steps) {
|
|
327922
328215
|
const stepStart = Date.now();
|
|
@@ -327924,6 +328217,7 @@ async function executeMobileTest(options) {
|
|
|
327924
328217
|
let status = "passed";
|
|
327925
328218
|
let stepError;
|
|
327926
328219
|
try {
|
|
328220
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} pre-check`, false);
|
|
327927
328221
|
switch (step.action) {
|
|
327928
328222
|
case "launchApp":
|
|
327929
328223
|
log2(" App launched (handled by session creation)");
|
|
@@ -327963,6 +328257,7 @@ async function executeMobileTest(options) {
|
|
|
327963
328257
|
break;
|
|
327964
328258
|
}
|
|
327965
328259
|
}
|
|
328260
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
|
|
327966
328261
|
} catch (err) {
|
|
327967
328262
|
status = "failed";
|
|
327968
328263
|
stepError = err instanceof Error ? err.message : String(err);
|
|
@@ -327983,6 +328278,10 @@ async function executeMobileTest(options) {
|
|
|
327983
328278
|
log2(`Aborting remaining steps \u2014 Appium session is no longer usable: ${environmental}`);
|
|
327984
328279
|
break;
|
|
327985
328280
|
}
|
|
328281
|
+
if (status === "failed" && shouldAbortAfterStepFailure(step.action)) {
|
|
328282
|
+
log2(`Aborting remaining steps \u2014 ${step.action} failed, so downstream mobile steps are no longer reliable.`);
|
|
328283
|
+
break;
|
|
328284
|
+
}
|
|
327986
328285
|
}
|
|
327987
328286
|
const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
327988
328287
|
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
|
|
@@ -329248,7 +329547,7 @@ function detachMirrorSession(sessionId) {
|
|
|
329248
329547
|
}
|
|
329249
329548
|
|
|
329250
329549
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
329251
|
-
var
|
|
329550
|
+
var import_runner_core9 = __toESM(require_dist2());
|
|
329252
329551
|
init_dist();
|
|
329253
329552
|
var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
|
|
329254
329553
|
function readStringValue(result) {
|
|
@@ -329258,7 +329557,7 @@ function readStringValue(result) {
|
|
|
329258
329557
|
}
|
|
329259
329558
|
return void 0;
|
|
329260
329559
|
}
|
|
329261
|
-
function
|
|
329560
|
+
function normalizeAndroidActivity2(activity, pkg2) {
|
|
329262
329561
|
const trimmed = activity?.trim();
|
|
329263
329562
|
if (!trimmed) return void 0;
|
|
329264
329563
|
const packageName = pkg2?.trim();
|
|
@@ -329294,7 +329593,7 @@ var MobileBridgeDriver = class {
|
|
|
329294
329593
|
return this.platform === "ANDROID" ? { appId: this.appId } : { bundleId: this.appId };
|
|
329295
329594
|
}
|
|
329296
329595
|
baseScreenSignal(source) {
|
|
329297
|
-
const parsed = (0,
|
|
329596
|
+
const parsed = (0, import_runner_core9.parseMobileSource)(source, this.platform);
|
|
329298
329597
|
return {
|
|
329299
329598
|
platform: this.platform,
|
|
329300
329599
|
screenHash: parsed.screenSignal.screenHash,
|
|
@@ -329313,11 +329612,11 @@ var MobileBridgeDriver = class {
|
|
|
329313
329612
|
const activity = readStringValue(activityRes);
|
|
329314
329613
|
const pkg2 = readStringValue(packageRes);
|
|
329315
329614
|
if (pkg2) signal.bundleId = pkg2;
|
|
329316
|
-
const normalizedActivity =
|
|
329615
|
+
const normalizedActivity = normalizeAndroidActivity2(activity, pkg2);
|
|
329317
329616
|
if (normalizedActivity) signal.activity = normalizedActivity;
|
|
329318
329617
|
} else {
|
|
329319
329618
|
const infoRes = await this.executeScript("mobile: activeAppInfo", []).catch(() => void 0);
|
|
329320
|
-
const bundleId =
|
|
329619
|
+
const bundleId = readActiveBundleId2(infoRes);
|
|
329321
329620
|
if (bundleId) signal.bundleId = bundleId;
|
|
329322
329621
|
}
|
|
329323
329622
|
} catch {
|
|
@@ -329459,7 +329758,7 @@ var MobileBridgeDriver = class {
|
|
|
329459
329758
|
return this.enrichScreenSignal(source);
|
|
329460
329759
|
}
|
|
329461
329760
|
};
|
|
329462
|
-
function
|
|
329761
|
+
function readActiveBundleId2(result) {
|
|
329463
329762
|
if (result && typeof result === "object" && "value" in result) {
|
|
329464
329763
|
const value = result.value;
|
|
329465
329764
|
if (value && typeof value === "object" && "bundleId" in value) {
|
|
@@ -329685,7 +329984,7 @@ function printDoctorReport(report) {
|
|
|
329685
329984
|
|
|
329686
329985
|
// src/runner.ts
|
|
329687
329986
|
init_dist();
|
|
329688
|
-
var
|
|
329987
|
+
var import_runner_core12 = __toESM(require_dist2());
|
|
329689
329988
|
function fingerprintTest(test) {
|
|
329690
329989
|
const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
|
|
329691
329990
|
return createHash("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
|
|
@@ -330130,7 +330429,7 @@ function pruneCollectedPageForPost(page) {
|
|
|
330130
330429
|
if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
|
|
330131
330430
|
return out;
|
|
330132
330431
|
}
|
|
330133
|
-
function prepareDiscoveryResultForPost(result, logs2) {
|
|
330432
|
+
function prepareDiscoveryResultForPost(result, logs2, options = {}) {
|
|
330134
330433
|
const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
|
|
330135
330434
|
if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
|
|
330136
330435
|
if (Array.isArray(safe.apiCalls)) safe.apiCalls = safe.apiCalls.map((call) => compactForAudit(call));
|
|
@@ -330146,6 +330445,10 @@ function prepareDiscoveryResultForPost(result, logs2) {
|
|
|
330146
330445
|
if (safe.snapshotEvidence) safe.snapshotEvidence = compactForAudit(safe.snapshotEvidence);
|
|
330147
330446
|
if (safe.discoveryCheckpoints) safe.discoveryCheckpoints = compactForAudit(safe.discoveryCheckpoints);
|
|
330148
330447
|
if (safe.pageScreenshotKeys && safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330448
|
+
if (options.omitScreenshots) {
|
|
330449
|
+
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330450
|
+
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330451
|
+
}
|
|
330149
330452
|
if (safe.exploreAlreadySaved === true) {
|
|
330150
330453
|
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330151
330454
|
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
@@ -330245,7 +330548,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330245
330548
|
const runMode = run2.mode ?? "run";
|
|
330246
330549
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
330247
330550
|
log.info(`Running test: ${run2.testCaseId ?? "auth-setup"} (run ${run2.runId}) [mode: ${runMode === "heal" ? "heal" : runMode === "auth-setup" ? "auth-setup" : RUNNER_MODE}]`);
|
|
330248
|
-
const liveBrowserHandle =
|
|
330551
|
+
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
330249
330552
|
sessionId: `${run2.runId}`,
|
|
330250
330553
|
mode: toLiveBrowserMode(runMode),
|
|
330251
330554
|
runId: run2.runId,
|
|
@@ -330254,7 +330557,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330254
330557
|
testCaseId: run2.testCaseId ?? void 0
|
|
330255
330558
|
});
|
|
330256
330559
|
requestLiveBrowserHeartbeat(true);
|
|
330257
|
-
const liveBrowserHeartbeatInterval = setInterval(() =>
|
|
330560
|
+
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
330561
|
+
liveBrowserHandle.patch({});
|
|
330562
|
+
requestLiveBrowserHeartbeat();
|
|
330563
|
+
}, 5e3);
|
|
330258
330564
|
liveBrowserHeartbeatInterval.unref?.();
|
|
330259
330565
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
330260
330566
|
...nativeOptions,
|
|
@@ -330281,7 +330587,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330281
330587
|
log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
|
|
330282
330588
|
return;
|
|
330283
330589
|
}
|
|
330284
|
-
const preflight = await (0,
|
|
330590
|
+
const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
|
|
330285
330591
|
if (preflight.error) {
|
|
330286
330592
|
await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
|
|
330287
330593
|
log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
|
|
@@ -330338,7 +330644,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330338
330644
|
return;
|
|
330339
330645
|
}
|
|
330340
330646
|
const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
|
|
330341
|
-
const batchResult = await (0,
|
|
330647
|
+
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
330342
330648
|
testVariables: run2.testVariables,
|
|
330343
330649
|
runNativeTest: runNativeWithLiveBrowser,
|
|
330344
330650
|
validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
|
|
@@ -330572,7 +330878,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330572
330878
|
if (classifyErrorMessage(composedError ?? null) === "config_issue") {
|
|
330573
330879
|
apiHealStatus = "ERROR";
|
|
330574
330880
|
} else {
|
|
330575
|
-
const rlDetection = (0,
|
|
330881
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330576
330882
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330577
330883
|
tags: run2.tags ?? [],
|
|
330578
330884
|
error: composedError ?? finalVerifyResult?.error ?? null,
|
|
@@ -330679,7 +330985,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330679
330985
|
log.info(`Heal engine: GROK (${source})`);
|
|
330680
330986
|
}
|
|
330681
330987
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
330682
|
-
const result2 = useGrokHeal ? await (0,
|
|
330988
|
+
const result2 = useGrokHeal ? await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
330683
330989
|
...healOpts,
|
|
330684
330990
|
onLog: grokHealLogStream.onLog
|
|
330685
330991
|
}) : await (0, import_runner_core2.executeTestWithHealing)(run2.playwrightCode, run2.steps, run2.baseUrl, healOpts);
|
|
@@ -330712,7 +331018,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330712
331018
|
let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
|
|
330713
331019
|
let healError = result2.error;
|
|
330714
331020
|
if (!result2.passed && !isConfigIssue) {
|
|
330715
|
-
const rlDetection = (0,
|
|
331021
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330716
331022
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330717
331023
|
tags: run2.tags ?? [],
|
|
330718
331024
|
error: result2.error,
|
|
@@ -330906,7 +331212,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330906
331212
|
liveBrowserHandle,
|
|
330907
331213
|
runNativeTest: runNativeWithLiveBrowser
|
|
330908
331214
|
};
|
|
330909
|
-
const healResult = useGrokHealSuite ? await (0,
|
|
331215
|
+
const healResult = useGrokHealSuite ? await (0, import_runner_core10.executeGrokPerTestHeal)(
|
|
330910
331216
|
failedTest.playwrightCode,
|
|
330911
331217
|
failedTest.steps ?? [],
|
|
330912
331218
|
run2.baseUrl,
|
|
@@ -331194,9 +331500,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331194
331500
|
signal: AbortSignal.timeout(3e4)
|
|
331195
331501
|
});
|
|
331196
331502
|
if (res.ok) return;
|
|
331197
|
-
|
|
331503
|
+
const responseText = await res.text().catch(() => "");
|
|
331504
|
+
const statusMessage = responseText ? `${res.status}: ${responseText.slice(0, 500)}` : String(res.status);
|
|
331505
|
+
if (res.status === 409) {
|
|
331506
|
+
throw new Error(`Discovery plan checkpoint rejected: ${statusMessage}`);
|
|
331507
|
+
}
|
|
331508
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} returned ${statusMessage}`);
|
|
331198
331509
|
} catch (err) {
|
|
331199
|
-
|
|
331510
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331511
|
+
if (/Discovery plan checkpoint rejected/i.test(message)) throw err;
|
|
331512
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} failed: ${message}`);
|
|
331200
331513
|
}
|
|
331201
331514
|
if (attempt < 2) await new Promise((r) => setTimeout(r, 3e3 * (attempt + 1)));
|
|
331202
331515
|
}
|
|
@@ -331204,6 +331517,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331204
331517
|
};
|
|
331205
331518
|
let discoveryResult;
|
|
331206
331519
|
const buildFinalBody = (r, overrideError, auditWarning) => {
|
|
331520
|
+
const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
|
|
331207
331521
|
const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
|
|
331208
331522
|
void _exploreFinalMessages;
|
|
331209
331523
|
const logs2 = auditWarning ? [rest.logs, auditWarning].filter(Boolean).join("\n") : rest.logs;
|
|
@@ -331253,7 +331567,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331253
331567
|
resolvedAppOrigin: rest.resolvedAppOrigin,
|
|
331254
331568
|
plans: rest.plans,
|
|
331255
331569
|
exploreStats: rest.exploreStats,
|
|
331256
|
-
screenshots: rest.screenshots,
|
|
331570
|
+
...isNativeDiscovery ? {} : { screenshots: rest.screenshots },
|
|
331257
331571
|
snapshotEvidence: rest.snapshotEvidence,
|
|
331258
331572
|
discoveryCheckpoints: rest.discoveryCheckpoints,
|
|
331259
331573
|
streamedTestCount: streamedTestKeys.size,
|
|
@@ -331262,8 +331576,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331262
331576
|
...explorePosted ? {} : {
|
|
331263
331577
|
collectedPages: rest.collectedPages,
|
|
331264
331578
|
collectedTransitions: rest.collectedTransitions,
|
|
331265
|
-
|
|
331266
|
-
|
|
331579
|
+
...isNativeDiscovery ? {} : {
|
|
331580
|
+
pageScreenshots: rest.pageScreenshots,
|
|
331581
|
+
pageScreenshotKeys: rest.pageScreenshotKeys
|
|
331582
|
+
}
|
|
331267
331583
|
},
|
|
331268
331584
|
...harPosted ? {} : {
|
|
331269
331585
|
apiCalls: rest.apiCalls,
|
|
@@ -331271,7 +331587,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331271
331587
|
},
|
|
331272
331588
|
// Only send tests that were NOT confirmed streamed to avoid duplicates
|
|
331273
331589
|
tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
|
|
331274
|
-
}, logs2);
|
|
331590
|
+
}, logs2, { omitScreenshots: isNativeDiscovery });
|
|
331275
331591
|
};
|
|
331276
331592
|
const acquireLoginBrowser = async () => {
|
|
331277
331593
|
const browser = await browserPool.acquireAsync(12e3);
|
|
@@ -331375,7 +331691,6 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331375
331691
|
thumbnailCapturedAt: Date.now()
|
|
331376
331692
|
});
|
|
331377
331693
|
requestLiveBrowserHeartbeat();
|
|
331378
|
-
void onScreenshot("mobile", base64);
|
|
331379
331694
|
},
|
|
331380
331695
|
onExploreComplete,
|
|
331381
331696
|
onTestGenerated,
|
|
@@ -331402,7 +331717,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331402
331717
|
setExecutorBusy(false);
|
|
331403
331718
|
}
|
|
331404
331719
|
} else if (ctx.mode === "import") {
|
|
331405
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
331720
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_runner_core10.runFrameworkImportOnRunner;
|
|
331406
331721
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
331407
331722
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
331408
331723
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -331732,7 +332047,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331732
332047
|
let runStatus = result.passed ? "PASSED" : "FAILED";
|
|
331733
332048
|
let runError = result.error;
|
|
331734
332049
|
if (!result.passed) {
|
|
331735
|
-
const rlDetection = (0,
|
|
332050
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
331736
332051
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
331737
332052
|
tags: run2.tags ?? [],
|
|
331738
332053
|
error: result.error,
|
|
@@ -331870,7 +332185,7 @@ async function startRunner(opts) {
|
|
|
331870
332185
|
capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
|
|
331871
332186
|
run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
|
|
331872
332187
|
};
|
|
331873
|
-
const RESOURCE_GOVERNOR_OPTIONS = (0,
|
|
332188
|
+
const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
|
|
331874
332189
|
isCloud: IS_CLOUD,
|
|
331875
332190
|
maxConcurrent: MAX_CONCURRENT,
|
|
331876
332191
|
typeLimits: TYPE_LIMITS
|
|
@@ -332021,8 +332336,8 @@ async function startRunner(opts) {
|
|
|
332021
332336
|
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
332022
332337
|
} : { web: true, ios: false, android: false };
|
|
332023
332338
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
332024
|
-
const resourceStatus = (0,
|
|
332025
|
-
const processInventory = await (0,
|
|
332339
|
+
const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
332340
|
+
const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
|
|
332026
332341
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
332027
332342
|
lastInventoryKinds.clear();
|
|
332028
332343
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
@@ -332049,8 +332364,8 @@ async function startRunner(opts) {
|
|
|
332049
332364
|
resourceSnapshot: resourceStatus.snapshot,
|
|
332050
332365
|
resourceGovernor: resourceStatus.governor,
|
|
332051
332366
|
processInventory,
|
|
332052
|
-
aiQueue: (0,
|
|
332053
|
-
liveBrowsers:
|
|
332367
|
+
aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
|
|
332368
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
332054
332369
|
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
332055
332370
|
// Cached & cheap — see getCachedDoctor above.
|
|
332056
332371
|
doctor: getCachedDoctor()
|
|
@@ -332144,7 +332459,7 @@ async function startRunner(opts) {
|
|
|
332144
332459
|
let pidsWatchdogWarned = false;
|
|
332145
332460
|
const pidsWatchdog = setInterval(() => {
|
|
332146
332461
|
if (shuttingDown) return;
|
|
332147
|
-
const snapshot = (0,
|
|
332462
|
+
const snapshot = (0, import_runner_core12.getResourceSnapshot)();
|
|
332148
332463
|
const pct = snapshot.pidsPct;
|
|
332149
332464
|
if (pct == null || !Number.isFinite(pct)) return;
|
|
332150
332465
|
if (pct >= PIDS_RECYCLE_PCT) {
|
|
@@ -332180,7 +332495,7 @@ async function startRunner(opts) {
|
|
|
332180
332495
|
log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
332181
332496
|
continue;
|
|
332182
332497
|
}
|
|
332183
|
-
const admission = (0,
|
|
332498
|
+
const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
332184
332499
|
mode: runMode,
|
|
332185
332500
|
activeRuns,
|
|
332186
332501
|
activeByType: Object.fromEntries(activeByType),
|