@aarwitz/tapp 0.17.0-rc.2 → 0.17.0-rc.4
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]);
|
|
@@ -188,7 +205,22 @@ export class AndroidDriver {
|
|
|
188
205
|
const r = await this.adb(["shell", "am", "start", "-W", "-n", component], { timeout: 30_000 });
|
|
189
206
|
if (r.code !== 0 || !/Status:\s*ok/i.test(String(r.stdout))) throw new Error((r.stderr || r.stdout || `Could not launch ${this.appId}`).trim());
|
|
190
207
|
await sleep(600);
|
|
191
|
-
return this.
|
|
208
|
+
return this.waitForOwnedSnapshot();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async waitForOwnedSnapshot(timeoutMs = 4_000) {
|
|
212
|
+
const deadline = Date.now() + timeoutMs;
|
|
213
|
+
let latest;
|
|
214
|
+
do {
|
|
215
|
+
latest = await this.snapshot();
|
|
216
|
+
if (isAndroidAppSnapshot(latest, this.appId)) return latest;
|
|
217
|
+
if (Date.now() >= deadline) break;
|
|
218
|
+
// Activity and UIAutomator are intentionally required to agree. During app
|
|
219
|
+
// launch they may momentarily describe opposite sides of the transition;
|
|
220
|
+
// retry that mixed snapshot instead of turning it into zero-screen evidence.
|
|
221
|
+
await sleep(150);
|
|
222
|
+
} while (Date.now() < deadline);
|
|
223
|
+
return latest;
|
|
192
224
|
}
|
|
193
225
|
|
|
194
226
|
async currentActivity() {
|
|
@@ -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
|