@aarwitz/tapp 0.17.0-rc.2 → 0.17.0-rc.3
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.
|
@@ -41,6 +41,16 @@ function runFile(command, args, { encoding = "utf8", timeout = 30_000, maxBuffer
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export function parseLatestAndroidCrashExitInfo(output) {
|
|
45
|
+
const blocks = String(output || "").split(/ApplicationExitInfo #\d+:/).slice(1);
|
|
46
|
+
for (const block of blocks) {
|
|
47
|
+
const reason = block.match(/\breason=(4|5)\s+\((?:APP CRASH|NATIVE CRASH)/);
|
|
48
|
+
const identity = block.match(/\btimestamp=([^\n]+?)\s+pid=(\d+)\b/);
|
|
49
|
+
if (reason && identity) return `${identity[1].trim()}|${identity[2]}|${reason[1]}`;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
function entityDecode(value) {
|
|
45
55
|
return String(value || "")
|
|
46
56
|
.replaceAll(""", '"').replaceAll("'", "'")
|
|
@@ -171,6 +181,13 @@ export class AndroidDriver {
|
|
|
171
181
|
return r.code === 0 && /\d/.test(String(r.stdout || ""));
|
|
172
182
|
}
|
|
173
183
|
|
|
184
|
+
async latestCrashExitInfo() {
|
|
185
|
+
if (!this.appId) return null;
|
|
186
|
+
const r = await this.adb(["shell", "dumpsys", "activity", "exit-info", this.appId]);
|
|
187
|
+
if (r.code !== 0) return null;
|
|
188
|
+
return parseLatestAndroidCrashExitInfo(r.stdout);
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
async clearData() {
|
|
175
192
|
if (!this.appId) throw new Error("appId is required to clear Android app data");
|
|
176
193
|
const r = await this.adb(["shell", "pm", "clear", this.appId]);
|
|
@@ -9,6 +9,7 @@ import { semanticUiKey } from "./ui-map.js";
|
|
|
9
9
|
const ERROR_RE = /\b(something went wrong|internal server error|an error occurred|failed to load|unhandled exception|has stopped)\b/i;
|
|
10
10
|
const DESTRUCTIVE_RE = /\b(delete|remove|purchase|buy now|pay now|reset|erase|unsubscribe|sign out|log out|logout)\b/i;
|
|
11
11
|
const AUTH_SUBMIT_RE = /\b(sign[ -]?in|log[ -]?in|continue|submit)\b/i;
|
|
12
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
13
|
|
|
13
14
|
function stateHash(snap) {
|
|
14
15
|
return snap.elements.map((e) => `${androidElementKey(e)}:${e.text}:${e.x},${e.y}`).join("|");
|
|
@@ -72,12 +73,26 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
|
|
|
72
73
|
const visited = new Map();
|
|
73
74
|
let issues = 0;
|
|
74
75
|
let actions = 0;
|
|
76
|
+
const crashExitBaseline = typeof d.latestCrashExitInfo === "function" ? await d.latestCrashExitInfo() : null;
|
|
75
77
|
let snap = await d.launch({ clearData });
|
|
76
78
|
let crashReported = false;
|
|
77
79
|
|
|
78
80
|
const reportProcessExit = async (screen, step) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
if (typeof d.isProcessAlive !== "function") return false;
|
|
82
|
+
// Android may reveal the launcher before the crashing process disappears from
|
|
83
|
+
// pidof. A one-shot liveness sample made identical crashes scheduler-dependent.
|
|
84
|
+
// Poll only after app ownership is already lost: external intents and ordinary
|
|
85
|
+
// Back boundaries keep the originating process alive and remain boundaries.
|
|
86
|
+
let alive = await d.isProcessAlive();
|
|
87
|
+
let latestCrash = typeof d.latestCrashExitInfo === "function" ? await d.latestCrashExitInfo() : null;
|
|
88
|
+
const exitDeadline = Date.now() + 2_000;
|
|
89
|
+
while (alive && (!latestCrash || latestCrash === crashExitBaseline) && Date.now() < exitDeadline) {
|
|
90
|
+
await sleep(150);
|
|
91
|
+
alive = await d.isProcessAlive();
|
|
92
|
+
latestCrash = typeof d.latestCrashExitInfo === "function" ? await d.latestCrashExitInfo() : null;
|
|
93
|
+
}
|
|
94
|
+
const recordedCrash = !!latestCrash && latestCrash !== crashExitBaseline;
|
|
95
|
+
if ((alive && !recordedCrash) || crashReported) return false;
|
|
81
96
|
crashReported = true;
|
|
82
97
|
emit("ISSUE", { type: "crash", severity: "critical", title: "App process exited during exploration", screen: screen || "Launch", step });
|
|
83
98
|
issues += 1;
|
package/package.json
CHANGED
package/scripts/ci-gate.sh
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# Usage:
|
|
13
13
|
# scripts/ci-gate.sh [--platform ios] --app <path/to/App.app> [--bundle-id <com.example.app>]
|
|
14
|
-
# scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app>
|
|
14
|
+
# scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app> [--serial <adb-serial>]
|
|
15
15
|
# scripts/ci-gate.sh --platform web [--url <http(s)://owned-app>]
|
|
16
16
|
# # omit --url with --project-dir to detect/build/start/stop one owned web target
|
|
17
17
|
# # bundle id is detected from the .app when omitted
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
# [--json-out <file.json>] # write the full report (use as the next baseline)
|
|
32
32
|
# [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
|
|
33
33
|
# [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
|
|
34
|
+
# [--serial <adb-serial>] # Android emulator/device (default: first connected device)
|
|
34
35
|
#
|
|
35
36
|
# The app must be a SIMULATOR build (xcodebuild ... -destination 'generic/platform=iOS Simulator').
|
|
36
37
|
set -uo pipefail
|