@aarwitz/tapp 0.17.0-rc.3 → 0.17.0-rc.5

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.
@@ -139,6 +139,13 @@ export function isAndroidAppSnapshot(snapshot, appId) {
139
139
  return ownsActivity || ownsElements;
140
140
  }
141
141
 
142
+ function isSystemOverlayOverApp(snapshot, appId) {
143
+ if (!snapshot || !appId || !String(snapshot.activity || "").startsWith(`${appId}/`)) return false;
144
+ const packages = new Set((snapshot.elements || []).map((element) => element.package).filter(Boolean));
145
+ return packages.size > 0 && !packages.has(appId)
146
+ && [...packages].every((name) => name === "android" || name.startsWith("com.android.systemui"));
147
+ }
148
+
142
149
  export class AndroidDriver {
143
150
  constructor({ adbPath = resolveAdbPath(), serial = "", appId = "" } = {}) {
144
151
  if (!adbPath) throw new Error("Android testing needs adb. Install Android SDK platform-tools or set ANDROID_SDK_ROOT.");
@@ -205,7 +212,42 @@ export class AndroidDriver {
205
212
  const r = await this.adb(["shell", "am", "start", "-W", "-n", component], { timeout: 30_000 });
206
213
  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());
207
214
  await sleep(600);
208
- return this.snapshot();
215
+ return this.waitForOwnedSnapshot();
216
+ }
217
+
218
+ async closeSystemDialogs() {
219
+ // A crash dialog from the previous app can outlive that process and cover a
220
+ // newly launched app. The shell broadcast only closes system-owned surfaces;
221
+ // it does not clear crash history or interact with the app under test.
222
+ await this.adb(["shell", "am", "broadcast", "-a", "android.intent.action.CLOSE_SYSTEM_DIALOGS"]);
223
+ }
224
+
225
+ async waitForOwnedSnapshot(timeoutMs = 12_000) {
226
+ const deadline = Date.now() + timeoutMs;
227
+ let latest;
228
+ let consecutiveOwned = 0;
229
+ let overlayDismissed = false;
230
+ do {
231
+ latest = await this.snapshot();
232
+ if (isAndroidAppSnapshot(latest, this.appId)) {
233
+ consecutiveOwned += 1;
234
+ // One mixed dumpsys/UIAutomator sample caused cross-run contamination.
235
+ // Require agreement twice before handing the surface to exploration.
236
+ if (consecutiveOwned >= 2) return latest;
237
+ } else {
238
+ consecutiveOwned = 0;
239
+ if (!overlayDismissed && isSystemOverlayOverApp(latest, this.appId)) {
240
+ overlayDismissed = true;
241
+ await this.closeSystemDialogs();
242
+ }
243
+ }
244
+ if (Date.now() >= deadline) break;
245
+ // Activity and UIAutomator are intentionally required to agree. During app
246
+ // launch they may momentarily describe opposite sides of the transition;
247
+ // retry that mixed snapshot instead of turning it into zero-screen evidence.
248
+ await sleep(150);
249
+ } while (Date.now() < deadline);
250
+ return latest;
209
251
  }
210
252
 
211
253
  async currentActivity() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.17.0-rc.3",
3
+ "version": "0.17.0-rc.5",
4
4
  "mcpName": "io.github.aarwitz/tapp",
5
5
  "description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
6
6
  "license": "MIT",