Troupeby AliveDocsSign in

Platforms

Mac Catalyst

Troupe runs a Mac Catalyst build of your app as a phone-sized window on your Mac and drives it from inside the app's own process. It needs no simulator, and it does less than a simulator can.

The platform's id is catalyst.

Requirements

  • macOS on Apple silicon, with the user who runs Troupe logged in to the desktop: the app opens a window. macOS 27.0 is verified.
  • Bun 1.4.3 or newer.
  • Xcode with the Mac Catalyst SDK, only to build the app. Running a built app needs no Xcode.
  • No Accessibility or Screen Recording permission.
  • Memory: Troupe budgets 500 MB for a session.

Build an app for this platform

The app is a Mac Catalyst build that links and embeds TroupeCatalystHost.framework. The framework's Xcode project, TroupeCatalystHost.xcodeproj, is in Troupe's repository under native/catalyst-host. It is not part of the npm package: your app's Xcode project references it.

With XcodeGen:

YAML
projectReferences:
  TroupeCatalystHost:
    path: ../troupe/native/catalyst-host/TroupeCatalystHost.xcodeproj
targets:
  MyAppCatalyst:
    type: application
    platform: iOS
    dependencies:
      - target: TroupeCatalystHost/TroupeCatalystHost
    settings:
      base:
        SUPPORTS_MACCATALYST: YES

The host starts itself when the framework loads. It does nothing unless Troupe launched the app, so the same build runs as usual when you open it yourself.

The app must adopt the UIScene life cycle. A SwiftUI App does.

Troupe checks the bundle before it runs anything and refuses with APP_INSTALL_FAILED:

ReasonThe bundle
wrong-platformIs an iOS Simulator build, or a Mac app that is not Catalyst
missing-scene-manifestHas no scene manifest
invalid-bundleDoes not embed Troupe's host

Run tests

Name the platform, a preset and the built app in troupe.config.ts:

TypeScript
import { defineConfig } from '@alivelabs/troupe'

export default defineConfig({
  use: { platform: 'catalyst', deviceType: 'iphone-17', app: './build/MyApp.app' },
})

Then check the Mac once and run:

Shell
bunx troupe install catalyst
bunx troupe test

troupe install catalyst builds nothing. It checks that the Mac is on Apple silicon and that a desktop session is logged in.

How it runs

A Catalyst "device" is a preset, which gives the window's size and the screenshot scale, plus a directory of Troupe's own under $TROUPE_HOME/catalyst/devices/. The directory holds the installed copy of the app and the home directory the app runs with, so app data stays on the device and is deleted with it.

At launch the runtime starts the app's executable as its own child process. The host inside the app listens on a Unix socket, and the runtime sends it queries and actions. One app runs per device: a launch ends the one running. If the runtime stops, the app exits by itself.

Capabilities

Catalyst input is semantic. A tap() activates the element, as assistive technology does, instead of touching a point on the screen.

AreaMac Catalyst
LocatorsRole, text, label, placeholder and test id, with regular expressions. No system UI
Test idaccessibilityIdentifier
Inputtap(), fill(), type() and scrolling
KeyboardText. There are no key events
ScreenshotsYes
LogsApp log and crash reports
OSThe Mac's own macOS. It cannot be chosen

Limits

The runtime refuses each of these with CAPABILITY_UNSUPPORTED:

  • Touch: a tap on a point, double tap, long press, swipe, drag, pinch and gestures.
  • Key presses, hardware buttons, rotation and hiding the keyboard.
  • Launch arguments and launch environment variables, deep links, clearing an app's data and permissions.
  • Location, appearance and the clipboard.
  • The live stream and screen recording. Troupe Studio shows no live picture for a Catalyst session; the app's window is on your desktop.

The test framework checks a project's options against these before any test runs. A project that sets launchArgs, launchEnv, permissions or appIsolation: 'clear' on Catalyst is refused, with the option and the missing capability in the message.

A suite that also runs on simulators can skip what Catalyst lacks with test.requires:

TypeScript
test.describe('gestures', () => {
  test.requires('input.touch')

  test('a long press is recognised', async ({ screen }) => {
    await screen.getByTestId('gesture-area').longPress()
    await expect(screen.getByTestId('last-gesture')).toHaveText('Last gesture: long press')
  })
})

Two more things differ from a simulator:

  • A SwiftUI view backed by a UIKit view (TextField, SecureField, List) has no test id in the tree. Find it by placeholder or role. SwiftUI's own views, such as Button, Text and Toggle, keep theirs.
  • Traces default to 'on-first-retry' instead of 'retain-on-failure', because a screenshot after every action costs more here. Set trace yourself to change it. See Traces.

Presets and OS

deviceType names a preset by id or by name:

deviceTypeNameScreen
iphone-17iPhone 17 (the default)402 × 874 points at 3×
iphone-17-pro-maxiPhone 17 Pro Max440 × 956 points at 3×
iphone-seiPhone SE375 × 667 points at 2×
ipad-pro-11iPad Pro 11-inch834 × 1210 points at 2×

Leave os out. The only value a spec may name is the Mac's macOS major version; anything else is DEVICE_SPEC_INVALID.

Host settings

platforms.catalyst in $TROUPE_HOME/host.json takes these keys:

KeyDefaultWhat it does
enabledtruefalse switches the platform off: a request for it fails with ENVIRONMENT_PLATFORM_DISABLED
maxDevices2The most Catalyst devices running at once
defaultDeviceTypeiphone-17The preset a spec gets when it names none

Next

esc

    Type to search every page, the protocol reference and the cloud API.