1+ <div align =" center " >
2+
13# DeviceDeck
24
3- ** Your simulators and emulators in a browser — and the session you drive by hand becomes a test
4- your existing framework can run.**
5+ ** Your simulators and emulators become a webpage — drive them with the test tools you already
6+ own, and turn a session you did by hand into a test that runs unchanged on real hardware.**
7+
8+ <sub >iOS  ; +  ; Android  ; ·  ; native UI mirrored as ** real DOM**   ; ·  ; Flow  ; Capture  ; ·  ; MCP  ; server  ; for  ; agents  ; ·  ; Playwright  ; /  ; Cypress  ; /  ; Puppeteer  ; with  ; zero  ; adapter</sub >
9+
10+ ![ License] ( https://img.shields.io/badge/license-Apache_2.0-blue.svg )
11+ ![ Platform] ( https://img.shields.io/badge/platform-macOS-lightgrey?logo=apple )
12+ ![ iOS + Android] ( https://img.shields.io/badge/devices-iOS_%2B_Android-success )
13+ ![ Go] ( https://img.shields.io/badge/Go-1.26-00ADD8?logo=go&logoColor=white )
14+ ![ Single binary] ( https://img.shields.io/badge/ships-single_binary-brightgreen )
15+
16+ [ Quick start] ( #quick-start ) · [ Examples] ( examples/ ) · [ Drive with an agent] ( examples/mcp/ ) · [ How it works] ( #the-mirror-is-the-point ) · [ Known limits] ( #known-limits )
17+
18+ </div >
19+
20+ ---
21+
22+ ## The mirror is the point
523
624A single Go binary. It streams an iOS Simulator or Android emulator to a browser, mirrors the
725app's native UI tree as ** real DOM** , and records what you do into a replayable Maestro flow.
826
9- The mirror is the point. Because the device is an ordinary web page, the tools you already own
10- drive it with no mobile-specific code and no Appium :
27+ Because the device is an ordinary web page, the tools you already own drive it with no
28+ mobile-specific code, no Appium, and no coordinates :
1129
1230``` ts
1331await page .goto (` /device/${udid }?app=dev.devicelab.testhive ` );
@@ -17,7 +35,34 @@ await page.getByRole('button', { name: 'Sign In' }).click();
1735await expect (page .getByText (' Hello, devicelab!' )).toBeVisible ();
1836```
1937
20- That is a real iOS Simulator being driven by stock Playwright.
38+ That is a real iOS Simulator being driven by stock Playwright. The same page, the same selectors,
39+ drive an Android emulator — and an AI agent drives it through an MCP server.
40+
41+ ## What you get
42+
43+ - ** A device that is a webpage.** The native accessibility tree is mirrored as real DOM —
44+ ` data-testid ` from identifiers, ARIA roles from element types — so ` getByTestId ` , ` getByRole ` ,
45+ ` click ` , and ` fill ` all just work. No driver, no DSL, no coordinates.
46+ - ** The tools you already know.** Playwright, Cypress, and Puppeteer each drive both platforms
47+ with zero adapter; any browser driver does. See runnable [ ` examples/ ` ] ( examples/ ) .
48+ - ** Flow Capture.** Record a session by hand and it becomes a Maestro flow with ** durable
49+ selectors** that replays unchanged on real hardware — the funnel from "I did this once" to
50+ "this is a test."
51+ - ** Agents, first-class.** ` devicedeck mcp ` is a Model Context Protocol server, and a
52+ [ Claude Code plugin] ( examples/mcp/ ) ships with it — an agent lists, boots, launches, inspects,
53+ taps, and screenshots devices through the one core.
54+ - ** Typing that actually lands.** Every keystroke is verified against the device's own read-back
55+ and retyped if it drifted, so a loaded machine does not silently drop a character.
56+ - ** One binary.** No relay, no agent split, no per-tool fork — the same core behind every surface.
57+
58+ ## Why not Appium
59+
60+ Every mobile testing framework rebuilds the same four primitives — a driver, a selector language,
61+ a way to send input, a way to read the tree. DeviceDeck doesn't ship any of them. It makes the
62+ device a ** web page** , so a web driver you already have * is* the driver, its selectors * are* the
63+ selectors, and its assertions * are* the assertions. The one thing it insists on is ** durable
64+ selectors over coordinates** , because that is what lets a flow captured on a simulator run
65+ unchanged on a real device — which is the whole point.
2166
2267## Status
2368
@@ -29,16 +74,16 @@ password fields. A captured flow replays unchanged under a stock `maestro-runner
297450/50 on Android with assertions, and a deliberate negative control confirming the gate can fail.
3075
3176What that sentence does not cover: it has only ever run on the machine it was built on. Expect
32- first-contact problems. See ** Known limits** below.
77+ first-contact problems. See [ ** Known limits** ] ( #known-limits ) below.
3378
3479## Requirements
3580
36- - macOS (the video and input sidecars talk to CoreSimulator)
37- - Xcode with at least one iOS Simulator runtime — ** iOS 26.2 or newer is strongly recommended ** ;
38- on 18.6 the simulator's render server crashes under repeated capture
39- - For Android: the Android SDK with ` adb ` and ` emulator ` on ` PATH `
81+ - ** macOS** (the video and input sidecars talk to CoreSimulator)
82+ - ** Xcode** with at least one iOS Simulator runtime — ** iOS 26.2 or newer is strongly
83+ recommended ** ; on 18.6 the simulator's render server crashes under repeated capture
84+ - ** For Android:** the Android SDK with ` adb ` and ` emulator ` on ` PATH `
4085
41- ## Install
86+ ## Quick start
4287
4388Download a release and run it — the archive holds the binary and its two sidecars, which it
4489expects to find beside itself:
@@ -49,15 +94,18 @@ cd devicedeck-<version>-darwin-arm64
4994./devicedeck serve
5095```
5196
52- From source:
97+ Then open ** < http://127.0.0.1:8787 > ** , pick a device, and it boots and starts streaming.
98+
99+ <details >
100+ <summary ><strong >Build from source</strong ></summary >
53101
54102``` bash
55103make sidecar # builds the Swift sidecars (macOS, Xcode toolchain)
56104make build
57105./devicedeck serve
58106```
59107
60- Then open < http://127.0.0.1:8787 > .
108+ </ details >
61109
62110## Using it
63111
@@ -98,9 +146,10 @@ field's contents live in its value, not its text, which is the one place the mir
98146" every native node is a div" .
99147
100148**Your own tests** point at `/device/{udid}?app={bundleId}` and use ordinary selectors.
101- ` examples/` has runnable Playwright, Cypress and Puppeteer projects, and `examples/captured/`
102- holds flows recorded through the console — including one from a Flutter app and one from React
103- Navigation. Note that **a device serves one driver at a time**; run with a single worker.
149+ [`examples/`](examples/) has runnable Playwright, Cypress and Puppeteer projects — each with a
150+ login and a checkout journey — and [`examples/captured/`](examples/captured/) holds flows recorded
151+ through the console, including one from a Flutter app and one from React Navigation. Note that
152+ **a device serves one driver at a time**; run with a single worker.
104153
105154**Agents drive it through an MCP server.** `devicedeck mcp` speaks the Model Context Protocol on
106155stdin/stdout — a thin adapter over the same API — so an agent (Claude, Cursor, any MCP client)
@@ -141,14 +190,14 @@ on durable selectors.
141190
142191# # Licence
143192
144- Apache License 2.0 — see `LICENSE`.
193+ Apache License 2.0 — see [ `LICENSE`](LICENSE) .
145194
146195# # Built on
147196
148197[maestro-runner](https://github.com/devicelab-dev/maestro-runner) for device drivers and flow
149198replay, both Apache-2.0. The Swift sidecars derive from
150199[baguette](https://github.com/tddworks/baguette) (Apache-2.0) and
151- [tapflow](https://github.com/jo-duchan/tapflow) (MIT); `ATTRIBUTION.md` records what was reused
152- and where it lives.
200+ [tapflow](https://github.com/jo-duchan/tapflow) (MIT); [ `ATTRIBUTION.md`](ATTRIBUTION.md) records
201+ what was reused and where it lives.
153202
154- Built by [DeviceLab.dev](https://devicelab.dev)
203+ Built by [** DeviceLab.dev** ](https://devicelab.dev)
0 commit comments