Roku WebDriver
Introduction
This page is a reference for the element model, supported commands, execute methods, and key mappings of the Roku WebDriver integration.
The Roku WebDriver integration is written by TV Labs for the Roku platform. It is an Appium compatible WebDriver, so it can be driven from any Appium or WebDriver client, and all Appium Roku driver commands are available. The integration interfaces with the device over ECP — Roku's External Control Protocol (HTTP and websocket) — and the device's developer web server.
Sessions
Select a build with the tvlabs:build capability and it is sideloaded onto the device and launched when the session starts. A sideloaded channel always has the app id dev.
A session started without a build is still a valid session — the device is returned to the home screen and no channel is launched. Element commands are unavailable until a developer channel is running; see Finding elements.
Finding elements
The element tree is the running channel's SceneGraph hierarchy, read from ECP's /query/app-ui.
Roku only serves /query/app-ui for the sideloaded developer channel. A store channel answers "Not authorized" and the home screen answers "No active app", and element commands return a no such element error carrying that message. This is a Roku restriction, not a TV Labs one.
getPageSource returns that hierarchy rooted at a synthetic <AppiumAUT> element, and locators are evaluated against the same document you get back from it.
Locator strategies
| Strategy | Matches |
|---|---|
xpath | Evaluated against the page source |
id, name | The node's name attribute |
accessibility id | The node's uiElementId attribute |
class name | The node's tag — its SceneGraph node type, e.g. Label, Poster |
tag name | The node's tag |
Any other strategy returns an invalid selector error, as does an XPath expression the server can't parse.
findElement and findElements honor the session's implicit wait, re-reading the tree from the device until the timeout expires. Set it once and let it cover the channel's own render latency:
await driver.setTimeout({ implicit: 10000 });
const playButton = await driver.$('//Label[@text="Play"]');
Element references
Roku's tree is re-read from the device on every element command, so an element reference is resolved fresh each time rather than pointing at a cached node. An element that has left the tree returns a stale element reference error, and a reference that no longer identifies a single node returns unknown error asking for a more specific selector.
Interacting with elements
Clicking
Roku has no pointer. click moves focus onto the element by pressing direction keys, then presses Select.
This means clicking depends on the channel drawing a focus path to the element:
- Something on screen has to report focus for the walk to have a direction to move from. If nothing does, the click returns
element not interactable. - Neither the element nor any of its ancestors having a focused sibling returns
element not interactableas well — there is nowhere to walk from. - The walk gives up after 20 key presses, also with
element not interactable.
To move focus onto an element without activating it, use roku: selectElement.
Entering text
sendKeys looks for Roku's on-screen keyboard in the element tree, presses Select on the element to summon one if there isn't one already, types the string, and then dismisses the keyboard — pressing its OK button when the dialog has one, and Back otherwise.
const searchField = await driver.$('//TextEditBox');
await searchField.setValue('planet earth');
clear is not supported. Clear a field with the remote instead, for example by pressing Backspace with roku: pressKey once per character.
Element properties
| Command | Behavior on Roku |
|---|---|
getText | The node's text attribute |
getAttribute | The SceneGraph attribute of that name, or null when the node doesn't carry it |
getProperty | The same as getAttribute — SceneGraph nodes have fields, not a separate property model |
getRect | The node's bounds, or a zero rect when it has none |
isSelected | Whether the remote's focus is on the element |
isDisplayed, isEnabled | true for any element that resolves — the tree carries no visibility or disabled state |
getComputedRole | The node's tag |
getComputedLabel | The node's text attribute |
bounds is a node's box in its parent's coordinate space, not the screen's. A label inside a container positioned at {225, 375} reports its own origin as {0, 0}. Compare rects between siblings, not across containers.
Session-level commands
| Command | Behavior on Roku |
|---|---|
getPageSource | The <AppiumAUT>-rooted element hierarchy |
getActiveElement | The innermost focused node |
takeScreenshot | Served from the device's video capture |
getWindowRect | The device's UI render resolution, which is the space bounds is expressed in |
minimizeWindow | Presses Home and returns the window rect |
getContexts | Always the single NATIVE_APP context |
Roku sessions are native-only, so the commands that need a web context are not available: arbitrary JavaScript through executeScript, navigation commands such as back, the cookie endpoints, and element screenshots all return an error. Press the remote's Back button with roku: pressKey instead.
Execute methods
The roku: execute methods expose the device's External Control Protocol (ECP). They are available on any Roku session.
roku: installApp and roku: removeApp also exist, but sideloading is managed by the platform. Select a build with the tvlabs:build capability instead.
roku: activateApp
Launches a channel.
Arguments:
appId(required, string)- The channel to launch
contentId(optional, string)- Deep-link content id
mediaType(optional, string)- Deep-link media type
Example:
await driver.execute('roku: activateApp', { appId: 'dev' });
roku: deepLink
Deep-links into content in the channel that is already running. Use roku: activateApp to deep-link into a channel that isn't.
Arguments:
contentId(required, string)- The content to open
mediaType(optional, string)- One of Roku's valid media types
- Default:
"movie"
Example:
await driver.execute('roku: deepLink', { contentId: '12345', mediaType: 'series' });
roku: pressKey
Presses a key on the remote. See the key map for the accepted names. Any other value is sent to ECP as a raw key value.
Arguments:
key(required, string)- The key to press
Example:
await driver.execute('roku: pressKey', { key: 'Home' });
roku: longPress
Holds a key down and then releases it.
Arguments:
key(required, string)- The key to hold, from the same key map as
roku: pressKey
- The key to hold, from the same key map as
durationMs(optional, number)- How long to hold it, in milliseconds
- Default:
1000
Example:
await driver.execute('roku: longPress', { key: 'Select', durationMs: 2000 });
roku: deviceInfo
Returns the device's /query/device-info fields as an object.
Arguments: None
Example:
const info = await driver.execute('roku: deviceInfo');
console.log(info['software-version']);
// 14.10.5
roku: getApps
Returns every channel installed on the device, as a list of objects with id, name, type, subtype, and version.
Arguments: None
Example:
const apps = await driver.execute('roku: getApps');
console.log(apps);
// [{ id: '12', name: 'Netflix', type: 'appl', subtype: 'ndka', version: '5.1.10' }, ...]
roku: activeApp
Returns the channel currently in the foreground, in the same shape as roku: getApps.
Unlike tvlabs: currentApp, this reports the raw ECP answer: the home screen comes back as an entry of its own rather than as an error.
Arguments: None
Example:
const app = await driver.execute('roku: activeApp');
console.log(app);
// { id: 'dev', name: 'TV Labs', type: 'appl', subtype: 'rsga', version: '1.0.0' }
roku: appUI
Returns the XML app hierarchy from /query/app-ui, as the <topscreen> element. Only available while the sideloaded channel is running.
Example:
const topscreen = await driver.execute('roku: appUI');
roku: playerState
Returns the state of the media player, from /query/media-player.
Arguments: None
Example:
const { player } = await driver.execute('roku: playerState');
console.log(player.state);
// play
roku: selectElement
Moves focus onto an element, pressing whichever remote keys get it there. Errors if focus can't be reached.
Arguments:
elementId(required, string)- The element to focus — the id returned by a previous
findElement, or an xpath locator
- The element to focus — the id returned by a previous
Example:
await driver.execute('roku: selectElement', { elementId: '//Label[@text="Play"]' });
roku: ecp
Calls an arbitrary ECP endpoint and returns the response body as text. The escape hatch for anything the methods above don't cover.
Arguments:
url(required, string)- The ECP path, starting with
/
- The ECP path, starting with
method(optional, string)- HTTP method
- Default:
"POST"
body(optional, string)- Request body
- Default:
""
Example:
const xml = await driver.execute('roku: ecp', {
url: '/query/device-info',
method: 'GET',
});
roku: ecpInput
Posts to the /input ECP endpoint. The argument object is the parameter set itself — any key/value pairs, URL-encoded for you.
Example:
await driver.execute('roku: ecpInput', {
'acceleration.x': '0.0',
'acceleration.y': '0.0',
'acceleration.z': '9.8',
});
Key map
The following key names are accepted by roku: pressKey and roku: longPress. Both spellings of each key work and matching is case-insensitive, so InstantReplay, instantreplay, and replay are the same key.
| ECP keycode | Alternate name | Description |
|---|---|---|
Home | home | Home |
Back | back | Back |
Up | up | Up |
Down | down | Down |
Left | left | Left |
Right | right | Right |
Select | select, ok | OK / Select |
Rev | rewind | Rewind |
Fwd | forward | Fast forward |
Play | play | Play / Pause |
InstantReplay | replay | Instant replay |
Info | info | Info / * |
Backspace | backspace | Backspace |
Search | search | Search |
Enter | enter | Enter |
VolumeUp | volume_up | Volume up |
VolumeDown | volume_down | Volume down |
VolumeMute | volume_mute, mute | Mute toggle |
ChannelUp | channel_up | Channel up |
ChannelDown | channel_down | Channel down |
InputTuner | input_tuner | Tuner input |
InputHDMI1 | input_hdmi1 | HDMI 1 input |
InputHDMI2 | input_hdmi2 | HDMI 2 input |
InputHDMI3 | input_hdmi3 | HDMI 3 input |
InputHDMI4 | input_hdmi4 | HDMI 4 input |
InputAV1 | input_av1 | AV 1 input |
Power | power | Power toggle |
PowerOn | power_on | Power on |
PowerOff | power_off | Power off |
FindRemote | find_remote | Find remote |
The volume, input, and channel keys only have an effect on Roku TVs. A streaming player has no tuner, inputs, or speakers of its own.
A key name that isn't in the table is sent to ECP verbatim, so any keycode Roku accepts works — including the Lit_ literal-character form:
await driver.execute('roku: pressKey', { key: 'Lit_a' });