diff --git a/packages/contracts/src/platform-runtime.test.ts b/packages/contracts/src/platform-runtime.test.ts index 81c726ee6e..94e49a9e02 100644 --- a/packages/contracts/src/platform-runtime.test.ts +++ b/packages/contracts/src/platform-runtime.test.ts @@ -13,6 +13,7 @@ import { sameRuntimeOwner, type BoundDeviceRuntime, type DeviceBinding, + unavailableFact, } from './platform-runtime.ts'; import { runtimeUse } from './platform-runtime-use.ts'; @@ -306,3 +307,17 @@ function testBinding( [Symbol.asyncDispose]: async () => undefined, }; } + +test('unavailableFact freezes the cell and omits hint when none is given', () => { + const bare = unavailableFact('unsupported-device-kind'); + const hinted = unavailableFact('unsupported-provider-mode', 'provider-owned'); + assert.deepEqual(bare, { available: false, reason: 'unsupported-device-kind' }); + assert.equal('hint' in bare, false); + assert.deepEqual(hinted, { + available: false, + reason: 'unsupported-provider-mode', + hint: 'provider-owned', + }); + assert.equal(Object.isFrozen(bare), true); + assert.equal(Object.isFrozen(hinted), true); +}); diff --git a/packages/contracts/src/platform-runtime.ts b/packages/contracts/src/platform-runtime.ts index dff20619e5..e2dcfc773f 100644 --- a/packages/contracts/src/platform-runtime.ts +++ b/packages/contracts/src/platform-runtime.ts @@ -134,6 +134,16 @@ export type RuntimeOperationUnavailability = Readonly<{ export type RuntimeOperationFact = Readonly<{ available: true }> | RuntimeOperationUnavailability; +/** One frozen unavailable cell; owners list their refusals as calls instead of literals. */ +export function unavailableFact( + reason: RuntimeOperationUnavailability['reason'], + hint?: string, +): RuntimeOperationUnavailability { + return Object.freeze( + hint === undefined ? { available: false, reason } : { available: false, reason, hint }, + ); +} + /** * An operation is present on a binding only when the owner's own facts admitted it. One helper so * every owner's `bind` reads as a list of admitted operations rather than a chain of branches — @@ -382,11 +392,7 @@ function normalizeRuntimeFact(value: RuntimeFactInput): RuntimeOperationFact | u if (value.available !== false) return undefined; if (!isRuntimeOperationUnavailabilityReason(value.reason)) return undefined; if (value.hint !== undefined && typeof value.hint !== 'string') return undefined; - return Object.freeze({ - available: false, - reason: value.reason, - ...(value.hint === undefined ? {} : { hint: value.hint }), - }); + return unavailableFact(value.reason, value.hint); } function requireRuntimeOperation( diff --git a/packages/platform-android/src/runtime.ts b/packages/platform-android/src/runtime.ts index 2a0e4c9f3b..c5ae12aa49 100644 --- a/packages/platform-android/src/runtime.ts +++ b/packages/platform-android/src/runtime.ts @@ -22,7 +22,11 @@ import { } from '@agent-device/contracts/gesture-admission'; import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-runtime'; import { scrollRuntimeOperationFacts } from '@agent-device/contracts/scroll-runtime'; -import { localRuntimeOwner, whenAdmitted } from '@agent-device/contracts/platform-runtime'; +import { + localRuntimeOwner, + unavailableFact, + whenAdmitted, +} from '@agent-device/contracts/platform-runtime'; import { audioProbeRuntimeOperationFacts } from '@agent-device/contracts/audio-probe-runtime'; import { perfRuntimeOperationFacts } from '@agent-device/contracts/perf-runtime'; import { screenshotRuntimeOperationFacts } from '@agent-device/contracts/screenshot-runtime'; @@ -63,114 +67,91 @@ import { const owner = localRuntimeOwner('android'); const available = Object.freeze({ available: true } as const); -const elementTextKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', -} as const); +const elementTextKindUnavailable = unavailableFact('unsupported-device-kind'); /** * Parity with the retired `focus` capability bucket (`{ emulator, device, unknown }`): every * Android kind drives touch through adb except the synthetic `simulator` row, which has no device. */ -const focusKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'focus is supported on Android emulators and physical devices.', -} as const); +const focusKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'focus is supported on Android emulators and physical devices.', +); /** adb drives keyboard actions on the same two kinds it drives everything else. */ -const keyboardKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'keyboard actions are supported on Android emulators and physical devices.', -} as const); -const hoverUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); +const keyboardKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'keyboard actions are supported on Android emulators and physical devices.', +); +const hoverUnavailable = unavailableFact('unsupported-platform-leaf'); /** * `home` and `app-switcher` are the system buttons `input keyevent` can press. Any other (the * iPhone Action Button today) is hardware with no Android key event behind it, so there is no adb * path to admit even on the kinds every other Android cell admits. */ -const systemButtonUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Android has no key event for this system button.', -} as const); +const systemButtonUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Android has no key event for this system button.', +); /** * Foldable Android emulators do carry a posture control (the emulator console's `fold` and * `posture` commands), but nothing in this project drives it yet, so the cell refuses on every * kind rather than advertising a pose it cannot set. */ -const foldUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'fold drives the hinge of a foldable iPhone simulator; the Android emulator posture control is not driven by agent-device yet.', -} as const); -const headlessUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Headless boot is supported only for Android emulators.', -} as const); -const appStateUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Android appstate is supported only for Android emulators and devices.', -} as const); -const prepareUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Apple runner preparation is supported only for Apple targets.', -} as const); -const openTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'open is supported only for Android emulators and devices.', -} as const); -const closeTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'close is supported only for Android emulators and devices.', -} as const); -const runtimeHintsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Runtime hints are supported only for Android emulators and devices.', -} as const); -const portReverseUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Port reverse is supported only by an owning provider runtime.', -} as const); -const shutdownKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'shutdown is supported only for Apple simulators and Android emulators.', -} as const); -const screenshotKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'screenshot is supported only for Android emulators and devices.', -} as const); -const snapshotKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'snapshot is supported only for Android emulators and devices.', -} as const); -const snapshotCustomActionsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Re-run without --actions, or target an iOS simulator.', -} as const); -const viewportUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'viewport resizes web targets only (--platform web).', -} as const); -const audioQueryUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'the stateless audio page probe is a web-session operation; Android targets use the host capture.', -} as const); +const foldUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'fold drives the hinge of a foldable iPhone simulator; the Android emulator posture control is not driven by agent-device yet.', +); +const headlessUnavailable = unavailableFact( + 'unsupported-device-kind', + 'Headless boot is supported only for Android emulators.', +); +const appStateUnavailable = unavailableFact( + 'unsupported-device-kind', + 'Android appstate is supported only for Android emulators and devices.', +); +const prepareUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Apple runner preparation is supported only for Apple targets.', +); +const openTargetUnavailable = unavailableFact( + 'unsupported-device-kind', + 'open is supported only for Android emulators and devices.', +); +const closeTargetUnavailable = unavailableFact( + 'unsupported-device-kind', + 'close is supported only for Android emulators and devices.', +); +const runtimeHintsUnavailable = unavailableFact( + 'unsupported-device-kind', + 'Runtime hints are supported only for Android emulators and devices.', +); +const portReverseUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Port reverse is supported only by an owning provider runtime.', +); +const shutdownKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'shutdown is supported only for Apple simulators and Android emulators.', +); +const screenshotKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'screenshot is supported only for Android emulators and devices.', +); +const snapshotKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'snapshot is supported only for Android emulators and devices.', +); +const snapshotCustomActionsUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Re-run without --actions, or target an iOS simulator.', +); +const viewportUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'viewport resizes web targets only (--platform web).', +); +const audioQueryUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'the stateless audio page probe is a web-session operation; Android targets use the host capture.', +); function androidLifecycleFacts(device: DeviceInfo) { const openTarget = androidOpenTargetFact(device); @@ -205,21 +186,18 @@ function androidRuntimeHintsFact(device: DeviceInfo) { : runtimeHintsUnavailable; } -const gestureKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Gestures are supported on Android emulators and physical devices.', -} as const); -const androidTvMultiTouchUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: ANDROID_TV_MULTI_TOUCH_UNSUPPORTED_HINT, -} as const); -const androidTvDragUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, -} as const); +const gestureKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'Gestures are supported on Android emulators and physical devices.', +); +const androidTvMultiTouchUnavailable = unavailableFact( + 'unsupported-platform-leaf', + ANDROID_TV_MULTI_TOUCH_UNSUPPORTED_HINT, +); +const androidTvDragUnavailable = unavailableFact( + 'unsupported-platform-leaf', + TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, +); /** * A TV target has no touch input at all, which is the one Android gate the retired admission @@ -240,11 +218,10 @@ function androidTouchFact(device: DeviceInfo) { return device.kind === 'simulator' ? focusKindUnavailable : available; } -const clipboardShellUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: ANDROID_CLIPBOARD_SHELL_COMMAND_UNAVAILABLE_HINT, -} as const); +const clipboardShellUnavailable = unavailableFact( + 'owner-capability-missing', + ANDROID_CLIPBOARD_SHELL_COMMAND_UNAVAILABLE_HINT, +); /** * The probe could not reach the device, so this owner does not know whether the build supports a @@ -253,11 +230,10 @@ const clipboardShellUnavailable = Object.freeze({ * exact failure fact-based admission exists to prevent. Deliberately not cached — the next * inspection asks again. */ -const clipboardShellUnknown = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'Could not determine whether this Android build supports a shell clipboard: the adb probe did not complete. Retry once the device is reachable.', -} as const); +const clipboardShellUnknown = unavailableFact( + 'owner-capability-missing', + 'Could not determine whether this Android build supports a shell clipboard: the adb probe did not complete. Retry once the device is reachable.', +); /** * `probe-failed` covers both ways this owner can end up without an answer: the probe ran and could @@ -273,11 +249,10 @@ async function probeClipboardShellSupport( return await probe.call(host.androidTools, device); } -const tvRemoteUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'tv-remote is supported only on Android TV targets.', -} as const); +const tvRemoteUnavailable = unavailableFact( + 'unsupported-device-kind', + 'tv-remote is supported only on Android TV targets.', +); /** * Parity with the retired `androidPlugin` closure: the TV-target gate, whose hint fired * regardless of device kind (the closure never distinguished the synthetic `simulator` row from diff --git a/packages/platform-apple/src/recording/runtime.ts b/packages/platform-apple/src/recording/runtime.ts index c0fc429e2b..56a347c823 100644 --- a/packages/platform-apple/src/recording/runtime.ts +++ b/packages/platform-apple/src/recording/runtime.ts @@ -3,7 +3,7 @@ import { AppError, asAppError } from '@agent-device/kernel/errors'; import { execFailureDetails } from '@agent-device/host-kit/command'; import type { CleanupOutcome, FinishOutcome } from '@agent-device/contracts/durable-resource'; import type { HostCommandResult } from '@agent-device/contracts/platform-runtime-host'; -import type { RuntimeOwnerRef } from '@agent-device/contracts/platform-runtime'; +import { type RuntimeOwnerRef, unavailableFact } from '@agent-device/contracts/platform-runtime'; import type { ScreenRecordingRuntimeHost } from '@agent-device/contracts/screen-recording-runtime-host'; import { RECORDING_OUTPUT_UNPLAYABLE_REASON, @@ -40,13 +40,13 @@ const SIMULATOR_BACKEND_LABEL = 'simctl recordVideo'; export function appleScreenRecordingFacts(device: DeviceInfo) { if (device.appleOs === 'watchos') - return unavailable('unsupported-platform-leaf', 'watchOS recording is not supported.'); + return unavailableFact('unsupported-platform-leaf', 'watchOS recording is not supported.'); if ( isIosFamily(device) && device.kind === 'device' && device.iosPhysicalDeviceBackend === 'xctest' ) { - return unavailable( + return unavailableFact( 'unsupported-device-backend', 'This command requires a CoreDevice-backed physical iOS device. The selected XCTest backend supports open, close, interactions, snapshots, and screenshots.', ); @@ -504,10 +504,3 @@ function snapshot( ...(timing.runnerSessionId === undefined ? {} : { runnerSessionId: timing.runnerSessionId }), }); } - -function unavailable( - reason: 'unsupported-platform-leaf' | 'unsupported-device-backend', - hint: string, -) { - return Object.freeze({ available: false, reason, hint } as const); -} diff --git a/packages/platform-apple/src/runtime.ts b/packages/platform-apple/src/runtime.ts index 31fb84423d..262253832b 100644 --- a/packages/platform-apple/src/runtime.ts +++ b/packages/platform-apple/src/runtime.ts @@ -2,6 +2,7 @@ import { type DeviceBinding, type RuntimeOperationFact, localRuntimeOwner, + unavailableFact, whenAdmitted, } from '@agent-device/contracts/platform-runtime'; import { bindSimulatorReadiness } from './runtime-simulator-readiness.ts'; @@ -78,40 +79,32 @@ import { createAppleSnapshotRoute } from './snapshot-route.ts'; const owner = localRuntimeOwner('apple'); const available = Object.freeze({ available: true } as const); -const unavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const viewportUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'viewport resizes web targets only (--platform web). Apple screen geometry is fixed by the selected simulator or device type — open a different simulator to test another screen size.', -} as const); -const audioQueryUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'the stateless audio page probe is a web-session operation; Apple targets use the host capture.', -} as const); -const perfUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-backend', - hint: 'perf requires an Apple simulator, macOS device, or CoreDevice-backed physical Apple device.', -} as const); +const unavailable = unavailableFact('unsupported-platform-leaf'); +const viewportUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'viewport resizes web targets only (--platform web). Apple screen geometry is fixed by the selected simulator or device type — open a different simulator to test another screen size.', +); +const audioQueryUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'the stateless audio page probe is a web-session operation; Apple targets use the host capture.', +); +const perfUnavailable = unavailableFact( + 'unsupported-device-backend', + 'perf requires an Apple simulator, macOS device, or CoreDevice-backed physical Apple device.', +); /** * Focus drives touch through the Apple interactor, which exists for the simulator and physical * device kinds only. Parity with the retired `focus` capability bucket * (`{ simulator: true, device: true }`), stated as one fact instead of an admission table. */ -const focusKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'focus is supported on Apple simulators and physical devices.', -} as const); -const appStateUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: "Apple appstate is unavailable here: the runner reads the session app's XCUIApplication state on iOS-family simulators and physical devices only, and the Apple target answers no sessionless foreground probe. The per-command answer is the targetActivation disclosure, which a capture carries when its command had to re-activate the session app (#2682).", -} as const); +const focusKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'focus is supported on Apple simulators and physical devices.', +); +const appStateUnavailable = unavailableFact( + 'unsupported-platform-leaf', + "Apple appstate is unavailable here: the runner reads the session app's XCUIApplication state on iOS-family simulators and physical devices only, and the Apple target answers no sessionless foreground probe. The per-command answer is the targetActivation disclosure, which a capture carries when its command had to re-activate the session app (#2682).", +); /** * A live runner reads the session app's `XCUIApplication.state` on the kinds it drives; the read @@ -123,40 +116,31 @@ function appleAppStateFact(device: DeviceInfo): RuntimeOperationFact { if (!isIosFamily(device) || device.appleOs === 'watchos') return appStateUnavailable; return device.kind === 'simulator' || device.kind === 'device' ? available : appStateUnavailable; } -const headlessUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Headless boot is supported only for local Android emulators.', -} as const); -const elementTextLeafUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'watchOS has no XCUITest-driveable UI, so element text comes from the captured tree only.', -} as const); -const elementTextKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', -} as const); -const watchOpenTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'watchOS open is not supported because XCUITest cannot drive watchOS UI.', -} as const); -const watchPrepareUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'watchOS runner preparation is not supported because XCUITest cannot drive watchOS UI.', -} as const); -const watchCloseTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'watchOS close is not supported because XCUITest cannot drive watchOS UI.', -} as const); -const runtimeHintsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Runtime hints are supported only for local iOS-family simulators and Android devices.', -} as const); +const headlessUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Headless boot is supported only for local Android emulators.', +); +const elementTextLeafUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'watchOS has no XCUITest-driveable UI, so element text comes from the captured tree only.', +); +const elementTextKindUnavailable = unavailableFact('unsupported-device-kind'); +const watchOpenTargetUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'watchOS open is not supported because XCUITest cannot drive watchOS UI.', +); +const watchPrepareUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'watchOS runner preparation is not supported because XCUITest cannot drive watchOS UI.', +); +const watchCloseTargetUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'watchOS close is not supported because XCUITest cannot drive watchOS UI.', +); +const runtimeHintsUnavailable = unavailableFact( + 'unsupported-device-kind', + 'Runtime hints are supported only for local iOS-family simulators and Android devices.', +); const appleOpenTargetKindUnavailable = unsupportedAppleDeviceKind( 'open is supported only for Apple simulators and devices.', @@ -167,39 +151,34 @@ const applePrepareKindUnavailable = unsupportedAppleDeviceKind( const appleCloseTargetKindUnavailable = unsupportedAppleDeviceKind( 'close is supported only for Apple simulators and devices.', ); -const portReverseUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Port reverse is supported only by an owning provider runtime.', -} as const); -const shutdownKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'shutdown is supported only for Apple simulators and Android emulators.', -} as const); +const portReverseUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Port reverse is supported only by an owning provider runtime.', +); +const shutdownKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'shutdown is supported only for Apple simulators and Android emulators.', +); const snapshotKindUnavailable = unsupportedAppleDeviceKind( 'snapshot is supported only for Apple simulators and devices.', ); -const snapshotCustomActionsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Re-run without --actions, or target an iOS simulator.', -} as const); -const screenshotWatchOsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'screenshot is not supported on watchOS because XCUITest cannot drive watchOS UI.', -} as const); +const snapshotCustomActionsUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Re-run without --actions, or target an iOS simulator.', +); +const screenshotWatchOsUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'screenshot is not supported on watchOS because XCUITest cannot drive watchOS UI.', +); const screenshotKindUnavailable = unsupportedAppleDeviceKind( 'screenshot is supported only for Apple simulators and devices.', ); -const snapshotActiveAppRequired = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'Open the app under test before capturing its snapshot.', -} as const); +const snapshotActiveAppRequired = unavailableFact( + 'owner-capability-missing', + 'Open the app under test before capturing its snapshot.', +); function unsupportedAppleDeviceKind(hint: string) { - return Object.freeze({ available: false, reason: 'unsupported-device-kind', hint } as const); + return unavailableFact('unsupported-device-kind', hint); } function shutdownFact(device: DeviceInfo) { @@ -256,18 +235,13 @@ function appleRuntimeHintsFact(device: DeviceInfo) { function appInventoryFacts(device: DeviceInfo) { if (device.appleOs === 'watchos') { - return Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf' as const, - hint: 'watchOS app inventory is not supported.', - }); + return unavailableFact('unsupported-platform-leaf', 'watchOS app inventory is not supported.'); } if (device.kind === 'device' && device.iosPhysicalDeviceBackend === 'xctest') { - return Object.freeze({ - available: false, - reason: 'unsupported-device-backend' as const, - hint: 'App inventory is available only on CoreDevice-backed physical iOS devices.', - }); + return unavailableFact( + 'unsupported-device-backend', + 'App inventory is available only on CoreDevice-backed physical iOS devices.', + ); } return available; } @@ -293,11 +267,7 @@ export function createApplePlatformRuntime(host: PlatformRuntimeHost): PlatformR : undefined; const recordingFacts = leafRecordingFacts.available && hostAvailability?.available === false - ? Object.freeze({ - available: false, - reason: 'unsupported-provider-mode' as const, - hint: hostAvailability.hint, - }) + ? unavailableFact('unsupported-provider-mode', hostAvailability.hint) : leafRecordingFacts; const readiness = device.appleOs === 'watchos' ? unavailable : available; const boot = isMacOs(device) || device.appleOs === 'watchos' ? unavailable : available; diff --git a/packages/platform-harmonyos/src/runtime.ts b/packages/platform-harmonyos/src/runtime.ts index 68036c3e61..e3ece28fa2 100644 --- a/packages/platform-harmonyos/src/runtime.ts +++ b/packages/platform-harmonyos/src/runtime.ts @@ -33,7 +33,11 @@ import { keyboardRuntimeOperationFacts } from '@agent-device/contracts/keyboard- import { orientationRuntimeOperationFacts } from '@agent-device/contracts/orientation-runtime'; import { audioProbeRuntimeOperationFacts } from '@agent-device/contracts/audio-probe-runtime'; import { perfRuntimeOperationFacts } from '@agent-device/contracts/perf-runtime'; -import { localRuntimeOwner, whenAdmitted } from '@agent-device/contracts/platform-runtime'; +import { + localRuntimeOwner, + unavailableFact, + whenAdmitted, +} from '@agent-device/contracts/platform-runtime'; import { bindLocalScreenshotInteractor, screenshotRuntimeOperationFacts, @@ -68,69 +72,55 @@ import { } from './deployment/runtime.ts'; const owner = localRuntimeOwner('harmonyos'); -const elementTextUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'HarmonyOS reads element text from the captured tree only.', -} as const); +const elementTextUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'HarmonyOS reads element text from the captured tree only.', +); /** Focus is available on the two HarmonyOS target kinds HDC can drive. */ -const focusKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'focus is supported on HarmonyOS emulators and physical devices.', -} as const); +const focusKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'focus is supported on HarmonyOS emulators and physical devices.', +); const available = Object.freeze({ available: true } as const); -const unavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const appStateUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'HarmonyOS appstate is supported only for HarmonyOS emulators and devices.', -} as const); -const prepareUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Apple runner preparation is supported only for Apple targets.', -} as const); +const unavailable = unavailableFact('unsupported-platform-leaf'); +const appStateUnavailable = unavailableFact( + 'unsupported-device-kind', + 'HarmonyOS appstate is supported only for HarmonyOS emulators and devices.', +); +const prepareUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Apple runner preparation is supported only for Apple targets.', +); const lifecycleAvailable = Object.freeze({ available: true } as const); -const openTargetKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'open is supported only for HarmonyOS emulators and devices.', -} as const); -const closeTargetKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'close is supported only for HarmonyOS emulators and devices.', -} as const); -const portReverseUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Port reverse is supported only by an owning provider runtime.', -} as const); -const snapshotKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'snapshot is supported only for HarmonyOS emulators and devices.', -} as const); -const screenshotKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'screenshot is supported only for HarmonyOS emulators and devices.', -} as const); -const snapshotCustomActionsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Re-run without --actions, or target an iOS simulator.', -} as const); -const viewportUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'viewport resizes web targets only (--platform web).', -} as const); +const openTargetKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'open is supported only for HarmonyOS emulators and devices.', +); +const closeTargetKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'close is supported only for HarmonyOS emulators and devices.', +); +const portReverseUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Port reverse is supported only by an owning provider runtime.', +); +const snapshotKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'snapshot is supported only for HarmonyOS emulators and devices.', +); +const screenshotKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'screenshot is supported only for HarmonyOS emulators and devices.', +); +const snapshotCustomActionsUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Re-run without --actions, or target an iOS simulator.', +); +const viewportUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'viewport resizes web targets only (--platform web).', +); function harmonyLifecycleFacts(device: DeviceInfo) { const openTarget = harmonyOpenTargetFact(device); @@ -160,24 +150,19 @@ function harmonyCloseTargetFact(device: DeviceInfo) { : closeTargetKindUnavailable; } -const gestureKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'Gestures are supported on HarmonyOS emulators and physical devices.', -} as const); +const gestureKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'Gestures are supported on HarmonyOS emulators and physical devices.', +); /** * hdc synthesizes one contact. The retired admission refused two-contact synthesis on every * platform that is neither Android nor Apple, with no hint — that is this cell. */ -const multiTouchUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const targetAuthoredDragUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, -} as const); +const multiTouchUnavailable = unavailableFact('unsupported-platform-leaf'); +const targetAuthoredDragUnavailable = unavailableFact( + 'unsupported-platform-leaf', + TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, +); function harmonyGestureFact(device: DeviceInfo): RuntimeOperationFact { return device.kind === 'emulator' || device.kind === 'device' @@ -190,24 +175,19 @@ function harmonyFocusFact(device: DeviceInfo): RuntimeOperationFact { } /** Public orientation and TV-remote operations are not supported by the HarmonyOS owner. */ -const harmonyPlatformLeafUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); +const harmonyPlatformLeafUnavailable = unavailableFact('unsupported-platform-leaf'); /** Android's live IME status read has no HarmonyOS counterpart (parity with the retired leaf, * which rejected `status`/`get` on every non-Android family). */ -const harmonyKeyboardStatusUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'keyboard status/get is not available through the public HarmonyOS HDC API; use keyboard dismiss or enter', -} as const); +const harmonyKeyboardStatusUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'keyboard status/get is not available through the public HarmonyOS HDC API; use keyboard dismiss or enter', +); -const audioProbeUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'audio is supported for web browser sessions, macOS sessions, iOS simulators, and Android emulators on macOS hosts', -} as const); +const audioProbeUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'audio is supported for web browser sessions, macOS sessions, iOS simulators, and Android emulators on macOS hosts', +); export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): PlatformRuntimeOwner { const appLogs = createHarmonyAppLogRuntime(host); diff --git a/packages/platform-linux/src/runtime.ts b/packages/platform-linux/src/runtime.ts index 2d17a9e940..78f6648cdc 100644 --- a/packages/platform-linux/src/runtime.ts +++ b/packages/platform-linux/src/runtime.ts @@ -22,7 +22,11 @@ import { scrollRuntimeOperationFacts } from '@agent-device/contracts/scroll-runt import { systemButtonRuntimeOperationFacts } from '@agent-device/contracts/system-button-runtime'; import { clipboardRuntimeOperationFacts } from '@agent-device/contracts/clipboard-runtime'; import { bindLocalInteractorOperationSet } from '@agent-device/contracts/local-interactor-operation-set'; -import { localRuntimeOwner, sameRuntimeOwner } from '@agent-device/contracts/platform-runtime'; +import { + localRuntimeOwner, + sameRuntimeOwner, + unavailableFact, +} from '@agent-device/contracts/platform-runtime'; import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable'; import { screenshotRuntimeOperationFacts } from '@agent-device/contracts/screenshot-runtime'; import { @@ -38,21 +42,21 @@ import { bindLinuxApplicationLifecycle } from './lifecycle.ts'; const supported = Object.freeze({ available: true } as const); const linuxOwner = localRuntimeOwner('linux'); -const unsupportedPlatformLeaf = unavailableLinuxRuntimeFact('unsupported-platform-leaf'); -const elementTextKindUnavailable = unavailableLinuxRuntimeFact('unsupported-device-kind'); -const focusKindUnavailable = unavailableLinuxRuntimeFact( +const unsupportedPlatformLeaf = unavailableFact('unsupported-platform-leaf'); +const elementTextKindUnavailable = unavailableFact('unsupported-device-kind'); +const focusKindUnavailable = unavailableFact( 'unsupported-device-kind', 'focus is supported only for the Linux desktop device.', ); -const typeKindUnavailable = unavailableLinuxRuntimeFact( +const typeKindUnavailable = unavailableFact( 'unsupported-device-kind', 'type is supported only for the Linux desktop device.', ); -const gestureKindUnavailable = unavailableLinuxRuntimeFact( +const gestureKindUnavailable = unavailableFact( 'unsupported-device-kind', 'Gestures are supported only for the Linux desktop device.', ); -const scrollKindUnavailable = unavailableLinuxRuntimeFact( +const scrollKindUnavailable = unavailableFact( 'unsupported-device-kind', 'scroll is supported only for the Linux desktop device.', ); @@ -61,62 +65,62 @@ const scrollKindUnavailable = unavailableLinuxRuntimeFact( * fling's speed semantics, which is the one gesture the retired admission refused BY PLATFORM * rather than by leaf or kind. */ -const directionalFlingUnavailable = unavailableLinuxRuntimeFact('unsupported-platform-leaf'); -const multiTouchUnavailable = unavailableLinuxRuntimeFact('unsupported-platform-leaf'); -const targetAuthoredDragUnavailable = unavailableLinuxRuntimeFact( +const directionalFlingUnavailable = unavailableFact('unsupported-platform-leaf'); +const multiTouchUnavailable = unavailableFact('unsupported-platform-leaf'); +const targetAuthoredDragUnavailable = unavailableFact( 'unsupported-platform-leaf', TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, ); /** No frame read of its own: a Linux gesture derives its viewport from a capture, as it does today. */ -const gestureViewportUnavailable = unavailableLinuxRuntimeFact('unsupported-platform-leaf'); -const runtimeHintsUnavailable = unavailableLinuxRuntimeFact( +const gestureViewportUnavailable = unavailableFact('unsupported-platform-leaf'); +const runtimeHintsUnavailable = unavailableFact( 'unsupported-platform-leaf', 'Runtime hints are supported only for local iOS-family simulators and Android devices.', ); -const appleRunnerUnavailable = unavailableLinuxRuntimeFact( +const appleRunnerUnavailable = unavailableFact( 'unsupported-platform-leaf', 'Apple runner preparation is supported only for Apple targets.', ); -const providerPortReverseUnavailable = unavailableLinuxRuntimeFact( +const providerPortReverseUnavailable = unavailableFact( 'unsupported-provider-mode', 'Port reverse is supported only by an owning provider runtime.', ); -const openTargetKindUnavailable = unavailableLinuxRuntimeFact( +const openTargetKindUnavailable = unavailableFact( 'unsupported-device-kind', 'open is supported only for the Linux desktop device.', ); -const closeTargetKindUnavailable = unavailableLinuxRuntimeFact( +const closeTargetKindUnavailable = unavailableFact( 'unsupported-device-kind', 'close is supported only for the Linux desktop device.', ); -const snapshotKindUnavailable = unavailableLinuxRuntimeFact( +const snapshotKindUnavailable = unavailableFact( 'unsupported-device-kind', 'snapshot is supported only for the Linux desktop device.', ); -const screenshotKindUnavailable = unavailableLinuxRuntimeFact( +const screenshotKindUnavailable = unavailableFact( 'unsupported-device-kind', 'screenshot is supported only for the Linux desktop device.', ); -const snapshotCustomActionsUnavailable = unavailableLinuxRuntimeFact( +const snapshotCustomActionsUnavailable = unavailableFact( 'unsupported-platform-leaf', 'Re-run without --actions, or target an iOS simulator.', ); -const backKindUnavailable = unavailableLinuxRuntimeFact( +const backKindUnavailable = unavailableFact( 'unsupported-device-kind', 'back is supported only for the Linux desktop device.', ); -const homeKindUnavailable = unavailableLinuxRuntimeFact( +const homeKindUnavailable = unavailableFact( 'unsupported-device-kind', 'home is supported only for the Linux desktop device.', ); -const clipboardKindUnavailable = unavailableLinuxRuntimeFact( +const clipboardKindUnavailable = unavailableFact( 'unsupported-device-kind', 'clipboard is supported only for the Linux desktop device.', ); // `orientation`, `tv-remote`, and every keyboard action never carried a Linux capability bucket // at all (the retired descriptors declared `linux: {}`), so they are unavailable unconditionally. const linuxPlatformLeafUnavailable = unsupportedPlatformLeaf; -const linuxAudioProbeUnavailable = unavailableLinuxRuntimeFact( +const linuxAudioProbeUnavailable = unavailableFact( 'unsupported-platform-leaf', 'audio is supported for web browser sessions, macOS sessions, iOS simulators, and Android emulators on macOS hosts', ); @@ -313,12 +317,3 @@ function linuxSnapshotOperations( captureSnapshotWithoutActiveApp: captureSnapshot, }); } - -function unavailableLinuxRuntimeFact( - reason: RuntimeOperationUnavailability['reason'], - hint?: string, -): RuntimeOperationUnavailability { - return Object.freeze( - hint === undefined ? { available: false, reason } : { available: false, reason, hint }, - ); -} diff --git a/packages/platform-vega/src/runtime.ts b/packages/platform-vega/src/runtime.ts index 064e5bd886..829bf35bd8 100644 --- a/packages/platform-vega/src/runtime.ts +++ b/packages/platform-vega/src/runtime.ts @@ -1,8 +1,4 @@ -import type { - DeviceBinding, - RuntimeFacts, - RuntimeOperationUnavailability, -} from '@agent-device/contracts/platform-runtime'; +import type { DeviceBinding, RuntimeFacts } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeHost, PlatformRuntimeOperations, @@ -15,7 +11,11 @@ import { import { backRuntimeOperationFacts } from '@agent-device/contracts/back-runtime'; import { systemButtonRuntimeOperationFacts } from '@agent-device/contracts/system-button-runtime'; import { bindAdmittedLocalInteractorOperations } from '@agent-device/contracts/interactor-operation-catalog'; -import { localRuntimeOwner, sameRuntimeOwner } from '@agent-device/contracts/platform-runtime'; +import { + localRuntimeOwner, + sameRuntimeOwner, + unavailableFact, +} from '@agent-device/contracts/platform-runtime'; import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable'; import { TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT } from '@agent-device/contracts/gesture-admission'; import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-runtime'; @@ -26,24 +26,24 @@ import { bindVegaApplicationLifecycle } from './lifecycle.ts'; const vegaOwner = localRuntimeOwner('vega'); const lifecycleAvailable = Object.freeze({ available: true } as const); -const unsupportedPlatformLeaf = vegaUnavailable('unsupported-platform-leaf'); -const runtimeHintsUnavailable = vegaUnavailable( +const unsupportedPlatformLeaf = unavailableFact('unsupported-platform-leaf'); +const runtimeHintsUnavailable = unavailableFact( 'unsupported-platform-leaf', 'Runtime hints are supported only for local iOS-family simulators and Android devices.', ); -const appleRunnerUnavailable = vegaUnavailable( +const appleRunnerUnavailable = unavailableFact( 'unsupported-platform-leaf', 'Apple runner preparation is supported only for Apple targets.', ); -const providerPortReverseUnavailable = vegaUnavailable( +const providerPortReverseUnavailable = unavailableFact( 'unsupported-provider-mode', 'Port reverse is supported only by an owning provider runtime.', ); -const openTargetUnavailable = vegaUnavailable( +const openTargetUnavailable = unavailableFact( 'unsupported-device-kind', 'open currently supports only Vega Virtual Devices.', ); -const closeTargetUnavailable = vegaUnavailable( +const closeTargetUnavailable = unavailableFact( 'unsupported-device-kind', 'close currently supports only Vega Virtual Devices.', ); @@ -88,25 +88,25 @@ export function createVegaPlatformRuntime(host: PlatformRuntimeHost): PlatformRu }); } -const screenshotUnavailable = vegaUnavailable( +const screenshotUnavailable = unavailableFact( 'unsupported-platform-leaf', 'screenshot is not supported on Vega OS: the Vega runtime exposes remote navigation only.', ); -const focusUnavailable = vegaUnavailable( +const focusUnavailable = unavailableFact( 'unsupported-platform-leaf', 'focus is not supported on Vega OS: the Vega runtime exposes remote navigation only.', ); -const typeUnavailable = vegaUnavailable( +const typeUnavailable = unavailableFact( 'unsupported-platform-leaf', 'type is not supported on Vega OS: the Vega runtime exposes remote navigation only.', ); -const gestureUnavailable = vegaUnavailable( +const gestureUnavailable = unavailableFact( 'unsupported-platform-leaf', 'Gestures are not supported on Vega OS: the Vega runtime exposes remote navigation only.', ); -const scrollUnavailable = vegaUnavailable( +const scrollUnavailable = unavailableFact( 'unsupported-platform-leaf', 'scroll is not supported on Vega OS: the Vega runtime exposes remote navigation only.', ); @@ -115,59 +115,59 @@ const scrollUnavailable = vegaUnavailable( * own wording: two-contact synthesis with no hint at all, and target-authored drag by naming the * phases an adapter has to preserve. */ -const multiTouchUnavailable = vegaUnavailable('unsupported-platform-leaf'); -const targetAuthoredDragUnavailable = vegaUnavailable( +const multiTouchUnavailable = unavailableFact('unsupported-platform-leaf'); +const targetAuthoredDragUnavailable = unavailableFact( 'unsupported-platform-leaf', TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, ); // `orientation` and every keyboard action never carried a Vega capability bucket at all; `back`, // `home`, and `tv-remote` did (the retired `vegaPlugin` closure), gated by the same VVD cell // their lifecycle open/close already require. -const orientationUnavailable = vegaUnavailable( +const orientationUnavailable = unavailableFact( 'unsupported-platform-leaf', 'orientation is not supported on Vega OS.', ); -const keyboardUnavailable = vegaUnavailable( +const keyboardUnavailable = unavailableFact( 'unsupported-platform-leaf', 'keyboard is not supported on Vega OS.', ); -const alertUnavailable = vegaUnavailable( +const alertUnavailable = unavailableFact( 'unsupported-platform-leaf', 'alert is not supported on Vega OS.', ); -const settingsUnavailable = vegaUnavailable( +const settingsUnavailable = unavailableFact( 'unsupported-platform-leaf', 'settings is not supported on Vega OS.', ); -const appEventUnavailable = vegaUnavailable( +const appEventUnavailable = unavailableFact( 'unsupported-platform-leaf', 'trigger-app-event is not supported on Vega OS.', ); -const systemButtonUnavailable = vegaUnavailable( +const systemButtonUnavailable = unavailableFact( 'unsupported-platform-leaf', 'System buttons other than home are not supported on Vega OS.', ); -const foldUnavailable = vegaUnavailable( +const foldUnavailable = unavailableFact( 'unsupported-platform-leaf', 'fold is not supported on Vega OS.', ); -const clipboardUnavailable = vegaUnavailable( +const clipboardUnavailable = unavailableFact( 'unsupported-platform-leaf', 'clipboard is not supported on Vega OS.', ); -const backUnavailable = vegaUnavailable( +const backUnavailable = unavailableFact( 'unsupported-device-kind', 'back currently supports only Vega Virtual Devices.', ); -const homeUnavailable = vegaUnavailable( +const homeUnavailable = unavailableFact( 'unsupported-device-kind', 'home currently supports only Vega Virtual Devices.', ); -const tvRemoteUnavailable = vegaUnavailable( +const tvRemoteUnavailable = unavailableFact( 'unsupported-device-kind', 'tv-remote currently supports only Vega Virtual Devices.', ); -const audioProbeUnavailable = vegaUnavailable( +const audioProbeUnavailable = unavailableFact( 'unsupported-platform-leaf', 'audio is supported for web browser sessions, macOS sessions, iOS simulators, and Android emulators on macOS hosts', ); @@ -245,15 +245,3 @@ function vegaFacts(device: DeviceInfo): RuntimeFacts }, }); } - -function vegaUnavailable( - reason: RuntimeOperationUnavailability['reason'], - hint?: string, -): RuntimeOperationUnavailability { - const fact: RuntimeOperationUnavailability = { - available: false, - reason, - ...(hint === undefined ? {} : { hint }), - }; - return Object.freeze(fact); -} diff --git a/packages/platform-web/src/runtime.ts b/packages/platform-web/src/runtime.ts index 883ac73069..ba6982bf2a 100644 --- a/packages/platform-web/src/runtime.ts +++ b/packages/platform-web/src/runtime.ts @@ -4,6 +4,7 @@ import { type RuntimeOperationFact, localRuntimeOwner, sameRuntimeOwner, + unavailableFact, whenAdmitted, } from '@agent-device/contracts/platform-runtime'; import type { @@ -66,54 +67,34 @@ import { bindWebApplicationLifecycle } from './lifecycle.ts'; const owner = localRuntimeOwner('web'); const available = Object.freeze({ available: true } as const); -const elementTextUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Web targets read element text from the captured tree only.', -} as const); -const appLogUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const recordingUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'record is not supported by this web provider', -} as const); -const snapshotCustomActionsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Re-run without --actions, or target an iOS simulator.', -} as const); -const readinessUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const appsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'apps is not supported on web targets.', -} as const); -const appStateUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); +const elementTextUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Web targets read element text from the captured tree only.', +); +const appLogUnavailable = unavailableFact('unsupported-platform-leaf'); +const recordingUnavailable = unavailableFact( + 'owner-capability-missing', + 'record is not supported by this web provider', +); +const snapshotCustomActionsUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Re-run without --actions, or target an iOS simulator.', +); +const readinessUnavailable = unavailableFact('unsupported-platform-leaf'); +const appsUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'apps is not supported on web targets.', +); +const appStateUnavailable = unavailableFact('unsupported-platform-leaf'); // `back`, `home`, `orientation`, `tv-remote`, and every keyboard action never carried a web // capability bucket (the retired `WEB_SUPPORTED_COMMANDS` overlay never listed them), so all are // unavailable unconditionally. -const navigationUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const prepareUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'Apple runner preparation is supported only for Apple targets.', -} as const); -const nativeRefUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing', -} as const); +const navigationUnavailable = unavailableFact('unsupported-platform-leaf'); +const prepareUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'Apple runner preparation is supported only for Apple targets.', +); +const nativeRefUnavailable = unavailableFact('owner-capability-missing'); /** * The web click affordance is one immediate pointer press, which is everything `press` needs for a * repeated `--count` series. Timed actions are a policy this owner declines rather than a missing @@ -121,16 +102,14 @@ const nativeRefUnavailable = Object.freeze({ * cell denies the hold rather than borrowing the tap cell that admits the series. The fused * double-click is declined one level down, by the interactor member the browser never supplies. */ -const holdUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'A web click is one immediate pointer press; the browser backend has no timed hold.', -} as const); -const audioCaptureUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: 'audio on web sessions is the stateless page probe; host capture is a macOS-host operation.', -} as const); +const holdUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'A web click is one immediate pointer press; the browser backend has no timed hold.', +); +const audioCaptureUnavailable = unavailableFact( + 'unsupported-platform-leaf', + 'audio on web sessions is the stateless page probe; host capture is a macOS-host operation.', +); function webOptionalOperationFact(operation: unknown, browserDevice: RuntimeOperationFact) { return typeof operation === 'function' ? browserDevice : nativeRefUnavailable; @@ -141,30 +120,23 @@ function webAvailableFact(condition: boolean, unavailable: RuntimeOperationFact) } /** `gesture` and `swipe` never carried a web capability bucket; the browser drives no synthesis. */ -const gestureUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', -} as const); -const targetAuthoredDragUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, -} as const); -const openTargetKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'open is supported only for web browser devices.', -} as const); -const closeTargetKindUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'close is supported only for web browser devices.', -} as const); -const portReverseUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Port reverse is supported only by an owning provider runtime.', -} as const); +const gestureUnavailable = unavailableFact('unsupported-platform-leaf'); +const targetAuthoredDragUnavailable = unavailableFact( + 'unsupported-platform-leaf', + TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, +); +const openTargetKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'open is supported only for web browser devices.', +); +const closeTargetKindUnavailable = unavailableFact( + 'unsupported-device-kind', + 'close is supported only for web browser devices.', +); +const portReverseUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Port reverse is supported only by an owning provider runtime.', +); function webLifecycleFacts(device: DeviceInfo) { const openTarget = webOpenTargetFact(device); @@ -355,11 +327,10 @@ function webRuntimeFacts( recordingAvailable: boolean, interactor: Interactor | undefined, ): RuntimeFacts { - const networkUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'network is not supported by this web provider', - } as const); + const networkUnavailable = unavailableFact( + 'owner-capability-missing', + 'network is not supported by this web provider', + ); // One browser-device cell, read by every operation this runtime binds through the interactor. const browserDevice = webAvailableFact(device.kind === 'device', openTargetKindUnavailable); return Object.freeze({