Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions packages/platform-apple/src/core/__tests__/app-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,20 @@ test('setIosSetting permission grant all passes all through as one simctl call',
);
});

test('setIosSetting clear-app-state wipes app data and keeps the container manager metadata', async () => {
test('setIosSetting clear-app-state leaves a fresh-install data container layout', async () => {
const containerPath = await mkdtempForTest('agent-device-ios-clear-app-state-container-');
const metadataFile = '.com.apple.mobile_container_manager.metadata.plist';
await fs.mkdir(path.join(containerPath, 'Documents'), { recursive: true });
await fs.writeFile(path.join(containerPath, 'Documents', 'db.sqlite'), 'db');
for (const [file, content] of Object.entries({
'Documents/db.sqlite': 'db',
'Library/Caches/blob': 'cache',
'Library/Preferences/com.example.app.plist': 'prefs',
'Library/Application Support/state.json': 'state',
'SystemData/com.apple.state': 'system',
'tmp/download.tmp': 'partial download',
})) {
await fs.mkdir(path.dirname(path.join(containerPath, file)), { recursive: true });
await fs.writeFile(path.join(containerPath, file), content);
}
await fs.writeFile(path.join(containerPath, 'Library.plist'), 'prefs');
await fs.writeFile(path.join(containerPath, '.app-dotfile'), 'app data');
await fs.writeFile(path.join(containerPath, metadataFile), 'metadata');
Expand All @@ -416,7 +425,15 @@ test('setIosSetting clear-app-state wipes app data and keeps the container manag
);
assert.equal(result?.cleared, true);
assert.equal(result?.bundleId, 'com.example.app');
assert.deepEqual(await fs.readdir(containerPath), [metadataFile]);
assert.deepEqual((await fs.readdir(containerPath, { recursive: true })).sort(), [
metadataFile,
'Documents',
'Library',
'Library/Caches',
'Library/Preferences',
'SystemData',
'tmp',
]);
assert.equal(await fs.readFile(path.join(containerPath, metadataFile), 'utf8'), 'metadata');

const flat = calls.map((args) => args.join(' '));
Expand Down
25 changes: 24 additions & 1 deletion packages/platform-apple/src/core/app-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
summarizeCommandAttemptFailures,
type CommandAttemptFailure,
} from '@agent-device/kernel/errors';
import { readHostDirectory, removeHostPath } from '@agent-device/host-kit/host-file';
import {
ensureHostDirectory,
readHostDirectory,
removeHostPath,
} from '@agent-device/host-kit/host-file';
import path from 'node:path';
import { requireExecSuccess } from '@agent-device/host-kit/command';
import { requireLocationCoordinates } from '@agent-device/kernel/location-coordinates';
Expand Down Expand Up @@ -198,6 +202,20 @@ export async function readIosSetting(
*/
const CONTAINER_MANAGER_METADATA_FILE = '.com.apple.mobile_container_manager.metadata.plist';

/**
* The directories a fresh install creates in the data container. iOS does not recreate them on
* relaunch; without `tmp`, every URLSession download task fails until the app is reinstalled.
* The list matches the iOS 26.5 fresh-install layout; older runtimes and tvOS or visionOS
* simulators may differ, but an extra empty directory there is harmless.
*/
const FRESH_INSTALL_DATA_DIRECTORIES = [
'Documents',
Comment thread
pvedula7 marked this conversation as resolved.
'Library/Caches',
'Library/Preferences',
'SystemData',
'tmp',
];

async function clearIosSimulatorAppState(
device: DeviceInfo,
app: string,
Expand Down Expand Up @@ -234,6 +252,11 @@ async function clearIosSimulatorAppState(
.filter((entry) => entry !== CONTAINER_MANAGER_METADATA_FILE)
.map((entry) => removeHostPath(path.join(containerPath, entry))),
);
await Promise.all(
FRESH_INSTALL_DATA_DIRECTORIES.map((directory) =>
ensureHostDirectory(path.join(containerPath, directory)),
Comment thread
pvedula7 marked this conversation as resolved.
),
);

return { bundleId, containerPath };
}
Expand Down
2 changes: 1 addition & 1 deletion website/docs/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ agent-device settings permission reset screen-recording --platform macos
- Android has no category ladder, only a `font_scale` multiplier, so the two are mapped: `large` is `1.0` up through `accessibility-extra-extra-extra-large` at `3.2`. A device holding a multiplier off that ladder reads back as the nearest rung with the exact multiplier as `platformValue`, and an unset `font_scale` reads as `large`/`1.0`.
- A text size change is a configuration change: an app already running adopts it on its next configuration change, so relaunch the app under test (`open <app> --relaunch`) when asserting rendered sizes.
- `settings location set <lat> <lon>` sets precise coordinates on iOS simulators and Android emulators.
- `settings clear-app-state [app-id]` clears the active session app data, or the provided app id. Android uses `pm clear`, which removes SharedPreferences, databases, files, and cache. iOS simulator removes the app data container contents. iOS physical devices and macOS are unsupported. It does not touch the keychain, so keychain-backed credentials (e.g. Firebase auth) survive it.
- `settings clear-app-state [app-id]` clears the active session app data, or the provided app id. Android uses `pm clear`, which removes SharedPreferences, databases, files, and cache. iOS simulator removes the app data container contents and leaves the fresh-install directory layout (empty `Documents`, `Library/Caches`, `Library/Preferences`, `SystemData`, and `tmp`, as on iOS 26.5; other runtimes may differ). iOS physical devices and macOS are unsupported. It does not touch the keychain, so keychain-backed credentials (e.g. Firebase auth) survive it.
- `settings reset-keychain clear` resets the iOS simulator's keychain (`xcrun simctl keychain <device> reset`), removing keychain-backed credentials such as Firebase auth tokens that `clear-app-state` leaves behind. simctl has no per-app keychain reset, so this clears the keychain for every app installed on that simulator, not only the app under test — treat it as a whole-simulator, opt-in operation and pair it with `clear-app-state` for a full fresh-install reset. iOS physical devices, Android, and macOS are unsupported.
- Face ID and Touch ID controls are iPhone and iPad simulator-only; Apple TV and Vision Pro simulators refuse them. They post the same Darwin notifications the Simulator.app Features menu posts (`xcrun simctl spawn <device> notifyutil`): `com.apple.BiometricKit_Sim.<pearl|fingerTouch>.<match|nomatch>` for match/nonmatch, and `com.apple.BiometricKit.enrollmentChanged` (set to `1`/`0`, then posted, then read back) for enroll/unenroll. No `simctl biometric` subcommand exists, so this route works on any Xcode.
- Android `settings airplane on|off` is applied by the connectivity service (`cmd connectivity airplane-mode`, Android 11+), which drives the radios rather than only writing the `airplane_mode_on` setting. The response reports the `airplaneMode` that service holds after the change, and Android builds without that command fail without changing device state. Connectivity takes a moment to settle after the switch, so poll the app under test rather than asserting offline behavior immediately.
Expand Down
Loading