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
13 changes: 4 additions & 9 deletions packages/provider-limrun/src/app-log-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,9 @@ test('fails closed for a stale Android identity before exposing facts or binding
});

/**
* back/orientation are admitted on both direct-session platforms; home/tv-remote differ by
* back/orientation/home are admitted on both direct-session platforms; tv-remote differs by
* platform (the Android leg reuses the local family's interactor factory, the iOS leg refuses
* both explicitly); keyboard status/dismiss/enter reuse that same Android-only interactor.
* it explicitly); keyboard status/dismiss/enter reuse that same Android-only interactor.
*/
function expectLimrunNavigationAndKeyboardFacts(
binding: DeviceBinding<PlatformRuntimeOperations>,
Expand All @@ -392,13 +392,8 @@ function expectLimrunNavigationAndKeyboardFacts(
expect(binding.operations.back).toBeTypeOf('function');
expect(binding.facts.operations.setOrientation).toEqual({ available: true });
expect(binding.operations.setOrientation).toBeTypeOf('function');
expect(binding.facts.operations.home.available).toBe(isAndroid);
expect(binding.operations.home).toBeTypeOf(isAndroid ? 'function' : 'undefined');
if (!isAndroid) {
expect(binding.facts.operations.home).toMatchObject({
hint: 'Limrun iOS direct sessions do not expose home yet.',
});
}
expect(binding.facts.operations.home).toEqual({ available: true });
expect(binding.operations.home).toBeTypeOf('function');
// tv-remote additionally requires a real TV target on the Android leg.
expect(binding.facts.operations.tvRemote.available).toBe(false);
expect(binding.operations.tvRemote).toBeUndefined();
Expand Down
13 changes: 4 additions & 9 deletions packages/provider-limrun/src/interaction-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ test('clipboard follows the same Android-reuse / iOS-refusal split its siblings
expect(stale.writeClipboard).toEqual(liveSessionUnavailable);
});

// R56: the springboard buttons follow the same Android-reuse / iOS-refusal split; a hardware
// button is refused on both legs, and a dead session closes the whole family.
test('home and app-switcher ride the Android interactor and are refused on the iOS leg', () => {
test('home is admitted on both legs, app-switcher only on Android, the action button on neither', () => {
const android = limrunSystemButtonOperationFacts(androidMobileDevice);
expect(android.home).toEqual({ available: true });
expect(android.appSwitcher).toEqual({ available: true });
Expand All @@ -89,11 +87,7 @@ test('home and app-switcher ride the Android interactor and are refused on the i
});

const ios = limrunSystemButtonOperationFacts(iosDevice);
expect(ios.home).toEqual({
available: false,
reason: 'unsupported-provider-mode',
hint: 'Limrun iOS direct sessions do not expose home yet.',
});
expect(ios.home).toEqual({ available: true });
expect(ios.appSwitcher).toEqual({
available: false,
reason: 'unsupported-provider-mode',
Expand Down Expand Up @@ -231,7 +225,8 @@ test('binding omits every operation an unavailable fact refused', () => {

expect(operations.back).toBeTypeOf('function');
expect(operations.setOrientation).toBeTypeOf('function');
expect(operations.home).toBeUndefined();
expect(operations.home).toBeTypeOf('function');
expect(operations.appSwitcher).toBeUndefined();
expect(operations.tvRemote).toBeUndefined();
});

Expand Down
15 changes: 5 additions & 10 deletions packages/provider-limrun/src/interaction-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,6 @@ function limrunGestureFacts(
viewport: available,
});
}
const homeUnavailableIos = Object.freeze({
available: false,
reason: 'unsupported-provider-mode',
hint: 'Limrun iOS direct sessions do not expose home yet.',
} as const);
const tvRemoteUnavailableIos = Object.freeze({
available: false,
reason: 'unsupported-provider-mode',
Expand Down Expand Up @@ -141,7 +136,7 @@ const appSwitcherUnavailableIos = Object.freeze({
reason: 'unsupported-provider-mode',
hint: 'Limrun iOS direct sessions do not expose app switcher yet.',
} as const);
/** No Limrun session exposes a hardware button; only the springboard buttons split by leg. */
/** No Limrun session exposes the action button; only `appSwitcher` still splits by leg. */
const systemButtonUnavailable = Object.freeze({
available: false,
reason: 'unsupported-provider-mode',
Expand Down Expand Up @@ -316,9 +311,9 @@ export function limrunAlertOperationFacts(
}

/**
* The system buttons split the way the navigation leaves do: the Android leg rides the local
* family's own interactor factory for `home` and `appSwitcher`, the iOS direct session refuses
* both, and no Limrun session exposes a hardware button on either leg.
* The Android leg rides the local family's own interactor factory for `home` and `appSwitcher`.
* The iOS direct session presses `home` through Limrun's button actions and refuses
* `appSwitcher`. No Limrun session exposes the action button.
*/
export function limrunSystemButtonOperationFacts(
device: DeviceInfo,
Expand All @@ -336,7 +331,7 @@ export function limrunSystemButtonOperationFacts(
}
return systemButtonRuntimeOperationFacts({
unsupported: systemButtonUnavailable,
home: homeUnavailableIos,
home: available,
Comment thread
jbroma marked this conversation as resolved.
appSwitcher: appSwitcherUnavailableIos,
});
}
Expand Down
35 changes: 35 additions & 0 deletions packages/provider-limrun/src/ios-interactor-home.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test, vi } from 'vitest';
import { createLimrunIosInteractor, type LimrunIosSession } from './ios.ts';

function sessionWithClient() {
const client = {
pressKey: vi.fn(async () => {}),
performActions: vi.fn(async () => ({ results: [] })),
};
const session = {
platform: 'ios',
instanceId: 'limrun-home-instance',
client,
} as unknown as LimrunIosSession;
return { interactor: createLimrunIosInteractor(session), client };
}

test('home presses and releases the hardware home button in one batch', async () => {
const { interactor, client } = sessionWithClient();

await interactor.home!();

expect(client.performActions).toHaveBeenCalledTimes(1);
expect(client.performActions).toHaveBeenCalledWith([
{ type: 'buttonDown', button: 'home' },
{ type: 'buttonUp', button: 'home' },
]);
expect(client.pressKey).not.toHaveBeenCalled();
});

test('a batch the device rejects surfaces as the home failure', async () => {
const { interactor, client } = sessionWithClient();
client.performActions.mockRejectedValueOnce(new Error('buttonDown: unsupported button'));

await expect(interactor.home!()).rejects.toThrow('buttonDown: unsupported button');
});
8 changes: 8 additions & 0 deletions packages/provider-limrun/src/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ class LimrunIosInteractor implements Interactor {
await this.session.client.pressKey('escape');
}

/** One press and release of the hardware home button, sent as a single action batch. */
async home(): Promise<void> {
await this.session.client.performActions([
{ type: 'buttonDown', button: 'home' },
{ type: 'buttonUp', button: 'home' },
]);
}

async setOrientation(orientation: DeviceRotation): Promise<void> {
if (orientation === 'portrait-upside-down') {
throw unsupported(
Expand Down
2 changes: 1 addition & 1 deletion website/docs/docs/limrun.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ agent-device disconnect

Limrun Android uses the direct ADB tunnel. Normal Android helper-backed snapshots, installs, and port reverse flow are available, including the usual Android reverse setup for a local Metro server.

Limrun iOS uses the direct Limrun iOS client. It supports app lifecycle commands, snapshots, screenshots, taps, text input, scrolling, and app installation. `open --launch-args` relaunches the app with those arguments through simctl. It cannot reverse a remote device port to a local host port. For iOS Metro or React DevTools, use a publicly reachable HTTPS endpoint or bridge URL instead of a local-only address.
Limrun iOS uses the direct Limrun iOS client. It supports app lifecycle commands, snapshots, screenshots, taps, text input, scrolling, the home button, and app installation. `open --launch-args` relaunches the app with those arguments through simctl. It cannot reverse a remote device port to a local host port. For iOS Metro or React DevTools, use a publicly reachable HTTPS endpoint or bridge URL instead of a local-only address.

Limrun iOS `settings` supports `appearance`, `permission`, `location`, and `clear-app-state`. They use the same arguments as a local simulator, and `clear-app-state` resets the app's data container through Limrun's soft reset. That reset relaunches the app once before agent-device stops it, so the app may run once after its data is cleared. `reset-keychain`, `wifi`, `airplane`, `faceid`, `touchid`, and `text-size` are unsupported, and no iOS setting can be read back.

Expand Down
Loading