diff --git a/src/client-types.ts b/src/client-types.ts index 9ba9378eb..97f6959f2 100644 --- a/src/client-types.ts +++ b/src/client-types.ts @@ -61,6 +61,7 @@ export type { } from './contracts/navigation.ts'; export type { ClipboardCommandResult } from './contracts/clipboard.ts'; export type { AppStateCommandResult } from './contracts/app-state.ts'; +export type { KeyboardCommandResult } from './contracts/keyboard.ts'; export type AgentDeviceDaemonTransport = ( req: Omit, @@ -475,21 +476,6 @@ export type AlertCommandResult = DaemonResponseData & { items?: string[]; }; -export type KeyboardCommandResult = DaemonResponseData & { - platform?: 'android' | 'ios'; - action?: 'status' | 'dismiss' | 'enter'; - visible?: boolean; - inputType?: string | null; - inputMethodPackage?: string | null; - type?: string | null; - focusedPackage?: string | null; - focusedResourceId?: string | null; - inputOwner?: 'app' | 'ime' | 'unknown'; - wasVisible?: boolean; - dismissed?: boolean; - attempts?: number; -}; - export type ReactNativeCommandOptions = DeviceCommandBaseOptions & { action: 'dismiss-overlay'; }; @@ -512,7 +498,7 @@ export type AgentDeviceCommandClient = { home: (options?: HomeCommandOptions) => Promise>; rotate: (options: RotateCommandOptions) => Promise>; appSwitcher: (options?: AppSwitcherCommandOptions) => Promise>; - keyboard: (options?: KeyboardCommandOptions) => Promise; + keyboard: (options?: KeyboardCommandOptions) => Promise>; clipboard: (options: ClipboardCommandOptions) => Promise>; reactNative: (options: ReactNativeCommandOptions) => Promise; prepare: (options: PrepareCommandOptions) => Promise; diff --git a/src/client.ts b/src/client.ts index 2ce7a72ff..ae0dd5f38 100644 --- a/src/client.ts +++ b/src/client.ts @@ -110,7 +110,8 @@ export function createAgentDeviceClient( rotate: async (options) => await executeCommand>('rotate', options), appSwitcher: async (options = {}) => await executeCommand>('app-switcher', options), - keyboard: async (options = {}) => await executeCommand('keyboard', options), + keyboard: async (options = {}) => + await executeCommand>('keyboard', options), clipboard: async (options) => await executeCommand>('clipboard', options), reactNative: async (options) => await executeCommand('react-native', options), diff --git a/src/contracts/keyboard.ts b/src/contracts/keyboard.ts new file mode 100644 index 000000000..a2b2c22f2 --- /dev/null +++ b/src/contracts/keyboard.ts @@ -0,0 +1,29 @@ +/** + * Closed result of the `keyboard` command, grounded in the dispatch handlers' + * literal returns (src/core/dispatch.ts `handleAndroidKeyboardCommand` / + * `handleIosKeyboardCommand`). + * + * `platform` and `action` are always present; the remaining fields appear per + * branch (Android `status`/`dismiss` carry the keyboard-state fields; `enter` + * and iOS `dismiss` carry a `message`). It is kept as a flat closed shape rather + * than a five-way `platform`×`action` union because the per-branch field sets + * overlap heavily and the underlying Android keyboard-state types live in the + * platform layer (below the public contract). The `Record` index signature of + * the previous hand-written mirror is dropped, and the spurious `| null`s are + * removed (the handler never returns `null` for these). + */ +export type KeyboardCommandResult = { + platform: 'android' | 'ios'; + action: 'status' | 'dismiss' | 'enter'; + visible?: boolean; + wasVisible?: boolean; + dismissed?: boolean; + attempts?: number; + inputType?: string; + type?: 'text' | 'number' | 'email' | 'phone' | 'password' | 'datetime' | 'unknown'; + inputMethodPackage?: string; + focusedPackage?: string; + focusedResourceId?: string; + inputOwner?: 'app' | 'ime' | 'unknown'; + message?: string; +}; diff --git a/src/core/command-descriptor/__tests__/command-result.test.ts b/src/core/command-descriptor/__tests__/command-result.test.ts index 3e3b28300..d8c4f4cfa 100644 --- a/src/core/command-descriptor/__tests__/command-result.test.ts +++ b/src/core/command-descriptor/__tests__/command-result.test.ts @@ -14,6 +14,7 @@ import type { } from '../../../contracts/navigation.ts'; import type { ClipboardCommandResult } from '../../../contracts/clipboard.ts'; import type { AppStateCommandResult } from '../../../contracts/app-state.ts'; +import type { KeyboardCommandResult } from '../../../contracts/keyboard.ts'; import type { CommandResult, CommandResultMap } from '../command-result.ts'; /** @@ -38,6 +39,7 @@ test('seeded CommandResult entries resolve to their existing contract result typ const appSwitcher: Equal, AppSwitcherCommandResult> = true; const clipboard: Equal, ClipboardCommandResult> = true; const appstate: Equal, AppStateCommandResult> = true; + const keyboard: Equal, KeyboardCommandResult> = true; expect([ press, fill, @@ -51,7 +53,8 @@ test('seeded CommandResult entries resolve to their existing contract result typ appSwitcher, clipboard, appstate, - ]).toEqual([true, true, true, true, true, true, true, true, true, true, true, true]); + keyboard, + ]).toEqual([true, true, true, true, true, true, true, true, true, true, true, true, true]); }); test('unmigrated commands fall back to the untyped Record bag, keeping the union total', () => { @@ -76,6 +79,7 @@ test('CommandResultMap is seeded only from already-existing contract result type | 'app-switcher' | 'clipboard' | 'appstate' + | 'keyboard' > = true; expect(keys).toBe(true); }); diff --git a/src/core/command-descriptor/command-result.ts b/src/core/command-descriptor/command-result.ts index 29321decc..7396941df 100644 --- a/src/core/command-descriptor/command-result.ts +++ b/src/core/command-descriptor/command-result.ts @@ -13,6 +13,7 @@ import type { } from '../../contracts/navigation.ts'; import type { ClipboardCommandResult } from '../../contracts/clipboard.ts'; import type { AppStateCommandResult } from '../../contracts/app-state.ts'; +import type { KeyboardCommandResult } from '../../contracts/keyboard.ts'; /** * The additive typed-result spine (ADR-0008, Phase 1 step 6). @@ -28,7 +29,8 @@ import type { AppStateCommandResult } from '../../contracts/app-state.ts'; * commands `home` / `back` / `rotate` / `app-switcher` alongside the seed * interaction trio. Batch 3 adds `clipboard` (a closed `read`/`write` union) and * `appstate` (a closed `platform` union — Apple session state with the iOS-only - * device locators, or Android package/activity). Each entry is grounded in a + * device locators, or Android package/activity). Batch 4 adds `keyboard` (a + * closed flat shape). Each entry is grounded in a * re-read of the handler's literal return; see the per-type docstrings. */ export interface CommandResultMap { @@ -44,6 +46,7 @@ export interface CommandResultMap { 'app-switcher': AppSwitcherCommandResult; clipboard: ClipboardCommandResult; appstate: AppStateCommandResult; + keyboard: KeyboardCommandResult; } /**