diff --git a/.claude/skills/update-from-upstream/SKILL.md b/.claude/skills/update-from-upstream/SKILL.md index d5aef74f0c..dbfc569356 100644 --- a/.claude/skills/update-from-upstream/SKILL.md +++ b/.claude/skills/update-from-upstream/SKILL.md @@ -28,6 +28,7 @@ The fork adds, on top of upstream Maestro: | `feat(orchestra): add readFile command and document fork additions` | `readFile` loads a JSON file into a namespaced object variable (`${data.field}`); adds object-typed JS bindings (`JsEngine.putObjectEnv`) via GraalJS proxies. Also introduces `FORK.md`. | | `feat(web): Support attaching to a running Chrome/Electron over CDP` | Attach mode (`--cdp-url` / `--cdp-target`) drives an already-running webview instead of launching Chrome; input routed over CDP since there is no Selenium session. | | `chore(fork): add verify-fork-stack skill and resync fork docs` | Fork tooling: the `verify-fork-stack` skill plus the `FORK.md` / stack-table reconciliation it enforces. | +| `feat(roku): add Roku platform support` | `RokuDriver` over the External Control Protocol (HTTP on device port 8060 — no on-device agent), `RokuEcpClient`/`RokuAppUIParser`/`RokuKeyMapping`, device discovery (`MAESTRO_ROKU_HOST` pin or SSDP scan), `DeviceSpec.Roku`, `RokuLocale`, three new remote KeyCodes (`Remote Info`/`Instant Replay`/`Search`, also mapped on Android), Studio TV mode, and the `e2e/roku_demo_app` fixture. D-pad-only: `tapOn` sends `Select`, swipes are repeated D-pad presses. Unsupported primitives (`setLocation`, `setProxy`, `setDarkMode`, `addMedia`, screen recording) throw or no-op deliberately. Contributed by the Nami team. | This table is the **fork commit stack** — keyed on commit subject, not SHA (rebasing rewrites SHAs, subjects are stable). Keep it in sync with `git log --oneline upstream/main..main`. diff --git a/AGENTS.md b/AGENTS.md index b0c09ed428..c5b1a2e220 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Shared context for any Claude Code skill or subagent operating in this repo. Skills (`.claude/skills/*`) reference this file rather than restating module roles; if a description here drifts from reality, fix it here once and every skill follows. -> **This is a fork.** [`FORK.md`](FORK.md) lists every capability `plexinc/Maestro` adds on top of upstream — fork-only flow commands (`readFile`, `@alias` path references), platforms (tvOS, Vega/Fire TV), web-driver enhancements, bundled Studio, and distribution. Read it before writing flows or tests so you don't treat fork features as missing or reinvent them. Any new fork-only feature must be documented there in the same commit. +> **This is a fork.** [`FORK.md`](FORK.md) lists every capability `plexinc/Maestro` adds on top of upstream — fork-only flow commands (`readFile`, `@alias` path references), platforms (tvOS, Vega/Fire TV, Roku), web-driver enhancements, bundled Studio, and distribution. Read it before writing flows or tests so you don't treat fork features as missing or reinvent them. Any new fork-only feature must be documented there in the same commit. ## Module map @@ -14,7 +14,7 @@ Top-level Gradle modules. Code lives under each module's `src/main/`. | `maestro-ios-driver/` | Host side of iOS driver wrapper (Kotlin). The actual XCTest runner lives in `maestro-ios-xctest-runner/`. | | `maestro-ios-xctest-runner/` | Swift XCTest runner that runs on the iOS device/simulator. The compiled artifacts (`maestro-driver-ios*.zip`) are checked in under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`. | | `maestro-ios/` | iOS host-side glue (small — most iOS host code lives in `maestro-client/`). | -| `maestro-client/` | Host-side Kotlin SDK that drives devices. Platform drivers live in `src/main/java/maestro/drivers/`: `AndroidDriver.kt`, `IOSDriver.kt`, `WebDriver.kt`, `CdpWebDriver.kt`. This is where most "auto-grant", "auto-dismiss", system-dialog handling and platform-specific quirks belong. | +| `maestro-client/` | Host-side Kotlin SDK that drives devices. Platform drivers live in `src/main/java/maestro/drivers/`: `AndroidDriver.kt`, `IOSDriver.kt`, `WebDriver.kt`, `CdpWebDriver.kt`, `VegaDriver.kt`, `RokuDriver.kt`. This is where most "auto-grant", "auto-dismiss", system-dialog handling and platform-specific quirks belong. | | `maestro-orchestra/` | Command execution layer. `Orchestra.kt` interprets each Maestro command, applies retries, manages the command lifecycle. Sub-packages: `error/`, `filter/`, `workspace/`, `yaml/`. | | `maestro-orchestra-models/` | Shared command/data models (used by `maestro-orchestra/` and consumers). | | `maestro-cli/` | CLI entry point + MCP server. Mixed Kotlin (~100 files) + Swift (~56 files for iOS-related CLI bits). | diff --git a/FORK.md b/FORK.md index 3eb5773c51..ce54980043 100644 --- a/FORK.md +++ b/FORK.md @@ -113,6 +113,61 @@ Demo app + flows under `e2e/vega_demo_app/` and `e2e/workspaces/vega_demo_app/`. Code: `DeviceSpec.Vega`, `maestro/device/DeviceService.kt`, `VegaLocale`. Commit `1743bfdf`. +### Roku + +Contributed by the [Nami](https://www.nami.ml) team ([rku.dev](https://www.rku.dev)). + +Driver for Roku devices over the **External Control Protocol** (ECP — an HTTP +REST API on device port 8060). Platform id `ROKU` (`--platform roku`). No +on-device driver process: view hierarchy from `/query/app-ui` (SceneGraph XML, +dev-mode channels only), D-pad input via `/keypress/`, text via +character-by-character `LIT_` keypresses, screenshots via the dev web server +(digest auth), app launch via `/launch/`. Roku is D-pad-only — `tapOn` +sends `Select` (activates the focused element), swipes/scrolls become repeated +D-pad presses in the direction the *content* moves — `swipe: UP` reveals what is +below it, so it presses Down, and `scroll` is `swipe: UP`, as on Vega and web. +`launchApp` is a cold launch: a channel that is already running is exited to the +home screen first so it restarts from its initial state, and it fails the flow if +the channel never becomes the active app. A launch (or an `openLink` deep link) +carries only the parameters the flow asked for. + +A rejected ECP command fails the flow rather than logging a warning — otherwise a +device with ECP access set to anything but Permissive serves `/query/app-ui` while +403-ing every keypress, and a flow passes green having never touched the device. +The 403 is reported with that setup hint. Nodes the device isn't rendering +(`visible="false"`, `opacity="0"`) are dropped from the hierarchy with their +subtrees, so `assertVisible` won't match a hidden element. + +**Device setup** (physical hardware only — Roku has no emulator): + +1. Enable developer mode: Home 3x, Up 2x, Right, Left, Right, Left, Right; set a + dev password. +2. Set ECP network access to Permissive: Settings > System > Advanced system + settings > Control by mobile apps > Network access > **Permissive** (recent + Roku OS versions return 403 on input commands otherwise). + +**Device selection** — Roku devices surface through the normal device listing: + +```bash +export MAESTRO_ROKU_HOST=192.168.1.100 # pin a device by IP (primary) +export MAESTRO_ROKU_PASSWORD=devpwd # dev-mode password (screenshots only) +export MAESTRO_ROKU_DISCOVERY=true # optional: SSDP LAN scan (~1s per listing) + +maestro test --platform roku flow.yaml +maestro test --device 192.168.1.100 flow.yaml +``` + +**New KeyCodes** (also mapped on Android): `Remote Info` (the `*` options +button), `Remote Instant Replay`, `Remote Search`. `Remote Menu` maps to `Info` +on Roku. Studio's TV mode is auto-on for Roku, like tvOS. + +Demo channel + flows under `e2e/roku_demo_app/` (BrightScript; sideload steps in +the flow headers) and `e2e/workspaces/roku_demo_app/` — mirrors the tvOS/Vega +demo apps (same screens, labels, and testIDs: Home menu, 2x2 navigation grid, +native-Keyboard text input, programmatic-focus test). +Code: `maestro/roku/` (`RokuEcpClient`, `RokuDeviceDiscovery`, `RokuAppUIParser`, +`RokuKeyMapping`), `drivers/RokuDriver.kt`, `DeviceSpec.Roku`, `RokuLocale`. + ### Web driver enhancements (canvas / D-pad TV web apps) For D-pad-driven or WebGL/canvas web apps (e.g. Lightning). Platform id `WEB`. diff --git a/README.md b/README.md index 89bb02e1dc..2fc10ad75b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - **Apple TV (tvOS) support** — a tvOS driver, device management (`start-device --platform tvos`), an RN Expo tvOS demo app, and tvOS e2e flows. - **Amazon Vega (Fire TV) support** — a driver for Amazon's Vega OS (a Linux/React Native OS, not Android), driven through the `vega`/`vda` CLI: view hierarchy from the on-device automation toolkit, D-pad/touch/swipe/text input, and screenshots. +- **Roku support** — a driver for Roku devices over the External Control Protocol (an HTTP API on the device, no on-device agent): SceneGraph view hierarchy, D-pad/remote-key input, text entry, screenshots, and network device discovery (`MAESTRO_ROKU_HOST` pin or SSDP scan). Contributed by the [Nami](https://www.nami.ml) team ([rku.dev](https://www.rku.dev)). - **Canvas-based UI (web) support** — for D-pad-driven TV web apps (e.g. Lightning/WebGL): `data-testid`-first element selection, D-pad/arrow-key mapping (with Menu→back), URL-shaped `appId` detection, a `focused:` selector, and `--platform web` device selection. - **Bundled Maestro Studio, restored and TV-aware** — re-adds the local `maestro studio` web IDE that upstream removed, plus a "TV mode" (auto-on for tvOS, toggleable for web) where taps/swipes and physical arrow keys / Enter / Esc drive the D-pad / select / back. - **`major.minor.patch.build` versioning** — `CLI_VERSION` tracks upstream Maestro; a fork-owned `PLEX_BUILD` segment lets us ship fork-only builds without drifting from the inherited version. diff --git a/e2e/roku_demo_app/components/FocusScreen.brs b/e2e/roku_demo_app/components/FocusScreen.brs new file mode 100644 index 0000000000..d9c11ca4c6 --- /dev/null +++ b/e2e/roku_demo_app/components/FocusScreen.brs @@ -0,0 +1,45 @@ +' Focus — verifies programmatic focus: Button 2 (not the first button) receives +' focus when the screen opens, the Roku equivalent of the tvOS/Vega screen's +' requestTVFocus + hasTVPreferredFocus. Same labels and testIDs as those apps. +sub init() + m.buttons = [ + m.top.findNode("focus-button-1"), + m.top.findNode("focus-button-2"), + m.top.findNode("back-button") + ] + m.focusIndex = 1 + + m.top.findNode("back-button").observeField("buttonSelected", "onBackSelected") + m.top.observeField("visible", "onVisibleChanged") +end sub + +sub onVisibleChanged() + if m.top.visible then + ' Programmatic focus lands on Button 2, not the first button. + m.focusIndex = 1 + m.buttons[m.focusIndex].setFocus(true) + end if +end sub + +sub onBackSelected() + m.top.done = true +end sub + +function onKeyEvent(key as string, press as boolean) as boolean + if not press then return false + + if key = "back" then + m.top.done = true + return true + else if key = "down" and m.focusIndex < m.buttons.count() - 1 then + m.focusIndex = m.focusIndex + 1 + m.buttons[m.focusIndex].setFocus(true) + return true + else if key = "up" and m.focusIndex > 0 then + m.focusIndex = m.focusIndex - 1 + m.buttons[m.focusIndex].setFocus(true) + return true + end if + + return false +end function diff --git a/e2e/roku_demo_app/components/FocusScreen.xml b/e2e/roku_demo_app/components/FocusScreen.xml new file mode 100644 index 0000000000..c8cba7293e --- /dev/null +++ b/e2e/roku_demo_app/components/FocusScreen.xml @@ -0,0 +1,13 @@ + + +