roku-test-automation
v2.0.10
Published
Helps with automating functional tests
Downloads
600
Readme
Roku Test Automation
Intro
Roku Test Automation (RTA from here on out) helps with automating functional tests for Roku devices. It has quite a bit more capabilities than Roku's first party option and does not require a Go server in the middle to convert ECP commands.
Contributing & Getting Help
There is always more things that we would like to add than we have time for. If you would like to help contribute the best spot to reach out is in the Roku Developers Slack. (We're in the #rta channel). If you are having trouble getting RTA working feel free to reach out there as well. If you see an issue or would like to see something added then feel free to create a new Github Issue
v2.0 Changes
Some incompatibility changes were made in v2.0. These include:
- Use of
AtKeyPath
was removed from all functions to shorten function call length observeField()
has been renamed toonFieldChangeOnce()
- The default base has been changed from
global
toscene
. A config valuedefaultBase
has been added to theOnDeviceComponent
config to allow switching this getFocusedNode()
now returns an object like all the ODC commands other thanhasFocus()
andisInFocusChain()
- While never documented or designed to be used externally, the
getNodeReferences()
function was removed and replaced withgetNodesInfo
callFunc()
will no longer automatically inject aninvalid
param if aparams
array was not provided.getValues()
now returns each result inside aresults
object to avoid potential variable collision- In v1.0 server was used to refer to the computer connecting to the Roku device. This is now the client and config settings related to this has been changed to reflect this
ECPKeys
was renamed toKey
and its cases were switched from upper case to pascal case andOPTIONS
is nowOption
keyPress
was renamed tokeypress
key
param for key path requests has been renamed tonodeRefKey
for easier autocomplete and more clarityRequestTypes
andBaseTypes
have been renamed toRequestType
andBaseType
NodeTree
has been renamed toTreeNode
- In an effort to provide clarity and to avoid shadowing cases, keys in key paths using a findNode selector will need to include a
#
leading character. As an example if you were trying to descend into the first child and then find a child node with an id ofposter
you will now need to have a keyPath of0.#poster
. If you wanted to access theuri
field though, that would not include the#
character since you are accessing a field and the key path would be0.#poster.uri
v2.0 also includes changing to using TCP sockets for all communication which should simplify setup communicating with Roku devices not on the same local network.
Integration
In your project go ahead and install the npm module:
npm install roku-test-automation --save-dev
Once installed, the standard way for using RTA is via the singletons like
import { ecp, odc, device, utils } from 'roku-test-automation';
if you tried to actually use these in your code you would likely get an exception thrown as no config is currently setup.
Currently the only necessary part of the config is at least one device host and password. Here's a sample config you could use as a start:
{
"$schema": "https://raw.githubusercontent.com/triwav/roku-test-automation/master/client/rta-config.schema.json",
"RokuDevice": {
"devices": [
{
"host": "",
"password": ""
}
]
},
"ECP": {
"default": {
"launchChannelId": "dev"
}
},
"OnDeviceComponent": {
"logLevel": "info"
}
}
RTA by default looks for ./rta-config.json
so saving it there is the easiest way to use it. Since device host and passwords are specific to you, you should likely also add it to your .gitignore
file. If you have settings you want stored in the repo then you can make a base config like and use the extends
key in main config to pull in the keys from the other file.
To keep a single config file and aid in running multiple tests at once, RTA reads its config from the environment variable process.env.rtaConfig
. For a basic setup you can use the helper
utils.setupEnvironmentFromConfigFile('<PATH-TO-CONFIG-FILE>');
to setup the environment for you. To avoid having to do this in each test file you can setup a global include in the mocha section of your package.json as demonstrated in /testProject/package.json
. Be sure to change the path to match where you put your include file.
If you're going to use the OnDeviceComponent
then there are a number of files that need to be copied over into your app. If you're not using the BrightScript Extension for VSCode yet then now is a great time to try it out. If you are all you need to is add:
{
"src": ["${workspaceFolder}/node_modules/roku-test-automation/dist/device/**/*"],
"dest": "/"
}
to your bsconfig.json
or launch.json
configuration files array. No need to keep the files in sync in the future when you upgrade this module.
RTA's RokuDevice also exposes a deploy()
function for including these files and also turning on a bs_const
for ENABLE_RTA
if included in your manifest.
If you did all the steps above correct you should be ready to start making use of RTA's components.
Components
ECP
RTA contains most of the standard ECP commands including:
- Launching/deeplinking into a channel
- Sending keypress and keypress sequences to the device
- Sending text to the device
- Getting the current active app
- Getting the media player
In addition, with the recent requirement for login and logout scripts, the following methods have been added:startRaspFileCreation
finishRaspFileCreation
and a copy of utils.sleep
that also includes a pause in your rasp file.
OnDeviceComponent
The core piece of RTA is the OnDeviceComponent. It functions similarly to Roku's RALE in that you have a component that is initialized on the device as used in the testProject here.
m.odc = createObject("roSGNode", "RTA_OnDeviceComponent")
Once setup you can send requests to the device to either kick off an event or check whether the expected outcome occurred or both. The following is a list of all current request types:
getValue
getValue(args: ODC.GetValueArgs, options: ODC.RequestOptions): {found: boolean, value}
At the heart of almost all requests internally is getValue
. It serves as your entry point from which you execute other requests but can also be used by itself to return a requested value. args
takes two properties:
base?: string
can be eitherglobal
,scene
,nodeRef
orfocusedNode
. If not supplied it defaults toglobal
keyPath?: string
builds off of the base and supplies the path to what you are interested in getting the value for. A simple example might be something likeAuthManager.isLoggedIn
which would let you check if a user is logged in or not. It can operate on much more than just keyed type fields though.
Array's can access index positions array.0.id
. Nodes can access their children node.0.id
as well as find nodes with a given id node.#idOfChildNodeToInspect
. The getValue
unit tests provide a full list of what is possible for a key path.
await odc.getValue({
base: 'global',
keyPath: 'AuthManager.isLoggedIn',
});
NOTE as of v2.0 keyPath
can also call a number of the Roku Brightscript interface functions on the appropriately typed objects. Currently these include:
- getParent()
- count()
- keys()
- len()
- getChildCount()
- threadinfo()
- getFieldTypes()
- subtype()
- boundingRect()
- localBoundingRect()
- sceneBoundingRect()
as an example:
await odc.getValue({
keyPath: '#rowList.boundingRect()',
});
As of v2.0 you can now access ArrayGrid children. To do this, we have added two special keywords in the keyPath: items
and title
. These don't actually exist at runtime, but RTA understands how to translate them into the proper lookup mechanisms on-device.
Here's an example showing how to access the 3rd item component in the second row:
await odc.getValue({
keyPath: '#rowList.1.items.2',
});
Notice the special keyword items
to identify we are accessing an item.
If you wanted to access the title
component for the second row, you would do:
await odc.getValue({
keyPath: '#rowList.1.title',
});
Again notice the special keyword title
to identify we are accessing a title component.
For other single level ArrayGrid types like MarkupGrid you can simply do:
await odc.getValue({
keyPath: '#markupGrid.1',
});
This would retrieve the second item component in the grid.
In addition as of v2.0 keyPath
is no longer required if just accessing the base node
getValues
getValues(args: ODC.GetValuesArgs, options: ODC.RequestOptions): {results: {[key: string]: {found: boolean; value?: any; }}, timeTaken: number}
getValues
allows you to retrieve multiple values with a single request. It takes one property for args
:
requests
:object
A list of the individualgetValue
args with a user supplied key that will be returned as the same key for the output results object.
The getValues
unit test provides an example of its usage
getNodesInfo
getNodesInfo(args: ODC.GetNodesInfoArgs, options: ODC.RequestOptions): results: {[key: string]: { subtype: string; fields: { [key: string]: { fieldType: string; type: string; value: any; } }; children: { subtype: string; }[] } }
Sometimes it may be necessary to know the type of a field on a node. This is primarily used by the vscode extension for the SceneGraph Inspector but may be useful for external use as well.
setValue
setValue(args: ODC.SetValueArgs, options: ODC.RequestOptions): {timeTaken: number}
Allows you to set a value at a key path. It takes the standard base
and keyPath
properties along with the following for args
:
value: any
The value you want to set at the suppliedkeyPath
. Setting is always done throughupdate(value, true)
so anything you can do there should be possible here as well.
await odc.setValue({
base: 'global',
keyPath: 'AuthManager.isLoggedIn',
value: false,
});
callFunc
callFunc(args: ODC.CallFuncArgs, options: ODC.RequestOptions): {value: any, timeTaken: number}
Allows you to run callFunc
on a node. It takes the standard base
and keyPath
properties along with the following for args
:
funcName: string
the name of the interface function that you want to runfuncParams?: any[]
an array of params to pass to the function.
await odc.callFunc({
base: 'global',
keyPath: 'AuthManager',
funcName: 'login',
funcParams: [{ username: 'AzureDiamond', password: 'hunter2' }],
});
getFocusedNode
getFocusedNode(args: ODC.GetFocusedNodeArgs, options: ODC.RequestOptions): {node?: NodeRepresentation, ref?: number, keyPath?: string, timeTaken: number}
Gets the currently focused node. args
includes the following:
includeRef?: boolean
returnsref
field in response that can be matched up withstoreNodeReferences
response for determining where we are in the node tree. Be sure to call storeNodeReferences first.key: string
Key that the references were stored on. If one isn't provided we use the automatically generated one
let focusedNode = await odc.getFocusedNode();
hasFocus
hasFocus(args: ODC.HasFocusArgs, options: ODC.RequestOptions): boolean
Check if the node at the supplied key path has focus or not. It takes the standard base
and keyPath
properties.
isInFocusChain
isInFocusChain(args: ODC.IsInFocusChainArgs, options: ODC.RequestOptions): boolean
Check if the node at the supplied key path is in the focus chain. It takes the standard base
and keyPath
properties.
const isBtnInFocusChain = await odc.isInFocusChain({
keyPath: '#homePage.#mainButton',
});
onFieldChangeOnce
onFieldChangeOnce(args: ODC.OnFieldChangeOnceArgs, options: ODC.RequestOptions): {observerFired: boolean, value}
Instead of having to do an arbitrary delay or polling repeatedly for a field to match an expected value, you can use onFieldChangeOnce
to setup an observer and be notified when the value changes. It takes the standard base
and keyPath
properties along with the following for args
:
match?: any | {base, keyPath, value}
Sometimes when you are observing a field you don't just want the first change. If you're looking for a specific value you can pass it for the match like:
await odc.onFieldChangeOnce({ keyPath: 'AuthManager.isLoggedIn', match: true });
In this case, base
and keyPath
for match are the same as those for the base level args. It's even more powerful than that though. You can also supply an object where the value your matching against actually comes from a totally different node than the one being observed.
One note, to simplify writing tests, if match
is supplied and the value already matches it will not setup an observer but will just return right away. Without this you'd have to write something like:
const onFieldChangeOncePromise = odc.onFieldChangeOnce(...);
await odc.setValue(...);
const result = await onFieldChangeOncePromise;
to avoid a race condition that the value already changed by the time you setup your observer. Instead you can write your test like:
await odc.setValue(...);
const result = await odc.onFieldChangeOnce(...);
to help distinguish if the observer actually fired the property observerFired
is returned in the response object
getNodesWithProperties
getNodesWithProperties(args: ODC.GetNodesWithPropertiesArgs, options: ODC.RequestOptions): {nodes: ODC.NodeRepresentation[], nodeRefs, number[]}
If you are trying to find a node but are unsure of where it is in the tree you can use getNodesWithProperties
. As an example, let's say you wanted to find all nodes with the a text field with the value of Play Movie
you could do:
const result = await odc.getNodesWithProperties({
properties: [{
value: 'Play Movie',
field: 'text'
}]
});
You'll notice that properties
is an array. If more than object is provided then each check will be done one after the other. Only nodes that match all properties will be returned.
By default an equal to check is performed. Let's take the previous case and say we wanted to match anything that contains Play
in its text field we could do:
const result = await odc.getNodesWithProperties({
properties: [{
value: 'Play',
operator: 'in',
field: 'text'
}]
});
There are number of comparison operators that can be used: '=' | '!=' | '>' | '>=' | '<' | '<=' | 'in' | '!in' | 'equal' | 'notEqual' | 'greaterThan' | 'greaterThanEqualTo' | 'lessThan' | 'lessThanEqualTo'
NOTE Not all comparison types can be used on all types. For example >=
can only be used on number types.
getAllCount
getAllCount(args: ODC.GetAllCountArgs, options: ODC.RequestOptions): {totalNodes: number, nodeCountByType: {[key: string]: number}, timeTaken: number}
Returns both the total number of nodes as returned by getAll()
on the field totalNodes
as well as the total count of each node subtype on the field nodeCountByType
.
getRootsCount
getRootsCount(args: ODC.GetRootsCountArgs, options: ODC.RequestOptions): {totalNodes: number, nodeCountByType: {[key: string]: number}, timeTaken: number}
Returns both the total number of nodes as returned by getRoots()
on the field totalNodes
as well as the total count of each node subtype on the field nodeCountByType
.
storeNodeReferences
storeNodeReferences(args: ODC.StoreNodeReferencesArgs, options: ODC.RequestOptions): StoreNodeReferencesResponse
Creates a list of nodes in the currently running application by traversing the node tree. The returned node indexes can then be used as the base for other functions such as getValue
deleteNodeReferences
deleteNodeReferences(args: ODC.DeleteNodeReferencesArgs, options: ODC.RequestOptions): {timeTaken: number}
Deletes the list of nodes previously stored by storeNodeReferences on the specified key
readRegistry
readRegistry(args: ODC.ReadRegistryArgs, options: ODC.RequestOptions): {values: { [section: string]: {[sectionItemKey: string]: string}}}
Allows for reading from the registry. If no specific sections are requested then it will return the entire contents of the registry.
writeRegistry
writeRegistry(args: ODC.WriteRegistryArgs, options: ODC.RequestOptions)
Allows for writing to the registry. If null
is passed for a sectionItemKey that key will be deleted. If null
is passed for a section that entire section will be deleted.
deleteRegistrySections
deleteRegistrySections(args: ODC.DeleteRegistrySectionsArgs, options: ODC.RequestOptions)
Allows for deleting sections from the registry. Similar functionality can be achieved with writeRegistry
passing null sections but helps to make it clearer if a mixed model isn't needed.
deleteEntireRegistry
deleteEntireRegistry(args: ODC.DeleteRegistrySectionsArgs, options: ODC.RequestOptions)
Provides a way to clear out all sections in registry. Uses deleteRegistrySections
under the hood but makes it clearer what is being done.
removeNodeChildren
removeNodeChildren(args: ODC.RemoveNodeChildrenArgs, options: ODC.RequestOptions): {timeTaken: number}
Allows removing children of the node at the specified key path
getApplicationStartTime
getApplicationStartTime(args: ODC.GetApplicationStartTimeArgs, options: ODC.RequestOptions): {startTime: number, timeTaken: number}
Gives access to Roku's roAppManager.getUptime() to allow a highly accurate time since application load that can be useful in performance tests.
disableScreenSaver
disableScreenSaver(args: ODC.DisableScreensaverArgs, options: ODC.RequestOptions): {timeTaken: number}
Allows for disabling the screen saver in the application. While the screen saver is running communication between the on device component and server is not possible. This can help avoid these issues.
RokuDevice
Serves as the middle man for ECP requests and provides access to some of the capabilities provided by the Roku's built in web server. Currently creates and retrieves a screenshot as well as provides a helper for deploying.
NetworkProxy
This class serves as a wrapper around the http-network-proxy npm module. It is still in active development and will change some as testing shows necessary changes. At it's core the idea is to be able to take a Charles config file and use those same rules in your tests. The following methods are exposed:
start(configFilePath: string = 'charlesRewrite.xml')
- sets up the proxy, loads the provided config in and writes to the device's registry to get it ready to proxy requests.stop()
- Used to shutdown the proxy port when you no longer want to proxy. Also sends an ODC request to the devicereloadConfig(configFilePath: string = 'charlesRewrite.xml')
- Gives the ability to reload the config without having to stop and restart the entire proxyaddBreakPointListener(onProxyRequestCallback: OnProxyRequestCallback)
- Allows you add a callback that will be called for every breakpoint Charles would have run intoobserveRequest(url: string, onProxyResponseCallback: OnProxyResponseCallback)
- Provides a simplified way of receiving a callback when a url is accessed without needing to create a Charles config file for that case.
utils
Contains a number of helpers that are mostly used internally but may also be of externally. Be sure to checkout the file for a full list. Below are few of the most useful ones for external use:
setupEnvironmentFromConfigFile
setupEnvironmentFromConfigFile(configFilePath: string = 'rta-config.json', deviceSelector: {} | number = 0)
As mentioned in the integration section, the config needs to be setup in the environment before using some of the components. This takes a path to your config file as its first param and an optional deviceSelector
param for its second. At its simplest you can give it an array index of the device you want to use. You can also pass an object as well though. If an object is supplied it will go through each key/value supplied and check them against the properties
object to see if properties match. An example usage of this might that you want to segment your devices as isLowEndDevice
true|false to allow you to run certain tests on only certain devices. It's user defined so feel free to use it however you'd like.
import { utils } from 'roku-test-automation';
// ...
utils.setupEnvironmentFromConfigFile('rta-config.json', { isLowEndDevice: true });
getMatchingDevices
getMatchingDevices(config: ConfigOptions, deviceSelector: {}): {[key: string]: DeviceConfigOptions}
If you're wanting to run multiple tests at the same time then this helper is useful for getting a list of all devices that match your device characteristics so you split it among multiple runners
addRandomPostfix
addRandomPostfix(message: string, length: number = 2)): string
A lot of times with tests it's useful to to append something to it to make sure a string is unique for each run/test.
sleep
sleep(milliseconds: number)
While doing arbitrary waiting is almost never needed thanks to onFieldChangeOnce
, there might be some use cases for this.
import { utils } from 'roku-test-automation';
// ...
await utils.sleep(2000);