Skip to content

Commit 9e88f31

Browse files
committed
update docs and cleanup
1 parent 2b3b284 commit 9e88f31

2 files changed

Lines changed: 68 additions & 41 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ App state:
183183
- Built-in retries cover transient runner connection failures, AX snapshot hiccups, and Android UI dumps.
184184
- For snapshot issues (missing elements), compare with `--raw` flag for unaltered output and scope with `-s "<label>"`.
185185

186+
Boot diagnostics:
187+
- Boot failures include normalized reason codes in `error.details.reason` (JSON mode) and verbose logs.
188+
- Reason codes: `BOOT_TIMEOUT`, `DEVICE_UNAVAILABLE`, `DEVICE_OFFLINE`, `PERMISSION_DENIED`, `TOOL_MISSING`, `BOOT_COMMAND_FAILED`, `UNKNOWN`.
189+
- Android boot waits fail fast for permission/tooling issues and do not always collapse into timeout errors.
190+
186191
## App resolution
187192
- Bundle/package identifiers are accepted directly (e.g., `com.apple.Preferences`).
188193
- Human-readable names are resolved when possible (e.g., `Settings`).
@@ -199,6 +204,14 @@ App state:
199204
pnpm test
200205
```
201206

207+
Useful local checks:
208+
209+
```bash
210+
pnpm typecheck
211+
pnpm test:unit
212+
pnpm test:smoke
213+
```
214+
202215
## Build
203216

204217
```bash
@@ -208,6 +221,7 @@ pnpm build
208221
Environment selectors:
209222
- `ANDROID_DEVICE=Pixel_9_Pro_XL` or `ANDROID_SERIAL=emulator-5554`
210223
- `IOS_DEVICE="iPhone 17 Pro"` or `IOS_UDID=<udid>`
224+
- `AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS=<ms>` to adjust iOS simulator boot timeout (default: `120000`, minimum: `5000`).
211225

212226
Test screenshots are written to:
213227
- `test/screenshots/android-settings.png`

src/platforms/android/devices.ts

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ import type { DeviceInfo } from '../../utils/device.ts';
55
import { Deadline, retryWithPolicy } from '../../utils/retry.ts';
66
import { classifyBootFailure } from '../boot-diagnostics.ts';
77

8+
const EMULATOR_SERIAL_PREFIX = 'emulator-';
9+
const ANDROID_BOOT_POLL_MS = 1000;
10+
11+
function adbArgs(serial: string, args: string[]): string[] {
12+
return ['-s', serial, ...args];
13+
}
14+
15+
function isEmulatorSerial(serial: string): boolean {
16+
return serial.startsWith(EMULATOR_SERIAL_PREFIX);
17+
}
18+
19+
async function readAndroidBootProp(serial: string): Promise<ExecResult> {
20+
return runCmd('adb', adbArgs(serial, ['shell', 'getprop', 'sys.boot_completed']), {
21+
allowFailure: true,
22+
});
23+
}
24+
25+
async function resolveAndroidDeviceName(serial: string, rawModel: string): Promise<string> {
26+
const modelName = rawModel.replace(/_/g, ' ').trim();
27+
if (!isEmulatorSerial(serial)) return modelName || serial;
28+
const avd = await runCmd('adb', adbArgs(serial, ['emu', 'avd', 'name']), {
29+
allowFailure: true,
30+
});
31+
const avdName = avd.stdout.trim();
32+
if (avd.exitCode === 0 && avdName) {
33+
return avdName.replace(/_/g, ' ');
34+
}
35+
return modelName || serial;
36+
}
37+
838
export async function listAndroidDevices(): Promise<DeviceInfo[]> {
939
const adbAvailable = await whichCmd('adb');
1040
if (!adbAvailable) {
@@ -13,57 +43,44 @@ export async function listAndroidDevices(): Promise<DeviceInfo[]> {
1343

1444
const result = await runCmd('adb', ['devices', '-l']);
1545
const lines = result.stdout.split('\n').map((l: string) => l.trim());
16-
const devices: DeviceInfo[] = [];
46+
const entries = lines
47+
.filter((line) => line.length > 0 && !line.startsWith('List of devices'))
48+
.map((line) => line.split(/\s+/))
49+
.filter((parts) => parts[1] === 'device')
50+
.map((parts) => ({
51+
serial: parts[0],
52+
rawModel: (parts.find((p: string) => p.startsWith('model:')) ?? '').replace('model:', ''),
53+
}));
1754

18-
for (const line of lines) {
19-
if (!line || line.startsWith('List of devices')) continue;
20-
const parts = line.split(/\s+/);
21-
const serial = parts[0];
22-
const state = parts[1];
23-
if (state !== 'device') continue;
24-
25-
const modelPart = parts.find((p: string) => p.startsWith('model:')) ?? '';
26-
const rawModel = modelPart.replace('model:', '').replace(/_/g, ' ').trim();
27-
let name = rawModel || serial;
28-
29-
if (serial.startsWith('emulator-')) {
30-
const avd = await runCmd('adb', ['-s', serial, 'emu', 'avd', 'name'], {
31-
allowFailure: true,
32-
});
33-
const avdName = (avd.stdout as string).trim();
34-
if (avd.exitCode === 0 && avdName) {
35-
name = avdName.replace(/_/g, ' ');
36-
}
37-
}
38-
39-
const booted = await isAndroidBooted(serial);
40-
41-
devices.push({
55+
const devices = await Promise.all(entries.map(async ({ serial, rawModel }) => {
56+
const [name, booted] = await Promise.all([
57+
resolveAndroidDeviceName(serial, rawModel),
58+
isAndroidBooted(serial),
59+
]);
60+
return {
4261
platform: 'android',
4362
id: serial,
4463
name,
45-
kind: serial.startsWith('emulator-') ? 'emulator' : 'device',
64+
kind: isEmulatorSerial(serial) ? 'emulator' : 'device',
4665
booted,
47-
});
48-
}
66+
} satisfies DeviceInfo;
67+
}));
4968

5069
return devices;
5170
}
5271

5372
export async function isAndroidBooted(serial: string): Promise<boolean> {
5473
try {
55-
const result = await runCmd('adb', ['-s', serial, 'shell', 'getprop', 'sys.boot_completed'], {
56-
allowFailure: true,
57-
});
58-
return (result.stdout as string).trim() === '1';
74+
const result = await readAndroidBootProp(serial);
75+
return result.stdout.trim() === '1';
5976
} catch {
6077
return false;
6178
}
6279
}
6380

6481
export async function waitForAndroidBoot(serial: string, timeoutMs = 60000): Promise<void> {
6582
const deadline = Deadline.fromTimeoutMs(timeoutMs);
66-
const maxAttempts = Math.max(1, Math.ceil(timeoutMs / 1000));
83+
const maxAttempts = Math.max(1, Math.ceil(timeoutMs / ANDROID_BOOT_POLL_MS));
6784
let lastBootResult: ExecResult | undefined;
6885
let timedOut = false;
6986
try {
@@ -78,13 +95,9 @@ export async function waitForAndroidBoot(serial: string, timeoutMs = 60000): Pro
7895
message: 'timeout',
7996
});
8097
}
81-
const result = await runCmd(
82-
'adb',
83-
['-s', serial, 'shell', 'getprop', 'sys.boot_completed'],
84-
{ allowFailure: true },
85-
);
98+
const result = await readAndroidBootProp(serial);
8699
lastBootResult = result;
87-
if ((result.stdout as string).trim() === '1') return;
100+
if (result.stdout.trim() === '1') return;
88101
throw new AppError('COMMAND_FAILED', 'Android device is still booting', {
89102
serial,
90103
stdout: result.stdout,
@@ -94,8 +107,8 @@ export async function waitForAndroidBoot(serial: string, timeoutMs = 60000): Pro
94107
},
95108
{
96109
maxAttempts,
97-
baseDelayMs: 1000,
98-
maxDelayMs: 1000,
110+
baseDelayMs: ANDROID_BOOT_POLL_MS,
111+
maxDelayMs: ANDROID_BOOT_POLL_MS,
99112
jitter: 0,
100113
shouldRetry: (error) => {
101114
const reason = classifyBootFailure({

0 commit comments

Comments
 (0)