@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.js
CHANGED
|
@@ -1037,6 +1037,65 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1037
1037
|
}
|
|
1038
1038
|
});
|
|
1039
1039
|
|
|
1040
|
+
// ../runner-core/dist/services/mobile/mobile-boundary.js
|
|
1041
|
+
var require_mobile_boundary = __commonJS({
|
|
1042
|
+
"../runner-core/dist/services/mobile/mobile-boundary.js"(exports2) {
|
|
1043
|
+
"use strict";
|
|
1044
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1045
|
+
exports2.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
|
|
1046
|
+
exports2.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
|
|
1047
|
+
exports2.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
|
|
1048
|
+
exports2.isTargetLikeMobileBoundary = isTargetLikeMobileBoundary2;
|
|
1049
|
+
exports2.mobileBoundaryNote = mobileBoundaryNote;
|
|
1050
|
+
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1051
|
+
"com.android.permissioncontroller",
|
|
1052
|
+
"com.google.android.permissioncontroller",
|
|
1053
|
+
"com.android.packageinstaller",
|
|
1054
|
+
"com.google.android.packageinstaller"
|
|
1055
|
+
]);
|
|
1056
|
+
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1057
|
+
"com.apple.springboard"
|
|
1058
|
+
]);
|
|
1059
|
+
function mobileOwnerFromScreenSignal(signal, targetAppId) {
|
|
1060
|
+
const bundleId = signal.bundleId?.trim();
|
|
1061
|
+
if (bundleId)
|
|
1062
|
+
return bundleId;
|
|
1063
|
+
const activity = signal.activity?.trim();
|
|
1064
|
+
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
1065
|
+
return targetAppId;
|
|
1066
|
+
}
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
function isTransientMobileSystemOwner(owner, platform3) {
|
|
1070
|
+
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
1071
|
+
}
|
|
1072
|
+
function classifyMobileScreenBoundary2(signal, targetAppId, platform3) {
|
|
1073
|
+
if (!targetAppId)
|
|
1074
|
+
return { kind: "target" };
|
|
1075
|
+
const owner = mobileOwnerFromScreenSignal(signal, targetAppId);
|
|
1076
|
+
if (!owner)
|
|
1077
|
+
return { kind: "unknown" };
|
|
1078
|
+
if (owner === targetAppId)
|
|
1079
|
+
return { kind: "target" };
|
|
1080
|
+
if (isTransientMobileSystemOwner(owner, platform3)) {
|
|
1081
|
+
return { kind: "transient_external", owner, targetAppId };
|
|
1082
|
+
}
|
|
1083
|
+
return { kind: "external", owner, targetAppId };
|
|
1084
|
+
}
|
|
1085
|
+
function isTargetLikeMobileBoundary2(boundary) {
|
|
1086
|
+
return boundary.kind === "target" || boundary.kind === "unknown";
|
|
1087
|
+
}
|
|
1088
|
+
function mobileBoundaryNote(boundary) {
|
|
1089
|
+
return [
|
|
1090
|
+
"",
|
|
1091
|
+
"## App Boundary",
|
|
1092
|
+
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
1093
|
+
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."
|
|
1094
|
+
].join("\n");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1040
1099
|
// ../shared/dist/types.js
|
|
1041
1100
|
function isUIVariant(value) {
|
|
1042
1101
|
return typeof value === "string" && UI_VARIANTS.includes(value);
|
|
@@ -1472,6 +1531,18 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1472
1531
|
` }`,
|
|
1473
1532
|
` throw new Error('No locator resolved. Tried: ' + JSON.stringify(selectors) + (__lastError ? ' (last error: ' + String(__lastError) + ')' : ''));`,
|
|
1474
1533
|
`}`,
|
|
1534
|
+
``,
|
|
1535
|
+
`async function __tapAt(driver: WebdriverIO.Browser, x: number, y: number) {`,
|
|
1536
|
+
` 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 }] }]);`,
|
|
1537
|
+
`}`,
|
|
1538
|
+
``,
|
|
1539
|
+
`async function __typeIntoFocused(driver: WebdriverIO.Browser, text: string) {`,
|
|
1540
|
+
` try {`,
|
|
1541
|
+
` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).addValue(text);`,
|
|
1542
|
+
` } catch {`,
|
|
1543
|
+
` await driver.keys(text);`,
|
|
1544
|
+
` }`,
|
|
1545
|
+
`}`,
|
|
1475
1546
|
...hasSwipe ? [
|
|
1476
1547
|
``,
|
|
1477
1548
|
`// Read the device window with a few retries (matches the runtime executor).`,
|
|
@@ -1530,13 +1601,25 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1530
1601
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1531
1602
|
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 }] }]);`);
|
|
1532
1603
|
} else if (step.action === "type" && hasSelector) {
|
|
1533
|
-
|
|
1604
|
+
if (bounds) {
|
|
1605
|
+
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1606
|
+
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1607
|
+
lines.push(` try {`);
|
|
1608
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1609
|
+
lines.push(` } catch {`);
|
|
1610
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1611
|
+
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1612
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1613
|
+
lines.push(` }`);
|
|
1614
|
+
} else {
|
|
1615
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1616
|
+
}
|
|
1534
1617
|
} else if (step.action === "type" && bounds) {
|
|
1535
1618
|
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1536
1619
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1537
|
-
lines.push(` await driver
|
|
1620
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1538
1621
|
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1539
|
-
lines.push(` await (
|
|
1622
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1540
1623
|
} else if (step.action === "clear" && hasSelector) {
|
|
1541
1624
|
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
|
|
1542
1625
|
} else if (step.action === "assertVisible" && hasSelector) {
|
|
@@ -4433,21 +4516,13 @@ var require_mobile_explorer = __commonJS({
|
|
|
4433
4516
|
var ai_retry_js_1 = require_ai_retry();
|
|
4434
4517
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
4435
4518
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
4519
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
4436
4520
|
var MAX_MOBILE_ITERATIONS_SURVEY = 40;
|
|
4437
4521
|
var MAX_MOBILE_ITERATIONS_DEEP = 60;
|
|
4438
4522
|
var MAX_SCREENSHOTS = 15;
|
|
4439
4523
|
var MAX_RECENT_EXCHANGES = 24;
|
|
4440
4524
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
4441
4525
|
var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
|
|
4442
|
-
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4443
|
-
"com.android.permissioncontroller",
|
|
4444
|
-
"com.google.android.permissioncontroller",
|
|
4445
|
-
"com.android.packageinstaller",
|
|
4446
|
-
"com.google.android.packageinstaller"
|
|
4447
|
-
]);
|
|
4448
|
-
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4449
|
-
"com.apple.springboard"
|
|
4450
|
-
]);
|
|
4451
4526
|
var MOBILE_SNAPSHOT_TOOL = {
|
|
4452
4527
|
type: "function",
|
|
4453
4528
|
function: {
|
|
@@ -4670,40 +4745,6 @@ var require_mobile_explorer = __commonJS({
|
|
|
4670
4745
|
return "timeout";
|
|
4671
4746
|
return "ai_error";
|
|
4672
4747
|
}
|
|
4673
|
-
function ownerFromScreenSignal(signal, targetAppId) {
|
|
4674
|
-
const bundleId = signal.bundleId?.trim();
|
|
4675
|
-
if (bundleId)
|
|
4676
|
-
return bundleId;
|
|
4677
|
-
const activity = signal.activity?.trim();
|
|
4678
|
-
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
4679
|
-
return targetAppId;
|
|
4680
|
-
}
|
|
4681
|
-
return null;
|
|
4682
|
-
}
|
|
4683
|
-
function isTransientSystemOwner(owner, platform3) {
|
|
4684
|
-
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
4685
|
-
}
|
|
4686
|
-
function classifyMobileScreenBoundary(signal, targetAppId, platform3) {
|
|
4687
|
-
if (!targetAppId)
|
|
4688
|
-
return { kind: "target" };
|
|
4689
|
-
const owner = ownerFromScreenSignal(signal, targetAppId);
|
|
4690
|
-
if (!owner)
|
|
4691
|
-
return { kind: "unknown" };
|
|
4692
|
-
if (owner === targetAppId)
|
|
4693
|
-
return { kind: "target" };
|
|
4694
|
-
if (isTransientSystemOwner(owner, platform3)) {
|
|
4695
|
-
return { kind: "transient_external", owner, targetAppId };
|
|
4696
|
-
}
|
|
4697
|
-
return { kind: "external", owner, targetAppId };
|
|
4698
|
-
}
|
|
4699
|
-
function boundaryNote(boundary) {
|
|
4700
|
-
return [
|
|
4701
|
-
"",
|
|
4702
|
-
"## App Boundary",
|
|
4703
|
-
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
4704
|
-
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."
|
|
4705
|
-
].join("\n");
|
|
4706
|
-
}
|
|
4707
4748
|
function resolveSnapshot(xml, windowSize, driverSignal, platform3) {
|
|
4708
4749
|
const parsed = (0, mobile_source_parser_js_1.parseMobileSource)(xml, platform3);
|
|
4709
4750
|
const signal = {
|
|
@@ -4901,10 +4942,10 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4901
4942
|
};
|
|
4902
4943
|
const currentBoundary = () => {
|
|
4903
4944
|
const current = getLatest();
|
|
4904
|
-
return current ? classifyMobileScreenBoundary(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4945
|
+
return current ? (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4905
4946
|
};
|
|
4906
4947
|
const absorbResolvedObservation = async (resolved, screenshot, iterationForObservation, source) => {
|
|
4907
|
-
const boundary = classifyMobileScreenBoundary(resolved.signal, targetAppId, platform3);
|
|
4948
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
4908
4949
|
if (boundary.kind === "target" || boundary.kind === "unknown") {
|
|
4909
4950
|
const screenId = ingestSnapshot(resolved, iterationForObservation);
|
|
4910
4951
|
captureScreenshot(screenshot, screenId);
|
|
@@ -4915,7 +4956,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4915
4956
|
if (boundary.kind === "transient_external") {
|
|
4916
4957
|
log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
|
|
4917
4958
|
return {
|
|
4918
|
-
observation: `${resolved.observation}${
|
|
4959
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
4919
4960
|
screenId: null,
|
|
4920
4961
|
transientExternal: true,
|
|
4921
4962
|
externalOwner: boundary.owner
|
|
@@ -4934,7 +4975,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4934
4975
|
externalOwner: boundary.owner
|
|
4935
4976
|
};
|
|
4936
4977
|
}
|
|
4937
|
-
const recoveredBoundary = classifyMobileScreenBoundary(recovered.signal, targetAppId, platform3);
|
|
4978
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
4938
4979
|
latest = recovered;
|
|
4939
4980
|
lastSnapshotIteration = iterationForObservation;
|
|
4940
4981
|
if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
|
|
@@ -4950,7 +4991,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4950
4991
|
if (recoveredBoundary.kind === "transient_external") {
|
|
4951
4992
|
log2(` Relaunch surfaced system overlay ${recoveredBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
4952
4993
|
return {
|
|
4953
|
-
observation: `${recovered.observation}${
|
|
4994
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
4954
4995
|
screenId: null,
|
|
4955
4996
|
externalBlocked: true,
|
|
4956
4997
|
transientExternal: true,
|
|
@@ -8594,6 +8635,7 @@ var require_mobile_generator = __commonJS({
|
|
|
8594
8635
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
8595
8636
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
8596
8637
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
8638
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
8597
8639
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
8598
8640
|
var MAX_RECENT_EXCHANGES = 24;
|
|
8599
8641
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
@@ -8779,8 +8821,9 @@ var require_mobile_generator = __commonJS({
|
|
|
8779
8821
|
function hasStableId(element) {
|
|
8780
8822
|
return !!nonEmpty2(element.accessibilityId) || !!nonEmpty2(element.resourceId);
|
|
8781
8823
|
}
|
|
8782
|
-
function buildSystemPrompt(platform3) {
|
|
8824
|
+
function buildSystemPrompt(platform3, targetAppId) {
|
|
8783
8825
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
8826
|
+
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
8784
8827
|
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.
|
|
8785
8828
|
|
|
8786
8829
|
## How the loop works
|
|
@@ -8800,7 +8843,8 @@ var require_mobile_generator = __commonJS({
|
|
|
8800
8843
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
8801
8844
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
8802
8845
|
- Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
|
|
8803
|
-
- Stay inside this app.
|
|
8846
|
+
- 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.
|
|
8847
|
+
- Finish promptly once the outcome is proven.
|
|
8804
8848
|
|
|
8805
8849
|
Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
|
|
8806
8850
|
}
|
|
@@ -8873,7 +8917,7 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8873
8917
|
}
|
|
8874
8918
|
}
|
|
8875
8919
|
async function runScenarioLoop(args) {
|
|
8876
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, log: log2 } = args;
|
|
8920
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, log: log2 } = args;
|
|
8877
8921
|
const actions = [];
|
|
8878
8922
|
let assertCount = 0;
|
|
8879
8923
|
let finish = null;
|
|
@@ -8887,14 +8931,98 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8887
8931
|
const fromActivity = signal.activity ? signal.activity.split(".").pop() : void 0;
|
|
8888
8932
|
return nonEmpty2(fromActivity) ?? nonEmpty2(signal.bundleId) ?? `screen-${signal.screenHash.slice(0, 8)}`;
|
|
8889
8933
|
};
|
|
8890
|
-
const
|
|
8891
|
-
if (!xml)
|
|
8892
|
-
return null;
|
|
8893
|
-
const resolved = resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8934
|
+
const setLatest = (resolved) => {
|
|
8894
8935
|
latest = resolved;
|
|
8895
8936
|
lastScreenName = screenNameFor(resolved);
|
|
8896
8937
|
return resolved;
|
|
8897
8938
|
};
|
|
8939
|
+
const buildResolved = (xml, driverSignal, windowSize) => {
|
|
8940
|
+
if (!xml)
|
|
8941
|
+
return null;
|
|
8942
|
+
return resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8943
|
+
};
|
|
8944
|
+
const emitScreenshot = (base64) => {
|
|
8945
|
+
if (!base64)
|
|
8946
|
+
return;
|
|
8947
|
+
try {
|
|
8948
|
+
onScreenshot?.(base64);
|
|
8949
|
+
} catch {
|
|
8950
|
+
}
|
|
8951
|
+
};
|
|
8952
|
+
const absorbResolvedObservation = async (resolved, screenshot, source) => {
|
|
8953
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
8954
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(boundary)) {
|
|
8955
|
+
setLatest(resolved);
|
|
8956
|
+
emitScreenshot(screenshot);
|
|
8957
|
+
return { resolved, observation: resolved.observation };
|
|
8958
|
+
}
|
|
8959
|
+
setLatest(resolved);
|
|
8960
|
+
if (boundary.kind === "transient_external") {
|
|
8961
|
+
log2(` system overlay detected from ${source}: ${boundary.owner}; not recording it as a target-app step.`);
|
|
8962
|
+
return {
|
|
8963
|
+
resolved,
|
|
8964
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
8965
|
+
transientExternal: true,
|
|
8966
|
+
externalOwner: boundary.owner
|
|
8967
|
+
};
|
|
8968
|
+
}
|
|
8969
|
+
log2(` external app detected from ${source}: ${boundary.owner}; rejecting the action and relaunching ${boundary.targetAppId}.`);
|
|
8970
|
+
try {
|
|
8971
|
+
await driver.relaunch();
|
|
8972
|
+
const snap = await driver.snapshot();
|
|
8973
|
+
const recovered = buildResolved(snap.xml, snap.screenSignal, snap.windowSize);
|
|
8974
|
+
if (!recovered) {
|
|
8975
|
+
return {
|
|
8976
|
+
resolved: null,
|
|
8977
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunched target app, but no UI tree was returned)`,
|
|
8978
|
+
externalBlocked: true,
|
|
8979
|
+
externalOwner: boundary.owner
|
|
8980
|
+
};
|
|
8981
|
+
}
|
|
8982
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
8983
|
+
setLatest(recovered);
|
|
8984
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(recoveredBoundary)) {
|
|
8985
|
+
emitScreenshot(snap.screenshot);
|
|
8986
|
+
return {
|
|
8987
|
+
resolved: recovered,
|
|
8988
|
+
observation: recovered.observation,
|
|
8989
|
+
externalBlocked: true,
|
|
8990
|
+
externalOwner: boundary.owner
|
|
8991
|
+
};
|
|
8992
|
+
}
|
|
8993
|
+
if (recoveredBoundary.kind === "transient_external") {
|
|
8994
|
+
log2(` relaunch surfaced system overlay ${recoveredBoundary.owner}; not recording it as a target-app step.`);
|
|
8995
|
+
return {
|
|
8996
|
+
resolved: recovered,
|
|
8997
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
8998
|
+
externalBlocked: true,
|
|
8999
|
+
transientExternal: true,
|
|
9000
|
+
externalOwner: boundary.owner
|
|
9001
|
+
};
|
|
9002
|
+
}
|
|
9003
|
+
log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
|
|
9004
|
+
return {
|
|
9005
|
+
resolved: recovered,
|
|
9006
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
|
|
9007
|
+
externalBlocked: true,
|
|
9008
|
+
externalOwner: boundary.owner
|
|
9009
|
+
};
|
|
9010
|
+
} catch (err) {
|
|
9011
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9012
|
+
return {
|
|
9013
|
+
resolved: null,
|
|
9014
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; failed to relaunch target app: ${message})`,
|
|
9015
|
+
externalBlocked: true,
|
|
9016
|
+
externalOwner: boundary.owner
|
|
9017
|
+
};
|
|
9018
|
+
}
|
|
9019
|
+
};
|
|
9020
|
+
const absorbObservation = async (xml, driverSignal, windowSize, screenshot, source) => {
|
|
9021
|
+
const resolved = buildResolved(xml, driverSignal, windowSize);
|
|
9022
|
+
if (!resolved)
|
|
9023
|
+
return { resolved: null, observation: null };
|
|
9024
|
+
return absorbResolvedObservation(resolved, screenshot, source);
|
|
9025
|
+
};
|
|
8898
9026
|
const resolveRef = (ref) => {
|
|
8899
9027
|
if (typeof ref !== "string" || !latest)
|
|
8900
9028
|
return null;
|
|
@@ -8902,11 +9030,11 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8902
9030
|
};
|
|
8903
9031
|
try {
|
|
8904
9032
|
const seed = await driver.snapshot();
|
|
8905
|
-
|
|
9033
|
+
await absorbObservation(seed.xml, seed.screenSignal, seed.windowSize, seed.screenshot, "seed snapshot");
|
|
8906
9034
|
} catch (err) {
|
|
8907
9035
|
log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
8908
9036
|
}
|
|
8909
|
-
const systemPrompt = buildSystemPrompt(platform3);
|
|
9037
|
+
const systemPrompt = buildSystemPrompt(platform3, targetAppId);
|
|
8910
9038
|
const kickoff = buildScenarioKickoff(scenario);
|
|
8911
9039
|
const recentExchanges = [];
|
|
8912
9040
|
let iteration = 0;
|
|
@@ -9017,7 +9145,7 @@ ${statePacket}` },
|
|
|
9017
9145
|
driver,
|
|
9018
9146
|
platform: platform3,
|
|
9019
9147
|
latest: getLatest,
|
|
9020
|
-
|
|
9148
|
+
absorbObservation,
|
|
9021
9149
|
resolveRef,
|
|
9022
9150
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
9023
9151
|
recordAction: (action) => {
|
|
@@ -9040,60 +9168,85 @@ ${statePacket}` },
|
|
|
9040
9168
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
9041
9169
|
}
|
|
9042
9170
|
async function dispatchGenerateTool(args) {
|
|
9043
|
-
const { toolName, toolArgs, driver, platform: platform3, latest,
|
|
9171
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, screenName, recordAction, log: log2 } = args;
|
|
9044
9172
|
const unknownRef = (ref) => ({
|
|
9045
9173
|
ok: false,
|
|
9046
9174
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
9047
9175
|
});
|
|
9176
|
+
const actionPayload = (result, absorbed) => {
|
|
9177
|
+
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
9178
|
+
return {
|
|
9179
|
+
ok: result.ok && !blocked,
|
|
9180
|
+
...result.error ? { error: result.error } : {},
|
|
9181
|
+
...blocked ? { error: "Action left the target app and was not recorded. Continue from the relaunched target app." } : {},
|
|
9182
|
+
observation: absorbed.observation ?? void 0,
|
|
9183
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9184
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9185
|
+
};
|
|
9186
|
+
};
|
|
9187
|
+
const shouldRecordAction = (result, absorbed) => result.ok && !absorbed.externalBlocked && !absorbed.transientExternal;
|
|
9048
9188
|
switch (toolName) {
|
|
9049
9189
|
case "mobile_snapshot": {
|
|
9050
9190
|
const snap = await driver.snapshot();
|
|
9051
|
-
const
|
|
9052
|
-
if (!resolved)
|
|
9191
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_snapshot");
|
|
9192
|
+
if (!absorbed.resolved)
|
|
9053
9193
|
return { ok: false, error: "snapshot returned no UI tree" };
|
|
9054
|
-
log2(` snapshot: ${resolved.screen.elements.length} elements`);
|
|
9055
|
-
return {
|
|
9194
|
+
log2(` snapshot: ${absorbed.resolved.screen.elements.length} elements`);
|
|
9195
|
+
return {
|
|
9196
|
+
observation: absorbed.observation,
|
|
9197
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9198
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9199
|
+
};
|
|
9056
9200
|
}
|
|
9057
9201
|
case "mobile_tap": {
|
|
9058
9202
|
const element = resolveRef(toolArgs.ref);
|
|
9059
9203
|
if (!element)
|
|
9060
9204
|
return unknownRef(toolArgs.ref);
|
|
9205
|
+
const beforeScreenName = screenName();
|
|
9061
9206
|
const result = await driver.tap(element.bounds);
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9207
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
9208
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9209
|
+
recordAction({
|
|
9210
|
+
type: "TAP",
|
|
9211
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9212
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9213
|
+
});
|
|
9214
|
+
}
|
|
9215
|
+
return actionPayload(result, absorbed);
|
|
9069
9216
|
}
|
|
9070
9217
|
case "mobile_type": {
|
|
9071
9218
|
const element = resolveRef(toolArgs.ref);
|
|
9072
9219
|
if (!element)
|
|
9073
9220
|
return unknownRef(toolArgs.ref);
|
|
9074
9221
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
9222
|
+
const beforeScreenName = screenName();
|
|
9075
9223
|
const result = await driver.type(value, element.bounds);
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9224
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_type");
|
|
9225
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9226
|
+
recordAction({
|
|
9227
|
+
type: "TYPE",
|
|
9228
|
+
value,
|
|
9229
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9230
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9231
|
+
});
|
|
9232
|
+
}
|
|
9233
|
+
return actionPayload(result, absorbed);
|
|
9084
9234
|
}
|
|
9085
9235
|
case "mobile_clear": {
|
|
9086
9236
|
const element = resolveRef(toolArgs.ref);
|
|
9087
9237
|
if (!element)
|
|
9088
9238
|
return unknownRef(toolArgs.ref);
|
|
9239
|
+
const beforeScreenName = screenName();
|
|
9089
9240
|
const result = await driver.clear(element.bounds);
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9241
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_clear");
|
|
9242
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9243
|
+
recordAction({
|
|
9244
|
+
type: "CLEAR",
|
|
9245
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9246
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9247
|
+
});
|
|
9248
|
+
}
|
|
9249
|
+
return actionPayload(result, absorbed);
|
|
9097
9250
|
}
|
|
9098
9251
|
case "mobile_swipe": {
|
|
9099
9252
|
const direction = toolArgs.direction;
|
|
@@ -9101,30 +9254,42 @@ ${statePacket}` },
|
|
|
9101
9254
|
return { ok: false, error: "direction must be one of up|down|left|right" };
|
|
9102
9255
|
}
|
|
9103
9256
|
const distance = typeof toolArgs.distance === "number" ? toolArgs.distance : void 0;
|
|
9257
|
+
const beforeScreenName = screenName();
|
|
9104
9258
|
const result = await driver.swipe(direction, distance);
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9259
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_swipe");
|
|
9260
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9261
|
+
recordAction({
|
|
9262
|
+
type: "SWIPE",
|
|
9263
|
+
metadata: {
|
|
9264
|
+
screenName: beforeScreenName,
|
|
9265
|
+
direction,
|
|
9266
|
+
...distance !== void 0 ? { distance } : {}
|
|
9267
|
+
}
|
|
9268
|
+
});
|
|
9269
|
+
}
|
|
9270
|
+
return actionPayload(result, absorbed);
|
|
9115
9271
|
}
|
|
9116
9272
|
case "mobile_back": {
|
|
9273
|
+
const beforeScreenName = screenName();
|
|
9117
9274
|
const result = await driver.back();
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9275
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_back");
|
|
9276
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9277
|
+
recordAction({ type: "BACK", metadata: { screenName: beforeScreenName } });
|
|
9278
|
+
}
|
|
9279
|
+
return actionPayload(result, absorbed);
|
|
9121
9280
|
}
|
|
9122
9281
|
case "mobile_relaunch": {
|
|
9123
9282
|
await driver.relaunch();
|
|
9124
9283
|
const snap = await driver.snapshot();
|
|
9125
|
-
const
|
|
9284
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
|
|
9126
9285
|
log2(" relaunched app for hermetic reset.");
|
|
9127
|
-
return {
|
|
9286
|
+
return {
|
|
9287
|
+
ok: true,
|
|
9288
|
+
observation: absorbed.observation,
|
|
9289
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9290
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9291
|
+
note: "App relaunched (clean state). Not recorded as a step."
|
|
9292
|
+
};
|
|
9128
9293
|
}
|
|
9129
9294
|
case "mobile_assert_visible": {
|
|
9130
9295
|
const recorded = recordAssertVisible(toolArgs, { resolveRef, screenName, platform: platform3, recordAction });
|
|
@@ -9262,7 +9427,7 @@ ${statePacket}` },
|
|
|
9262
9427
|
return test;
|
|
9263
9428
|
}
|
|
9264
9429
|
async function runMobileGeneratePhase(args) {
|
|
9265
|
-
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated } = args;
|
|
9430
|
+
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
|
|
9266
9431
|
const log2 = (line) => {
|
|
9267
9432
|
try {
|
|
9268
9433
|
onLog?.(line);
|
|
@@ -9276,6 +9441,7 @@ ${statePacket}` },
|
|
|
9276
9441
|
const provider = (0, ai_provider_js_1.getProviderForModel)(agentModel);
|
|
9277
9442
|
const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
|
|
9278
9443
|
const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
9444
|
+
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
9279
9445
|
log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
|
|
9280
9446
|
const tests = [];
|
|
9281
9447
|
for (let i = 0; i < scenarios.length; i++) {
|
|
@@ -9294,6 +9460,8 @@ ${statePacket}` },
|
|
|
9294
9460
|
aiClient,
|
|
9295
9461
|
auditGroupId,
|
|
9296
9462
|
onAICall,
|
|
9463
|
+
onScreenshot,
|
|
9464
|
+
targetAppId,
|
|
9297
9465
|
log: log2
|
|
9298
9466
|
});
|
|
9299
9467
|
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
@@ -311423,7 +311591,9 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311423
311591
|
return {
|
|
311424
311592
|
collectedPages: explore.collectedPages,
|
|
311425
311593
|
collectedTransitions,
|
|
311426
|
-
|
|
311594
|
+
// Native screenshots are multi-megabyte PNGs. The live mirror streams them
|
|
311595
|
+
// through heartbeat telemetry; discovery checkpoints keep the screen graph
|
|
311596
|
+
// and transitions so large Android surveys cannot exceed server body limits.
|
|
311427
311597
|
exploreStats: explore.exploreStats,
|
|
311428
311598
|
healthObservations: explore.healthObservations
|
|
311429
311599
|
};
|
|
@@ -311581,6 +311751,7 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311581
311751
|
ctx,
|
|
311582
311752
|
onLog: (line) => log2(line),
|
|
311583
311753
|
onAICall,
|
|
311754
|
+
onScreenshot,
|
|
311584
311755
|
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
|
|
311585
311756
|
});
|
|
311586
311757
|
const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -325286,7 +325457,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
325286
325457
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
325287
325458
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
325288
325459
|
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
325289
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "
|
|
325460
|
+
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
325290
325461
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
325291
325462
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
325292
325463
|
var LiveBrowserRegistry = class {
|
|
@@ -325782,6 +325953,7 @@ var require_dist2 = __commonJS({
|
|
|
325782
325953
|
__exportStar(require_ai_provider(), exports2);
|
|
325783
325954
|
__exportStar(require_mobile_types(), exports2);
|
|
325784
325955
|
__exportStar(require_mobile_source_parser(), exports2);
|
|
325956
|
+
__exportStar(require_mobile_boundary(), exports2);
|
|
325785
325957
|
var mobile_explorer_js_1 = require_mobile_explorer();
|
|
325786
325958
|
Object.defineProperty(exports2, "runMobileExplorePhase", { enumerable: true, get: function() {
|
|
325787
325959
|
return mobile_explorer_js_1.runMobileExplorePhase;
|
|
@@ -326114,7 +326286,7 @@ var require_package4 = __commonJS({
|
|
|
326114
326286
|
"package.json"(exports2, module2) {
|
|
326115
326287
|
module2.exports = {
|
|
326116
326288
|
name: "@validate.qa/runner",
|
|
326117
|
-
version: "1.0.
|
|
326289
|
+
version: "1.0.6",
|
|
326118
326290
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326119
326291
|
bin: {
|
|
326120
326292
|
"validate-runner": "dist/cli.js"
|
|
@@ -326212,7 +326384,7 @@ var import_runner_core = __toESM(require_dist2());
|
|
|
326212
326384
|
var import_runner_core2 = __toESM(require_dist2());
|
|
326213
326385
|
|
|
326214
326386
|
// src/runner.ts
|
|
326215
|
-
var
|
|
326387
|
+
var import_runner_core10 = __toESM(require_dist2());
|
|
326216
326388
|
|
|
326217
326389
|
// src/services/regression-runner.ts
|
|
326218
326390
|
var import_runner_core3 = __toESM(require_dist2());
|
|
@@ -326227,7 +326399,7 @@ var import_runner_core5 = __toESM(require_dist2());
|
|
|
326227
326399
|
var import_runner_core6 = __toESM(require_dist2());
|
|
326228
326400
|
|
|
326229
326401
|
// src/runner.ts
|
|
326230
|
-
var
|
|
326402
|
+
var import_runner_core11 = __toESM(require_dist2());
|
|
326231
326403
|
|
|
326232
326404
|
// src/services/auth-state.ts
|
|
326233
326405
|
var INLINE_LOGIN_TAG = "@inline-login";
|
|
@@ -326247,6 +326419,7 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
326247
326419
|
|
|
326248
326420
|
// src/services/appium-executor.ts
|
|
326249
326421
|
init_dist();
|
|
326422
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
326250
326423
|
|
|
326251
326424
|
// src/services/appium-lifecycle.ts
|
|
326252
326425
|
var import_child_process = require("child_process");
|
|
@@ -327535,6 +327708,86 @@ async function openDeepLink(sessionId, platform3, deeplink, appId) {
|
|
|
327535
327708
|
body: JSON.stringify({ script: "mobile: deepLink", args })
|
|
327536
327709
|
});
|
|
327537
327710
|
}
|
|
327711
|
+
function readExecuteStringValue(result) {
|
|
327712
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327713
|
+
const value = result.value;
|
|
327714
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
327715
|
+
}
|
|
327716
|
+
return void 0;
|
|
327717
|
+
}
|
|
327718
|
+
function readActiveBundleId(result) {
|
|
327719
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327720
|
+
const value = result.value;
|
|
327721
|
+
if (value && typeof value === "object" && "bundleId" in value) {
|
|
327722
|
+
const bundleId = value.bundleId;
|
|
327723
|
+
if (typeof bundleId === "string" && bundleId.trim()) return bundleId.trim();
|
|
327724
|
+
}
|
|
327725
|
+
}
|
|
327726
|
+
return void 0;
|
|
327727
|
+
}
|
|
327728
|
+
function normalizeAndroidActivity(activity, pkg2) {
|
|
327729
|
+
const trimmed = activity?.trim();
|
|
327730
|
+
if (!trimmed) return void 0;
|
|
327731
|
+
const packageName = pkg2?.trim();
|
|
327732
|
+
if (!packageName) return trimmed;
|
|
327733
|
+
if (trimmed.startsWith(".")) return `${packageName}${trimmed}`;
|
|
327734
|
+
if (!trimmed.includes(".")) return `${packageName}.${trimmed}`;
|
|
327735
|
+
return trimmed;
|
|
327736
|
+
}
|
|
327737
|
+
async function executeMobileScript(sessionId, script, args) {
|
|
327738
|
+
return wdRequest(`/session/${sessionId}/execute/sync`, {
|
|
327739
|
+
method: "POST",
|
|
327740
|
+
body: JSON.stringify({ script, args })
|
|
327741
|
+
});
|
|
327742
|
+
}
|
|
327743
|
+
async function readForegroundScreenSignal(sessionId, platform3) {
|
|
327744
|
+
if (platform3 === "ANDROID") {
|
|
327745
|
+
const [packageResult, activityResult] = await Promise.all([
|
|
327746
|
+
executeMobileScript(sessionId, "mobile: getCurrentPackage", []),
|
|
327747
|
+
executeMobileScript(sessionId, "mobile: getCurrentActivity", []).catch(() => void 0)
|
|
327748
|
+
]);
|
|
327749
|
+
const pkg2 = readExecuteStringValue(packageResult);
|
|
327750
|
+
if (!pkg2) return null;
|
|
327751
|
+
const activity = normalizeAndroidActivity(readExecuteStringValue(activityResult), pkg2);
|
|
327752
|
+
return {
|
|
327753
|
+
platform: platform3,
|
|
327754
|
+
bundleId: pkg2,
|
|
327755
|
+
...activity ? { activity } : {},
|
|
327756
|
+
screenHash: "foreground"
|
|
327757
|
+
};
|
|
327758
|
+
}
|
|
327759
|
+
const infoResult = await executeMobileScript(sessionId, "mobile: activeAppInfo", []);
|
|
327760
|
+
const bundleId = readActiveBundleId(infoResult);
|
|
327761
|
+
if (!bundleId) return null;
|
|
327762
|
+
return { platform: platform3, bundleId, screenHash: "foreground" };
|
|
327763
|
+
}
|
|
327764
|
+
async function activateTargetApp(sessionId, platform3, appId) {
|
|
327765
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
327766
|
+
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
327767
|
+
}
|
|
327768
|
+
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
327769
|
+
let boundary = null;
|
|
327770
|
+
try {
|
|
327771
|
+
const signal = await readForegroundScreenSignal(sessionId, target.platform);
|
|
327772
|
+
boundary = signal ? (0, import_runner_core8.classifyMobileScreenBoundary)(signal, target.appId, target.platform) : null;
|
|
327773
|
+
} catch (err) {
|
|
327774
|
+
log2(` App boundary check skipped after ${source}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327775
|
+
return;
|
|
327776
|
+
}
|
|
327777
|
+
if (!boundary || (0, import_runner_core8.isTargetLikeMobileBoundary)(boundary) || boundary.kind === "transient_external") {
|
|
327778
|
+
return;
|
|
327779
|
+
}
|
|
327780
|
+
const message = `Foreground app changed to ${boundary.owner}, outside target app ${boundary.targetAppId}`;
|
|
327781
|
+
log2(` ${message}; activating target app.`);
|
|
327782
|
+
try {
|
|
327783
|
+
await activateTargetApp(sessionId, target.platform, target.appId);
|
|
327784
|
+
} catch (err) {
|
|
327785
|
+
log2(` Could not reactivate ${target.appId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327786
|
+
}
|
|
327787
|
+
if (failOnExternal) {
|
|
327788
|
+
throw new Error(`${message}; relaunched target app and failed the step so mobile tests do not continue in another app.`);
|
|
327789
|
+
}
|
|
327790
|
+
}
|
|
327538
327791
|
async function deleteSession(sessionId) {
|
|
327539
327792
|
try {
|
|
327540
327793
|
await wdRequest(`/session/${sessionId}`, { method: "DELETE" });
|
|
@@ -327661,6 +327914,29 @@ async function getActiveElementId(sessionId) {
|
|
|
327661
327914
|
return null;
|
|
327662
327915
|
}
|
|
327663
327916
|
}
|
|
327917
|
+
async function sendKeysToFocusedInput(sessionId, value) {
|
|
327918
|
+
await wdRequest(`/session/${sessionId}/keys`, {
|
|
327919
|
+
method: "POST",
|
|
327920
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
327921
|
+
});
|
|
327922
|
+
}
|
|
327923
|
+
function shouldAbortAfterStepFailure(action) {
|
|
327924
|
+
switch (action) {
|
|
327925
|
+
case "launchApp":
|
|
327926
|
+
case "tap":
|
|
327927
|
+
case "type":
|
|
327928
|
+
case "clear":
|
|
327929
|
+
case "swipe":
|
|
327930
|
+
case "scroll":
|
|
327931
|
+
case "back":
|
|
327932
|
+
case "home":
|
|
327933
|
+
case "waitForElement":
|
|
327934
|
+
return true;
|
|
327935
|
+
case "assertVisible":
|
|
327936
|
+
case "assertText":
|
|
327937
|
+
return false;
|
|
327938
|
+
}
|
|
327939
|
+
}
|
|
327664
327940
|
async function executeTap(sessionId, step) {
|
|
327665
327941
|
const bounds = boundsFromStep(step);
|
|
327666
327942
|
if (step.target) {
|
|
@@ -327682,8 +327958,10 @@ async function executeTap(sessionId, step) {
|
|
|
327682
327958
|
}
|
|
327683
327959
|
async function executeType(sessionId, step) {
|
|
327684
327960
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
327961
|
+
const value = String(step.value);
|
|
327685
327962
|
const bounds = boundsFromStep(step);
|
|
327686
327963
|
let elementId = null;
|
|
327964
|
+
let tappedBounds = false;
|
|
327687
327965
|
if (step.target) {
|
|
327688
327966
|
try {
|
|
327689
327967
|
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
@@ -327693,19 +327971,33 @@ async function executeType(sessionId, step) {
|
|
|
327693
327971
|
}
|
|
327694
327972
|
if (!elementId && bounds) {
|
|
327695
327973
|
await tapCoordinates(sessionId, bounds);
|
|
327974
|
+
tappedBounds = true;
|
|
327696
327975
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327697
327976
|
elementId = await getActiveElementId(sessionId);
|
|
327698
327977
|
}
|
|
327699
327978
|
if (!elementId) {
|
|
327700
327979
|
elementId = await getActiveElementId(sessionId);
|
|
327701
327980
|
}
|
|
327702
|
-
if (
|
|
327703
|
-
|
|
327981
|
+
if (elementId) {
|
|
327982
|
+
try {
|
|
327983
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
|
|
327984
|
+
method: "POST",
|
|
327985
|
+
body: JSON.stringify({ text: value })
|
|
327986
|
+
});
|
|
327987
|
+
return;
|
|
327988
|
+
} catch (error2) {
|
|
327989
|
+
if (!bounds) throw error2;
|
|
327990
|
+
}
|
|
327704
327991
|
}
|
|
327705
|
-
|
|
327706
|
-
|
|
327707
|
-
|
|
327708
|
-
|
|
327992
|
+
if (bounds) {
|
|
327993
|
+
if (!tappedBounds) {
|
|
327994
|
+
await tapCoordinates(sessionId, bounds);
|
|
327995
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327996
|
+
}
|
|
327997
|
+
await sendKeysToFocusedInput(sessionId, value);
|
|
327998
|
+
return;
|
|
327999
|
+
}
|
|
328000
|
+
throw new Error("TYPE step could not resolve a target or focused element");
|
|
327709
328001
|
}
|
|
327710
328002
|
async function executeClear(sessionId, step) {
|
|
327711
328003
|
const bounds = boundsFromStep(step);
|
|
@@ -327909,6 +328201,7 @@ async function executeMobileTest(options) {
|
|
|
327909
328201
|
log2(` \u2717 Deep link failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
327910
328202
|
}
|
|
327911
328203
|
}
|
|
328204
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
327912
328205
|
let environmental = null;
|
|
327913
328206
|
for (const step of options.steps) {
|
|
327914
328207
|
const stepStart = Date.now();
|
|
@@ -327916,6 +328209,7 @@ async function executeMobileTest(options) {
|
|
|
327916
328209
|
let status = "passed";
|
|
327917
328210
|
let stepError;
|
|
327918
328211
|
try {
|
|
328212
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} pre-check`, false);
|
|
327919
328213
|
switch (step.action) {
|
|
327920
328214
|
case "launchApp":
|
|
327921
328215
|
log2(" App launched (handled by session creation)");
|
|
@@ -327955,6 +328249,7 @@ async function executeMobileTest(options) {
|
|
|
327955
328249
|
break;
|
|
327956
328250
|
}
|
|
327957
328251
|
}
|
|
328252
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
|
|
327958
328253
|
} catch (err) {
|
|
327959
328254
|
status = "failed";
|
|
327960
328255
|
stepError = err instanceof Error ? err.message : String(err);
|
|
@@ -327975,6 +328270,10 @@ async function executeMobileTest(options) {
|
|
|
327975
328270
|
log2(`Aborting remaining steps \u2014 Appium session is no longer usable: ${environmental}`);
|
|
327976
328271
|
break;
|
|
327977
328272
|
}
|
|
328273
|
+
if (status === "failed" && shouldAbortAfterStepFailure(step.action)) {
|
|
328274
|
+
log2(`Aborting remaining steps \u2014 ${step.action} failed, so downstream mobile steps are no longer reliable.`);
|
|
328275
|
+
break;
|
|
328276
|
+
}
|
|
327978
328277
|
}
|
|
327979
328278
|
const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
327980
328279
|
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
|
|
@@ -329240,7 +329539,7 @@ function detachMirrorSession(sessionId) {
|
|
|
329240
329539
|
}
|
|
329241
329540
|
|
|
329242
329541
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
329243
|
-
var
|
|
329542
|
+
var import_runner_core9 = __toESM(require_dist2());
|
|
329244
329543
|
init_dist();
|
|
329245
329544
|
var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
|
|
329246
329545
|
function readStringValue(result) {
|
|
@@ -329250,7 +329549,7 @@ function readStringValue(result) {
|
|
|
329250
329549
|
}
|
|
329251
329550
|
return void 0;
|
|
329252
329551
|
}
|
|
329253
|
-
function
|
|
329552
|
+
function normalizeAndroidActivity2(activity, pkg2) {
|
|
329254
329553
|
const trimmed = activity?.trim();
|
|
329255
329554
|
if (!trimmed) return void 0;
|
|
329256
329555
|
const packageName = pkg2?.trim();
|
|
@@ -329286,7 +329585,7 @@ var MobileBridgeDriver = class {
|
|
|
329286
329585
|
return this.platform === "ANDROID" ? { appId: this.appId } : { bundleId: this.appId };
|
|
329287
329586
|
}
|
|
329288
329587
|
baseScreenSignal(source) {
|
|
329289
|
-
const parsed = (0,
|
|
329588
|
+
const parsed = (0, import_runner_core9.parseMobileSource)(source, this.platform);
|
|
329290
329589
|
return {
|
|
329291
329590
|
platform: this.platform,
|
|
329292
329591
|
screenHash: parsed.screenSignal.screenHash,
|
|
@@ -329305,11 +329604,11 @@ var MobileBridgeDriver = class {
|
|
|
329305
329604
|
const activity = readStringValue(activityRes);
|
|
329306
329605
|
const pkg2 = readStringValue(packageRes);
|
|
329307
329606
|
if (pkg2) signal.bundleId = pkg2;
|
|
329308
|
-
const normalizedActivity =
|
|
329607
|
+
const normalizedActivity = normalizeAndroidActivity2(activity, pkg2);
|
|
329309
329608
|
if (normalizedActivity) signal.activity = normalizedActivity;
|
|
329310
329609
|
} else {
|
|
329311
329610
|
const infoRes = await this.executeScript("mobile: activeAppInfo", []).catch(() => void 0);
|
|
329312
|
-
const bundleId =
|
|
329611
|
+
const bundleId = readActiveBundleId2(infoRes);
|
|
329313
329612
|
if (bundleId) signal.bundleId = bundleId;
|
|
329314
329613
|
}
|
|
329315
329614
|
} catch {
|
|
@@ -329451,7 +329750,7 @@ var MobileBridgeDriver = class {
|
|
|
329451
329750
|
return this.enrichScreenSignal(source);
|
|
329452
329751
|
}
|
|
329453
329752
|
};
|
|
329454
|
-
function
|
|
329753
|
+
function readActiveBundleId2(result) {
|
|
329455
329754
|
if (result && typeof result === "object" && "value" in result) {
|
|
329456
329755
|
const value = result.value;
|
|
329457
329756
|
if (value && typeof value === "object" && "bundleId" in value) {
|
|
@@ -329677,7 +329976,7 @@ function printDoctorReport(report) {
|
|
|
329677
329976
|
|
|
329678
329977
|
// src/runner.ts
|
|
329679
329978
|
init_dist();
|
|
329680
|
-
var
|
|
329979
|
+
var import_runner_core12 = __toESM(require_dist2());
|
|
329681
329980
|
function fingerprintTest(test) {
|
|
329682
329981
|
const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
|
|
329683
329982
|
return (0, import_node_crypto2.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);
|
|
@@ -330122,7 +330421,7 @@ function pruneCollectedPageForPost(page) {
|
|
|
330122
330421
|
if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
|
|
330123
330422
|
return out;
|
|
330124
330423
|
}
|
|
330125
|
-
function prepareDiscoveryResultForPost(result, logs2) {
|
|
330424
|
+
function prepareDiscoveryResultForPost(result, logs2, options = {}) {
|
|
330126
330425
|
const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
|
|
330127
330426
|
if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
|
|
330128
330427
|
if (Array.isArray(safe.apiCalls)) safe.apiCalls = safe.apiCalls.map((call) => compactForAudit(call));
|
|
@@ -330138,6 +330437,10 @@ function prepareDiscoveryResultForPost(result, logs2) {
|
|
|
330138
330437
|
if (safe.snapshotEvidence) safe.snapshotEvidence = compactForAudit(safe.snapshotEvidence);
|
|
330139
330438
|
if (safe.discoveryCheckpoints) safe.discoveryCheckpoints = compactForAudit(safe.discoveryCheckpoints);
|
|
330140
330439
|
if (safe.pageScreenshotKeys && safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330440
|
+
if (options.omitScreenshots) {
|
|
330441
|
+
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330442
|
+
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330443
|
+
}
|
|
330141
330444
|
if (safe.exploreAlreadySaved === true) {
|
|
330142
330445
|
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330143
330446
|
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
@@ -330237,7 +330540,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330237
330540
|
const runMode = run2.mode ?? "run";
|
|
330238
330541
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
330239
330542
|
log.info(`Running test: ${run2.testCaseId ?? "auth-setup"} (run ${run2.runId}) [mode: ${runMode === "heal" ? "heal" : runMode === "auth-setup" ? "auth-setup" : RUNNER_MODE}]`);
|
|
330240
|
-
const liveBrowserHandle =
|
|
330543
|
+
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
330241
330544
|
sessionId: `${run2.runId}`,
|
|
330242
330545
|
mode: toLiveBrowserMode(runMode),
|
|
330243
330546
|
runId: run2.runId,
|
|
@@ -330246,7 +330549,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330246
330549
|
testCaseId: run2.testCaseId ?? void 0
|
|
330247
330550
|
});
|
|
330248
330551
|
requestLiveBrowserHeartbeat(true);
|
|
330249
|
-
const liveBrowserHeartbeatInterval = setInterval(() =>
|
|
330552
|
+
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
330553
|
+
liveBrowserHandle.patch({});
|
|
330554
|
+
requestLiveBrowserHeartbeat();
|
|
330555
|
+
}, 5e3);
|
|
330250
330556
|
liveBrowserHeartbeatInterval.unref?.();
|
|
330251
330557
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
330252
330558
|
...nativeOptions,
|
|
@@ -330273,7 +330579,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330273
330579
|
log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
|
|
330274
330580
|
return;
|
|
330275
330581
|
}
|
|
330276
|
-
const preflight = await (0,
|
|
330582
|
+
const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
|
|
330277
330583
|
if (preflight.error) {
|
|
330278
330584
|
await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
|
|
330279
330585
|
log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
|
|
@@ -330330,7 +330636,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330330
330636
|
return;
|
|
330331
330637
|
}
|
|
330332
330638
|
const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
|
|
330333
|
-
const batchResult = await (0,
|
|
330639
|
+
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
330334
330640
|
testVariables: run2.testVariables,
|
|
330335
330641
|
runNativeTest: runNativeWithLiveBrowser,
|
|
330336
330642
|
validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
|
|
@@ -330564,7 +330870,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330564
330870
|
if (classifyErrorMessage(composedError ?? null) === "config_issue") {
|
|
330565
330871
|
apiHealStatus = "ERROR";
|
|
330566
330872
|
} else {
|
|
330567
|
-
const rlDetection = (0,
|
|
330873
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330568
330874
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330569
330875
|
tags: run2.tags ?? [],
|
|
330570
330876
|
error: composedError ?? finalVerifyResult?.error ?? null,
|
|
@@ -330671,7 +330977,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330671
330977
|
log.info(`Heal engine: GROK (${source})`);
|
|
330672
330978
|
}
|
|
330673
330979
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
330674
|
-
const result2 = useGrokHeal ? await (0,
|
|
330980
|
+
const result2 = useGrokHeal ? await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
330675
330981
|
...healOpts,
|
|
330676
330982
|
onLog: grokHealLogStream.onLog
|
|
330677
330983
|
}) : await (0, import_runner_core2.executeTestWithHealing)(run2.playwrightCode, run2.steps, run2.baseUrl, healOpts);
|
|
@@ -330704,7 +331010,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330704
331010
|
let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
|
|
330705
331011
|
let healError = result2.error;
|
|
330706
331012
|
if (!result2.passed && !isConfigIssue) {
|
|
330707
|
-
const rlDetection = (0,
|
|
331013
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330708
331014
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330709
331015
|
tags: run2.tags ?? [],
|
|
330710
331016
|
error: result2.error,
|
|
@@ -330898,7 +331204,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330898
331204
|
liveBrowserHandle,
|
|
330899
331205
|
runNativeTest: runNativeWithLiveBrowser
|
|
330900
331206
|
};
|
|
330901
|
-
const healResult = useGrokHealSuite ? await (0,
|
|
331207
|
+
const healResult = useGrokHealSuite ? await (0, import_runner_core10.executeGrokPerTestHeal)(
|
|
330902
331208
|
failedTest.playwrightCode,
|
|
330903
331209
|
failedTest.steps ?? [],
|
|
330904
331210
|
run2.baseUrl,
|
|
@@ -331186,9 +331492,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331186
331492
|
signal: AbortSignal.timeout(3e4)
|
|
331187
331493
|
});
|
|
331188
331494
|
if (res.ok) return;
|
|
331189
|
-
|
|
331495
|
+
const responseText = await res.text().catch(() => "");
|
|
331496
|
+
const statusMessage = responseText ? `${res.status}: ${responseText.slice(0, 500)}` : String(res.status);
|
|
331497
|
+
if (res.status === 409) {
|
|
331498
|
+
throw new Error(`Discovery plan checkpoint rejected: ${statusMessage}`);
|
|
331499
|
+
}
|
|
331500
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} returned ${statusMessage}`);
|
|
331190
331501
|
} catch (err) {
|
|
331191
|
-
|
|
331502
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331503
|
+
if (/Discovery plan checkpoint rejected/i.test(message)) throw err;
|
|
331504
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} failed: ${message}`);
|
|
331192
331505
|
}
|
|
331193
331506
|
if (attempt < 2) await new Promise((r) => setTimeout(r, 3e3 * (attempt + 1)));
|
|
331194
331507
|
}
|
|
@@ -331196,6 +331509,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331196
331509
|
};
|
|
331197
331510
|
let discoveryResult;
|
|
331198
331511
|
const buildFinalBody = (r, overrideError, auditWarning) => {
|
|
331512
|
+
const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
|
|
331199
331513
|
const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
|
|
331200
331514
|
void _exploreFinalMessages;
|
|
331201
331515
|
const logs2 = auditWarning ? [rest.logs, auditWarning].filter(Boolean).join("\n") : rest.logs;
|
|
@@ -331245,7 +331559,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331245
331559
|
resolvedAppOrigin: rest.resolvedAppOrigin,
|
|
331246
331560
|
plans: rest.plans,
|
|
331247
331561
|
exploreStats: rest.exploreStats,
|
|
331248
|
-
screenshots: rest.screenshots,
|
|
331562
|
+
...isNativeDiscovery ? {} : { screenshots: rest.screenshots },
|
|
331249
331563
|
snapshotEvidence: rest.snapshotEvidence,
|
|
331250
331564
|
discoveryCheckpoints: rest.discoveryCheckpoints,
|
|
331251
331565
|
streamedTestCount: streamedTestKeys.size,
|
|
@@ -331254,8 +331568,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331254
331568
|
...explorePosted ? {} : {
|
|
331255
331569
|
collectedPages: rest.collectedPages,
|
|
331256
331570
|
collectedTransitions: rest.collectedTransitions,
|
|
331257
|
-
|
|
331258
|
-
|
|
331571
|
+
...isNativeDiscovery ? {} : {
|
|
331572
|
+
pageScreenshots: rest.pageScreenshots,
|
|
331573
|
+
pageScreenshotKeys: rest.pageScreenshotKeys
|
|
331574
|
+
}
|
|
331259
331575
|
},
|
|
331260
331576
|
...harPosted ? {} : {
|
|
331261
331577
|
apiCalls: rest.apiCalls,
|
|
@@ -331263,7 +331579,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331263
331579
|
},
|
|
331264
331580
|
// Only send tests that were NOT confirmed streamed to avoid duplicates
|
|
331265
331581
|
tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
|
|
331266
|
-
}, logs2);
|
|
331582
|
+
}, logs2, { omitScreenshots: isNativeDiscovery });
|
|
331267
331583
|
};
|
|
331268
331584
|
const acquireLoginBrowser = async () => {
|
|
331269
331585
|
const browser = await browserPool.acquireAsync(12e3);
|
|
@@ -331367,7 +331683,6 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331367
331683
|
thumbnailCapturedAt: Date.now()
|
|
331368
331684
|
});
|
|
331369
331685
|
requestLiveBrowserHeartbeat();
|
|
331370
|
-
void onScreenshot("mobile", base64);
|
|
331371
331686
|
},
|
|
331372
331687
|
onExploreComplete,
|
|
331373
331688
|
onTestGenerated,
|
|
@@ -331394,7 +331709,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331394
331709
|
setExecutorBusy(false);
|
|
331395
331710
|
}
|
|
331396
331711
|
} else if (ctx.mode === "import") {
|
|
331397
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
331712
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_runner_core10.runFrameworkImportOnRunner;
|
|
331398
331713
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
331399
331714
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
331400
331715
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -331724,7 +332039,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331724
332039
|
let runStatus = result.passed ? "PASSED" : "FAILED";
|
|
331725
332040
|
let runError = result.error;
|
|
331726
332041
|
if (!result.passed) {
|
|
331727
|
-
const rlDetection = (0,
|
|
332042
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
331728
332043
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
331729
332044
|
tags: run2.tags ?? [],
|
|
331730
332045
|
error: result.error,
|
|
@@ -331862,7 +332177,7 @@ async function startRunner(opts) {
|
|
|
331862
332177
|
capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
|
|
331863
332178
|
run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
|
|
331864
332179
|
};
|
|
331865
|
-
const RESOURCE_GOVERNOR_OPTIONS = (0,
|
|
332180
|
+
const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
|
|
331866
332181
|
isCloud: IS_CLOUD,
|
|
331867
332182
|
maxConcurrent: MAX_CONCURRENT,
|
|
331868
332183
|
typeLimits: TYPE_LIMITS
|
|
@@ -332013,8 +332328,8 @@ async function startRunner(opts) {
|
|
|
332013
332328
|
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
332014
332329
|
} : { web: true, ios: false, android: false };
|
|
332015
332330
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
332016
|
-
const resourceStatus = (0,
|
|
332017
|
-
const processInventory = await (0,
|
|
332331
|
+
const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
332332
|
+
const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
|
|
332018
332333
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
332019
332334
|
lastInventoryKinds.clear();
|
|
332020
332335
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
@@ -332041,8 +332356,8 @@ async function startRunner(opts) {
|
|
|
332041
332356
|
resourceSnapshot: resourceStatus.snapshot,
|
|
332042
332357
|
resourceGovernor: resourceStatus.governor,
|
|
332043
332358
|
processInventory,
|
|
332044
|
-
aiQueue: (0,
|
|
332045
|
-
liveBrowsers:
|
|
332359
|
+
aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
|
|
332360
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
332046
332361
|
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
332047
332362
|
// Cached & cheap — see getCachedDoctor above.
|
|
332048
332363
|
doctor: getCachedDoctor()
|
|
@@ -332136,7 +332451,7 @@ async function startRunner(opts) {
|
|
|
332136
332451
|
let pidsWatchdogWarned = false;
|
|
332137
332452
|
const pidsWatchdog = setInterval(() => {
|
|
332138
332453
|
if (shuttingDown) return;
|
|
332139
|
-
const snapshot = (0,
|
|
332454
|
+
const snapshot = (0, import_runner_core12.getResourceSnapshot)();
|
|
332140
332455
|
const pct = snapshot.pidsPct;
|
|
332141
332456
|
if (pct == null || !Number.isFinite(pct)) return;
|
|
332142
332457
|
if (pct >= PIDS_RECYCLE_PCT) {
|
|
@@ -332172,7 +332487,7 @@ async function startRunner(opts) {
|
|
|
332172
332487
|
log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
332173
332488
|
continue;
|
|
332174
332489
|
}
|
|
332175
|
-
const admission = (0,
|
|
332490
|
+
const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
332176
332491
|
mode: runMode,
|
|
332177
332492
|
activeRuns,
|
|
332178
332493
|
activeByType: Object.fromEntries(activeByType),
|