Start
Quick start
This page takes you from an empty directory to a passing test on an iOS Simulator, its HTML report, and a live
view of the device. It uses five commands: troupe init, troupe install, troupe test, troupe show-report
and troupe studio.
Before you start
You need a Mac with Apple silicon, Xcode with an iOS simulator runtime, and Bun 1.4.3 or newer. You also need a
simulator build of your app: a .app built for the iOS Simulator. Install has the full list,
including what Android needs.
1. Add Troupe to your project
In your app's directory, or a new one:
bun add -d --exact @alivelabs/troupeRun this before any bunx troupe command. bunx runs the troupe command from the project's node_modules;
where there is none it fetches whatever npm calls troupe, which is not this.
2. Create the test project
Name your simulator build with --app:
bunx troupe init --app ./build/MyApp.appIn a new directory, after step 1, it prints:
troupe init in /Users/you/shop:
wrote troupe.config.ts (ios: iPhone 18 Pro, 27.0, ./build/MyApp.app; android: commented out)
wrote e2e/example.e2e.ts
added package.json: scripts.e2e
kept package.json: @alivelabs/troupe
wrote .gitignore: /test-results/, /troupe-report/, /blob-report/After those lines it lists the commands to run next, which are the steps below. The device type and iOS
version are the defaults installed on your Mac. troupe init never replaces a file: what exists is skipped,
and it says so. The config it wrote, with its comments left out:
import { defineConfig } from '@alivelabs/troupe'
export default defineConfig({
testDir: 'e2e',
reporter: [['list'], ['html']],
use: {
trace: 'retain-on-failure',
},
projects: [
{
name: 'ios',
use: {
platform: 'ios',
deviceType: 'iPhone 18 Pro',
os: '27.0',
app: './build/MyApp.app',
},
},
],
})An Android project is written too, commented out until you name an .apk. --app takes one iOS build and one
Android build: bunx troupe init --app ./build/MyApp.app --app ./build/my-app.apk.
3. Prepare the Mac
bunx troupe install iosThis runs once. It records your Xcode in ~/.troupe/host.json, runs the environment checks (each failure prints
its fix), builds the XCUITest runner and a simulator template, and proves the device path with one simulator.
The first run can take minutes. It ends with the line ios-simulator: a device was ready in <n> s., where
<n> is the time it took.
4. Run the test
bunx troupe testThe first run starts the runtime daemon in the background, boots a simulator named troupe-… and installs your
app. A released device stays warm for five minutes, so a run soon after reuses it. With the durations left out,
the list reporter prints:
Running 1 test using 1 worker
✓ 1 [ios] › e2e/example.e2e.ts:11:1 › the app launches and shows its first screen (…)
1 passed (…)The example test passes on any app. It waits for the app's first screen and attaches a screenshot of it:
import { expect, test } from '@alivelabs/troupe'
test('the app launches and shows its first screen', async ({ screen }, testInfo) => {
// The screen's accessibility tree, as the device reports it; an app that shows anything has one.
await expect.poll(() => screen.ariaSnapshot()).not.toBe('')
await testInfo.attach('first screen', {
body: await screen.screenshot(),
contentType: 'image/png',
})
})5. Open the report
bunx troupe show-reportIt serves troupe-report/ on a loopback port, prints the address and runs until you press Ctrl+C. It does not
open a browser: open the address yourself. The report lists every test with its steps and attachments, and
links each kept trace.
A failing test keeps its trace by default. To keep every test's trace and open one in the trace viewer:
bunx troupe test --trace on
bunx troupe show-trace test-results/<test>/trace.zip6. Write your own test
Add a file in e2e/ whose name ends in .e2e.ts. Locators find elements on the device and wait for them;
assertions retry until the screen matches. For an app with a sign-in form:
// e2e/sign-in.e2e.ts
import { expect, test } from '@alivelabs/troupe'
test('signs in', async ({ screen }) => {
await screen.getByLabel('Email').fill('a@b.co')
await screen.getByRole('button', { name: 'Sign in' }).tap()
await expect(screen.getByRole('heading', { name: 'Welcome back' })).toBeVisible()
})7. Watch the device
bunx troupe studioIt prints an address of the form http://127.0.0.1:<port>/#otc=<code>. Open it in a browser on the same Mac
within a minute: the code in the address pairs the page with the runtime and works once. In the studio you create
a session, watch and drive the device, inspect the accessibility tree and read the logs. The tree is where you
find the roles, labels and test ids to use in locators.