From ac73e119c3d1d4bf8361563d64a011c0e4931a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 1 Jul 2026 11:22:57 +0200 Subject: [PATCH] feat: gate watchOS as an explicit unsupported sentinel (closes #977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XCUITest cannot drive watchOS UI (no XCUIApplication), so a watchOS device has no runner backend. Today `appleOs: 'watchos'` silently falls through to the iOS runner profile (resolveRunnerPlatformNameForAppleOs). Reject it explicitly at the admission seam — createAppleInteractor — with a clear UNSUPPORTED_PLATFORM error, before any runner work. Discovery never produces watchOS today (resolveAppleOs only yields ios/ipados/tvos/visionos), so this changes no real-device behavior; it makes the "declared but unsupported" watchOS case honest instead of a silent wrong fallback. Per-AppleOS capability-table integration is deferred to d.5 (#978). Adds a focused unit test asserting the watchOS rejection and that a non-watchOS appleOs does not trigger the sentinel. --- .../__tests__/apple-watchos-sentinel.test.ts | 50 +++++++++++++++++++ src/core/interactors/apple.ts | 10 ++++ 2 files changed, 60 insertions(+) create mode 100644 src/core/interactors/__tests__/apple-watchos-sentinel.test.ts diff --git a/src/core/interactors/__tests__/apple-watchos-sentinel.test.ts b/src/core/interactors/__tests__/apple-watchos-sentinel.test.ts new file mode 100644 index 0000000000..32279566b0 --- /dev/null +++ b/src/core/interactors/__tests__/apple-watchos-sentinel.test.ts @@ -0,0 +1,50 @@ +import { test, expect } from 'vitest'; +import { createAppleInteractor } from '../apple.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; +import type { RunnerContext } from '../../interactor-types.ts'; +import { AppError } from '../../../kernel/errors.ts'; + +// watchOS is an explicit unsupported sentinel: XCUITest cannot drive watchOS UI, +// so a `appleOs: 'watchos'` device must be rejected at interactor creation (the +// admission seam) rather than silently falling through to the iOS runner. +const watchOsDevice: DeviceInfo = { + platform: 'ios', + id: 'watch-1', + name: 'Apple Watch Series 10', + kind: 'device', + appleOs: 'watchos', +}; + +test('createAppleInteractor rejects a watchOS device as UNSUPPORTED_PLATFORM', () => { + // The guard throws before touching runnerContext, so an empty context is fine. + const create = () => createAppleInteractor(watchOsDevice, {} as RunnerContext); + expect(create).toThrow(AppError); + try { + create(); + expect.unreachable('expected createAppleInteractor to throw for watchOS'); + } catch (error) { + expect(error).toBeInstanceOf(AppError); + expect((error as AppError).code).toBe('UNSUPPORTED_PLATFORM'); + expect((error as AppError).message).toMatch(/watchOS/i); + } +}); + +test('a non-watchOS appleOs does not trigger the watchOS sentinel', () => { + // A tvOS device must not throw the watchOS-specific rejection. (It may still + // fail later on the empty runner context, so we only assert it is not the + // watchOS sentinel error.) + const tvOsDevice: DeviceInfo = { + platform: 'ios', + id: 'tv-1', + name: 'Apple TV 4K', + kind: 'simulator', + target: 'tv', + appleOs: 'tvos', + booted: true, + }; + try { + createAppleInteractor(tvOsDevice, {} as RunnerContext); + } catch (error) { + expect((error as AppError).message ?? '').not.toMatch(/watchOS/i); + } +}); diff --git a/src/core/interactors/apple.ts b/src/core/interactors/apple.ts index 87ec130575..69b8666e17 100644 --- a/src/core/interactors/apple.ts +++ b/src/core/interactors/apple.ts @@ -28,6 +28,16 @@ export function createAppleInteractor( device: DeviceInfo, runnerContext: RunnerContext, ): Interactor { + // watchOS unsupported sentinel: XCUITest cannot drive watchOS UI (no + // XCUIApplication), so a watchOS device has no runner backend. Reject it + // explicitly here rather than letting `appleOs: 'watchos'` silently fall + // through to the iOS runner profile (see resolveRunnerPlatformNameForAppleOs). + if (device.appleOs === 'watchos') { + throw new AppError( + 'UNSUPPORTED_PLATFORM', + 'watchOS is not supported: XCUITest cannot drive watchOS UI, so this device has no runner backend.', + ); + } const { overrides, runnerOpts } = iosRunnerOverrides(device, runnerContext); return { open: (app, options) =>