Skip to content

Commit 748c0dd

Browse files
committed
ios appstate: prefer xctest before ax fallback
1 parent 4d2a161 commit 748c0dd

3 files changed

Lines changed: 131 additions & 34 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ Settings helpers:
192192
Note: iOS supports these only on simulators. iOS wifi/airplane toggles status bar indicators, not actual network state. Airplane off clears status bar overrides.
193193

194194
App state:
195-
- `appstate` shows the foreground app/activity (Android). On iOS it uses the current session app when available, otherwise it falls back to a snapshot-based guess (`xctest` on devices; AX-first on simulators with XCTest fallback).
195+
- `appstate` shows the foreground app/activity (Android). On iOS it uses the current session app when available, otherwise it uses a snapshot-based guess (XCTest first; AX fallback on simulators).
196196
- `apps --metadata` returns app list with minimal metadata.
197197

198198
## Debug
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import type { DeviceInfo } from '../../utils/device.ts';
4+
import { resolveIosAppStateFromSnapshots } from '../app-state.ts';
5+
import type { CommandFlags, dispatchCommand } from '../../core/dispatch.ts';
6+
7+
const iosSimulator: DeviceInfo = {
8+
platform: 'ios',
9+
id: 'sim-1',
10+
name: 'iPhone Simulator',
11+
kind: 'simulator',
12+
booted: true,
13+
};
14+
15+
const iosDevice: DeviceInfo = {
16+
platform: 'ios',
17+
id: '00008110-000E12341234002E',
18+
name: 'iPhone',
19+
kind: 'device',
20+
booted: true,
21+
};
22+
23+
test('appstate uses xctest first on iOS simulator', async () => {
24+
const backends: string[] = [];
25+
const fakeDispatch: typeof dispatchCommand = async (_device, _command, _positionals, _outPath, context) => {
26+
backends.push(context?.snapshotBackend ?? 'unknown');
27+
return {
28+
nodes: [
29+
{
30+
type: 'XCUIElementTypeApplication',
31+
label: 'Settings',
32+
identifier: 'com.apple.Preferences',
33+
},
34+
],
35+
};
36+
};
37+
38+
const result = await resolveIosAppStateFromSnapshots(
39+
iosSimulator,
40+
'/tmp/daemon.log',
41+
undefined,
42+
{} as CommandFlags,
43+
fakeDispatch,
44+
);
45+
46+
assert.deepEqual(backends, ['xctest']);
47+
assert.equal(result.source, 'snapshot-xctest');
48+
assert.equal(result.appBundleId, 'com.apple.Preferences');
49+
});
50+
51+
test('appstate falls back to ax on simulator when xctest is empty', async () => {
52+
const backends: string[] = [];
53+
const fakeDispatch: typeof dispatchCommand = async (_device, _command, _positionals, _outPath, context) => {
54+
const backend = context?.snapshotBackend ?? 'unknown';
55+
backends.push(backend);
56+
if (backend === 'xctest') {
57+
return { nodes: [] };
58+
}
59+
return {
60+
nodes: [
61+
{
62+
type: 'AXApplication',
63+
label: 'Calculator',
64+
identifier: 'com.apple.calculator',
65+
},
66+
],
67+
};
68+
};
69+
70+
const result = await resolveIosAppStateFromSnapshots(
71+
iosSimulator,
72+
'/tmp/daemon.log',
73+
undefined,
74+
{} as CommandFlags,
75+
fakeDispatch,
76+
);
77+
78+
assert.deepEqual(backends, ['xctest', 'ax']);
79+
assert.equal(result.source, 'snapshot-ax');
80+
assert.equal(result.appBundleId, 'com.apple.calculator');
81+
});
82+
83+
test('appstate does not try ax on iOS device', async () => {
84+
const backends: string[] = [];
85+
const fakeDispatch: typeof dispatchCommand = async (_device, _command, _positionals, _outPath, context) => {
86+
backends.push(context?.snapshotBackend ?? 'unknown');
87+
return { nodes: [] };
88+
};
89+
90+
const result = await resolveIosAppStateFromSnapshots(
91+
iosDevice,
92+
'/tmp/daemon.log',
93+
undefined,
94+
{} as CommandFlags,
95+
fakeDispatch,
96+
);
97+
98+
assert.deepEqual(backends, ['xctest']);
99+
assert.equal(result.source, 'snapshot-xctest');
100+
assert.equal(result.appName, 'unknown');
101+
});

src/daemon/app-state.ts

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,39 @@ export async function resolveIosAppStateFromSnapshots(
99
logPath: string,
1010
traceLogPath: string | undefined,
1111
flags: CommandFlags | undefined,
12+
dispatch: typeof dispatchCommand = dispatchCommand,
1213
): Promise<{ appName: string; appBundleId?: string; source: 'snapshot-ax' | 'snapshot-xctest' }> {
14+
const xctestResult = await dispatch(device, 'snapshot', [], flags?.out, {
15+
...contextFromFlags(
16+
logPath,
17+
{
18+
...flags,
19+
snapshotDepth: 1,
20+
snapshotCompact: true,
21+
snapshotBackend: 'xctest',
22+
},
23+
undefined,
24+
traceLogPath,
25+
),
26+
});
27+
const xcNode = extractAppNodeFromSnapshot(xctestResult as { nodes?: RawSnapshotNode[] });
28+
if (xcNode?.appName || xcNode?.appBundleId) {
29+
return {
30+
appName: xcNode.appName ?? xcNode.appBundleId ?? 'unknown',
31+
appBundleId: xcNode.appBundleId,
32+
source: 'snapshot-xctest',
33+
};
34+
}
35+
1336
if (device.kind === 'device') {
14-
const xctestResult = await dispatchCommand(device, 'snapshot', [], flags?.out, {
15-
...contextFromFlags(
16-
logPath,
17-
{
18-
...flags,
19-
snapshotDepth: 1,
20-
snapshotCompact: true,
21-
snapshotBackend: 'xctest',
22-
},
23-
undefined,
24-
traceLogPath,
25-
),
26-
});
27-
const xcNode = extractAppNodeFromSnapshot(xctestResult as { nodes?: RawSnapshotNode[] });
2837
return {
29-
appName: xcNode?.appName ?? xcNode?.appBundleId ?? 'unknown',
30-
appBundleId: xcNode?.appBundleId,
38+
appName: 'unknown',
39+
appBundleId: undefined,
3140
source: 'snapshot-xctest',
3241
};
3342
}
3443

35-
const axResult = await dispatchCommand(device, 'snapshot', [], flags?.out, {
44+
const axResult = await dispatch(device, 'snapshot', [], flags?.out, {
3645
...contextFromFlags(
3746
logPath,
3847
{
@@ -53,23 +62,10 @@ export async function resolveIosAppStateFromSnapshots(
5362
source: 'snapshot-ax',
5463
};
5564
}
56-
const xctestResult = await dispatchCommand(device, 'snapshot', [], flags?.out, {
57-
...contextFromFlags(
58-
logPath,
59-
{
60-
...flags,
61-
snapshotDepth: 1,
62-
snapshotCompact: true,
63-
snapshotBackend: 'xctest',
64-
},
65-
undefined,
66-
traceLogPath,
67-
),
68-
});
69-
const xcNode = extractAppNodeFromSnapshot(xctestResult as { nodes?: RawSnapshotNode[] });
65+
7066
return {
71-
appName: xcNode?.appName ?? xcNode?.appBundleId ?? 'unknown',
72-
appBundleId: xcNode?.appBundleId,
67+
appName: 'unknown',
68+
appBundleId: undefined,
7369
source: 'snapshot-xctest',
7470
};
7571
}

0 commit comments

Comments
 (0)