├── .gitignore
├── test
├── helpers
│ ├── assets
│ │ ├── netflix.jpeg
│ │ ├── active-app.xml
│ │ ├── info.xml
│ │ └── apps.xml
│ ├── utils.js
│ ├── superagent-error-config.js
│ ├── xml-fixtures.js
│ ├── ssdp-mock.js
│ └── superagent-mock-config.js
├── discovery.test.js
└── nodeku.test.js
├── index.js
├── circle.yml
├── LICENSE
├── lib
├── keys.js
├── discovery.js
└── device.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.log
3 |
4 | .nyc_output
5 | coverage
6 |
7 | .DS_Store
--------------------------------------------------------------------------------
/test/helpers/assets/netflix.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgnl/nodeku/HEAD/test/helpers/assets/netflix.jpeg
--------------------------------------------------------------------------------
/test/helpers/utils.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | module.exports = {
5 | log: log => {
6 | process.stdout.write('superagent call', log);
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/helpers/assets/active-app.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hulu
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const Discovery = require('./lib/discovery');
5 | const Device = require('./lib/device');
6 | const Keys = require('./lib/keys');
7 |
8 | Discovery.Discovery = Discovery;
9 | Discovery.Device = Device;
10 | Discovery.Keys = Keys;
11 |
12 | module.exports = Discovery;
13 |
--------------------------------------------------------------------------------
/test/helpers/superagent-error-config.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const Fixtures = require('./xml-fixtures');
5 |
6 | module.exports = [
7 | {
8 | pattern: '192.168.1.17:8060(.*)',
9 | fixtures: (/* match, params, headers */) => {
10 | throw new Error(404, Fixtures);
11 | }
12 | }
13 | ];
14 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 7.0.0
4 |
5 | test:
6 | post:
7 | - npm run report
8 |
9 | deployment:
10 | npm:
11 | branch: master
12 | commands:
13 | # login using environment variables
14 | - echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login
15 | - npm run publish-npm
--------------------------------------------------------------------------------
/test/helpers/xml-fixtures.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const Fs = require('fs');
5 | const Path = require('path');
6 |
7 | const XmlFiles = [
8 | { name: 'AppsXML', location: './assets/apps.xml' },
9 | { name: 'ActiveAppXML', location: './assets/active-app.xml' },
10 | { name: 'InfoXML', location: './assets/info.xml' },
11 | { name: 'NetflixIcon', location: './assets/netflix.jpeg' }
12 | ];
13 |
14 | module.exports = XmlFiles.reduce((module, file) => {
15 | module[file.name] = Fs.readFileSync(Path.join(__dirname, file.location));
16 | return module;
17 | }, {});
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 Ray Farias
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/test/discovery.test.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | /* eslint
5 | import/order: "off",
6 | */
7 |
8 | const test = require('ava');
9 | const proxyquire = require('proxyquire');
10 | const ssdpMock = require('./helpers/ssdp-mock');
11 |
12 | const discovery = proxyquire('../lib/discovery', {
13 | 'node-ssdp': ssdpMock
14 | });
15 |
16 | test.serial('discovery exists and returns a Promise', t => {
17 | t.true(discovery instanceof Function, 'is a Function');
18 | });
19 |
20 | test.serial('discovery returns device module when a Roku device is found', async t => {
21 | const device = await discovery();
22 |
23 | t.is(typeof device, 'object', 'is a module');
24 | });
25 |
--------------------------------------------------------------------------------
/test/helpers/ssdp-mock.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | // Stubs node-ssdp client interface
5 | class Client {
6 | constructor(override) {
7 | this.override = Boolean(override);
8 | }
9 |
10 | // Mock .search()
11 | // @param {String} serviceType 'ssdp:all'
12 | // @return empty
13 | search() {
14 | console.info('mock search method called');
15 | return 0;
16 | }
17 |
18 | // Mock .on() event listener method
19 | // @param {String} eventName 'response'
20 | // @param {Function} callback pass data back to callee
21 | // @return {[type]} mock data response
22 | on(eventName, callback) {
23 | if (this.override) {
24 | return;
25 | }
26 |
27 | callback({
28 | 'CACHE-CONTROL': 'max-age=3600',
29 | ST: 'urn:dial-multiscreen-org:service:dial:1',
30 | USN: 'uuid:00000000-0000-0000-0000-000000000000::urn:dial-multiscreen-org:service:dial:1',
31 | EXT: '',
32 | SERVER: 'Roku UPnP/1.0 MiniUPnPd/1.4',
33 | LOCATION: 'http://192.168.1.17:8060/dial/dd.xml'
34 | });
35 | }
36 | }
37 |
38 | module.exports = {
39 | Client
40 | };
41 |
--------------------------------------------------------------------------------
/lib/keys.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | /**
5 | * Create a mapping of keys to make them easier to remember.
6 | * @see https://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide#ExternalControlGuide-KeypressKeyValues
7 | */
8 |
9 | module.exports = {
10 | // Standard Keys
11 | HOME: 'Home',
12 | REV: 'Rev',
13 | REVERSE: 'Rev',
14 | FWD: 'Fwd',
15 | FORWARD: 'Fwd',
16 | PLAY: 'Play',
17 | SELECT: 'Select',
18 | LEFT: 'Left',
19 | RIGHT: 'Right',
20 | DOWN: 'Down',
21 | UP: 'Up',
22 | BACK: 'Back',
23 | INSTANT_REPLAY: 'InstantReplay',
24 | INFO: 'Info',
25 | BACKSPACE: 'Backspace',
26 | SEARCH: 'Search',
27 | ENTER: 'Enter',
28 |
29 | // For devices that support "Find Remote"
30 | FIND_REMOTE: 'FindRemote',
31 |
32 | // For Roku TV
33 | VOLUME_DOWN: 'VolumeDown',
34 | VOLUME_UP: 'VolumeUp',
35 | VOLUME_MUTE: 'VolumeMute',
36 |
37 | // For Roku TV while on TV tuner channel
38 | CHANNEL_UP: 'ChannelUp',
39 | CHANNEL_DOWN: 'ChannelDown',
40 |
41 | // For Roku TV current input
42 | INPUT_TUNER: 'InputTuner',
43 | INPUT_HDMI1: 'InputHDMI1',
44 | INPUT_HDMI2: 'InputHDMI2',
45 | INPUT_HDMI3: 'InputHDMI3',
46 | INPUT_HDMI4: 'InputHDMI4',
47 | INPUT_AV1: 'InputAV1',
48 |
49 | // For devices that support being turned on/off
50 | POWER: 'Power'
51 | };
52 |
53 |
--------------------------------------------------------------------------------
/lib/discovery.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const nodeSSDPClient = require('node-ssdp').Client;
5 | const device = require('./device');
6 |
7 | module.exports = function (timeout) {
8 | const self = this || {};
9 |
10 | timeout = timeout || self._timeout || 10000;
11 |
12 | return new Promise((resolve, reject) => {
13 | const client = new nodeSSDPClient();
14 |
15 | // Open the flood gates
16 | const intervalId = setInterval(() => {
17 | client.search('roku:ecp');
18 | }, 1000);
19 |
20 | // Discovery timeout for roku device; default 10000ms
21 | const timeoutId = setTimeout(() => {
22 | clearInterval(intervalId);
23 | clearTimeout(timeoutId);
24 |
25 | return reject(new Error(`Could not find any Roku devices. Time spent: ${timeout / 1000} seconds`));
26 | }, timeout);
27 |
28 | client.on('response', headers => {
29 | if (self.debug) {
30 | return;
31 | }
32 |
33 | // Roku devices operate on PORT 8060
34 | const ipAddress = /(\d+.*:8060)(?=\/)/.exec(headers.LOCATION);
35 |
36 | if ('SERVER' in headers && Boolean(~headers.SERVER.search(/Roku/)) && ipAddress) {
37 | clearInterval(intervalId);
38 | clearTimeout(timeoutId);
39 |
40 | return resolve(device(ipAddress[0]));
41 | }
42 | });
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/test/helpers/assets/info.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 04546142-6001-1014-80ff-ac3a7a80ae09
4 | 4E652H070911
5 | 4E652H070911
6 | ec7f71ee-35d7-5a2b-bafd-e4b9f3947337
7 | Roku
8 | Roku 3
9 | 4230X
10 | US
11 | ac:3a:7a:80:ae:09
12 | ac:3a:7a:80:ae:08
13 | wifi
14 |
15 | 7.2.0
16 | 4100
17 | true
18 | en
19 | US
20 | en_US
21 | US/Hawaii
22 | -600
23 | PowerOn
24 | false
25 | false
26 |
27 | true
28 | true
29 | true
30 | true
31 | false
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodeku",
3 | "version": "1.0.20",
4 | "description": "",
5 | "engines": {
6 | "node": ">=7.0.0"
7 | },
8 | "main": "index.js",
9 | "scripts": {
10 | "lint": "xo",
11 | "test": "npm run lint && nyc ava",
12 | "report": "nyc report --reporter=html --report-dir $CIRCLE_ARTIFACTS",
13 | "publish-npm": "publish"
14 | },
15 | "keywords": [
16 | "roku",
17 | "ssdp"
18 | ],
19 | "bugs": {
20 | "url": "https://github.com/sgnl/nodeku/issues"
21 | },
22 | "homepage": "https://github.com/sgnl/nodeku",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/sgnl/nodeku"
26 | },
27 | "author": "Ray Farias (http://github.com/sgnl)",
28 | "license": "Apache-2.0",
29 | "devDependencies": {
30 | "ava": "^0.19.1",
31 | "nyc": "^11.0.2",
32 | "proxyquire": "^1.8.0",
33 | "publish": "^0.6.0",
34 | "superagent-mock": "^3.4.0",
35 | "xo": "^0.18.2"
36 | },
37 | "dependencies": {
38 | "bluebird": "^3.4.7",
39 | "got": "^7.0.0",
40 | "node-ssdp": "^3.2.1",
41 | "npm": "^6.0.0",
42 | "superagent": "^3.5.2",
43 | "xml2js": "^0.4.17"
44 | },
45 | "ava": {
46 | "files": [
47 | "test/*.test.js"
48 | ],
49 | "concurrency": 10
50 | },
51 | "nyc": {
52 | "include": [
53 | "lib/discovery.js",
54 | "lib/device.js"
55 | ]
56 | },
57 | "xo": {
58 | "space": true,
59 | "rules": {
60 | "new-cap": "off",
61 | "no-prototype-builtins": "off",
62 | "object-curly-spacing": [
63 | "error",
64 | "always"
65 | ]
66 | },
67 | "ignores": [],
68 | "overrides": []
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/test/helpers/assets/apps.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Netflix
4 | Amazon Video
5 | Crackle
6 | Hulu
7 | Showtime
8 | HBO NOW
9 | VUDU
10 | Roku Newscaster
11 | Angry Birds
12 | Plex
13 | Spotify
14 | VEVO
15 | Time Warner Cable
16 | Roku Media Player
17 | YouTube
18 | Play Movies
19 | Roku Recommends
20 | Twitch
21 | NewsOn
22 | Smithsonian Channel
23 | NASA TV
24 | Jupiter Broadcasting
25 | Security Now
26 | Air Mozilla
27 | Pugs
28 | Seeso
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nodeku
2 | [](https://circleci.com/gh/sgnl/nodeku/tree/master)
3 |
4 | Discover Roku devices via `ssdp` and control the device with methods that perform `http` requests to the device.
5 |
6 | **requirements:**
7 | - node `7.0.0 or higher`
8 | - connected to the same network as the Roku device.
9 | - a router/network that supports UPnP (for ssdp)
10 |
11 | ## usage
12 |
13 | ```javascript
14 |
15 | const Nodeku = require('nodeku')
16 |
17 | Nodeku()
18 | .then(device => {
19 | console.log(`device found at: ${ device.ip() }`)
20 | // 'xxx.xxx.xxx.xxx:8060'
21 | return device.apps()
22 | })
23 | .then(apps => {
24 | apps.forEach(app => console.log(app))
25 | // [{ id, name, type, version }, ...]
26 | })
27 | .catch(err => {
28 | console.error(err.stack)
29 | })
30 |
31 | ```
32 | ## getting started
33 | `$ npm install nodeku`
34 |
35 | ## nodeku
36 | Invoking `Nodeku` will return a promise and on success it will pass a device module. This module will contain the methods needed to control a roku device. Commands are sent to the Roku device via `HTTP` protocol as found on the [docs][1].
37 |
38 | ## api methods
39 | | **method name** | **params** | **return type** | **details** |
40 | |---|---|---|---|
41 | | `.ip()` | None | `String` | network ip and port `xxx.xxx.xxx.xxx:8060` |
42 | | `.apps()` | None | `List[{}, ...]` | list of many objects with props: `id, name, type, version` |
43 | | `.active()` | None | `List[{}]` | list with one object with props `id, name, type, version` |
44 | | `.info()` | None | `Map{}` | map with *too many(29) props* |
45 | | `.keypress('...')` | String | `Boolean` | true if success, false if error |
46 | | `.keydown('...')`| String | `Boolean` | true if successful, false if error |
47 | | `.keyup('...')` | String | `Boolean` | true if successful, false if error |
48 | | `'.icon(1)` | Number | `Buffer` | jpeg image as buffer |
49 | | `'.launch(1)` | Number | `Boolean` | true if successful, false if error |
50 |
51 | ### keypress values
52 | - `Home`
53 | - `Rev`
54 | - `Fwd`
55 | - `Play`
56 | - `Select`
57 | - `Left`
58 | - `Right`
59 | - `Down`
60 | - `Up`
61 | - `Back`
62 | - `InstantReplay`
63 | - `Info`
64 | - `Backspace`
65 | - `Search`
66 | - `Enter`
67 |
68 | ## tests
69 | `$ npm test`
70 |
71 |
72 | ## references
73 | [Roku - External Control Service Commands][1]
74 | [Roku - Keypress Key Values][3]
75 |
76 | ### additional information
77 | Only tested on OSX and with Roku3 device. halp?
78 |
79 |
80 | [1]: https://sdkdocs.roku.com/display/sdkdoc/External+Control+API
81 | [2]: http://facebook.github.io/immutable-js/
82 | [3]: https://sdkdocs.roku.com/display/sdkdoc/External+Control+API#ExternalControlAPI-KeypressKeyValues
83 |
--------------------------------------------------------------------------------
/test/helpers/superagent-mock-config.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const Fixtures = require('./xml-fixtures');
5 |
6 | module.exports = [
7 | {
8 | pattern: '192.168.1.17:8060/query/apps',
9 | fixtures: () => {
10 | return true;
11 | },
12 | get: () => {
13 | return { body: Fixtures.AppsXML };
14 | }
15 | },
16 | {
17 | pattern: '192.168.1.17:8060/query/active-app',
18 | fixtures: () => {
19 | return true;
20 | },
21 | get: () => {
22 | return { body: Fixtures.ActiveAppXML };
23 | }
24 | },
25 | {
26 | pattern: '192.168.1.17:8060/query/device-info',
27 | fixtures: () => {
28 | return true;
29 | },
30 | get: () => {
31 | return { body: Fixtures.InfoXML };
32 | }
33 | },
34 | {
35 | pattern: '192.168.1.17:8060/keypress/(.*)',
36 | fixtures: match => {
37 | const validKeys = ['Info', 'Home'];
38 |
39 | if (validKeys.indexOf(match[1]) === -1) {
40 | const newErr = new Error(404);
41 | newErr.response = 'invalid key identifier';
42 | newErr.status = 404;
43 | throw newErr;
44 | }
45 | return true;
46 | },
47 | post: () => {
48 | return { status: 200 };
49 | }
50 | },
51 | {
52 | pattern: '192.168.1.17:8060/keydown/(.*)',
53 | fixtures: match => {
54 | const validKeys = ['Info', 'Home'];
55 |
56 | if (validKeys.indexOf(match[1]) === -1) {
57 | const newErr = new Error(404);
58 | newErr.response = 'invalid key identifier';
59 | newErr.status = 404;
60 | throw newErr;
61 | }
62 | return true;
63 | },
64 | post: () => {
65 | return { status: 200 };
66 | }
67 | },
68 | {
69 | pattern: '192.168.1.17:8060/keyup/(.*)',
70 | fixtures: match => {
71 | const validKeys = ['Info', 'Home'];
72 |
73 | if (validKeys.indexOf(match[1]) === -1) {
74 | const newErr = new Error(404);
75 | newErr.response = 'invalid key identifier';
76 | newErr.status = 404;
77 | throw newErr;
78 | }
79 | return true;
80 | },
81 | post: () => {
82 | return { status: 200 };
83 | }
84 | },
85 | {
86 | pattern: '192.168.1.17:8060/query/icon/(.*)',
87 | fixtures: match => {
88 | const validKeys = ['12'];
89 |
90 | if (validKeys.indexOf(match[1]) === -1) {
91 | const newErr = new Error(404);
92 | newErr.response = 'invalid key identifier';
93 | newErr.status = 404;
94 | throw newErr;
95 | }
96 | return true;
97 | },
98 | get: () => {
99 | return { body: Fixtures.NetflixIcon };
100 | }
101 | },
102 | {
103 | pattern: '192.168.1.17:8060/launch/(.*)',
104 | fixtures: () => {},
105 | post: () => {
106 | return { status: 200 };
107 | }
108 | }
109 | ];
110 |
--------------------------------------------------------------------------------
/lib/device.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const bluebird = require('bluebird');
5 | const xml2Json = bluebird.promisifyAll(require('xml2js')).parseStringAsync;
6 | const req = require('got');
7 |
8 | module.exports = function (deviceIp) {
9 | const endpoints = {
10 | path: deviceIp,
11 | apps: `${deviceIp}/query/apps`,
12 | activeApp: `${deviceIp}/query/active-app`,
13 | info: `${deviceIp}/query/device-info`,
14 | keypress: `${deviceIp}/keypress`,
15 | keydown: `${deviceIp}/keydown`,
16 | keyup: `${deviceIp}/keyup`,
17 | icon: `${deviceIp}/query/icon`,
18 | launch: `${deviceIp}/launch`
19 | };
20 |
21 | endpoints.get = key => endpoints[key];
22 | Object.freeze(endpoints);
23 |
24 | const ip = () => endpoints.get('path');
25 |
26 | const apps = () => req(endpoints.get('apps'))
27 | .then(res => res.body.toString())
28 | .then(xml2Json)
29 | .then(({ apps }) => {
30 | // Tidy up json data
31 | return apps.app.map(a => {
32 | return {
33 | id: a.$.id,
34 | name: a._,
35 | type: a.$.type,
36 | version: a.$.version
37 | };
38 | });
39 | });
40 |
41 | const active = () => req(endpoints.get('activeApp'))
42 | .then(res => res.body.toString())
43 | .then(xml2Json)
44 | .then(json => {
45 | // Tidy up json data
46 | return json['active-app'].app.map(app => {
47 | // If no app is currently active, a single field is returned without
48 | // any properties
49 | if (!app.$ || !app.$.id) {
50 | return null;
51 | }
52 | return {
53 | id: app.$.id,
54 | name: app._,
55 | type: app.$.type,
56 | version: app.$.version
57 | };
58 | }).filter(app => app !== null);
59 | });
60 |
61 | const activeApp = () => {
62 | return active().then(active => {
63 | return active.length === 0 ? null : active[0];
64 | });
65 | };
66 |
67 | const info = () => req(endpoints.get('info'))
68 | .then(res => res.body.toString())
69 | .then(xml2Json)
70 | .then(json => {
71 | const flatJson = {};
72 | json = json['device-info'];
73 |
74 | Object.entries(json).forEach(([key, value]) => {
75 | flatJson[key] = value[0];
76 | });
77 |
78 | return flatJson;
79 | });
80 |
81 | const icon = appId => req.get(`${endpoints.get('icon')}/${appId}`)
82 | .then(res => res.body);
83 |
84 | const launch = appId => req.post(`${endpoints.get('launch')}/${appId}`)
85 | .then(res => res.status === 200);
86 |
87 | const keyhelper = endpoint => key => req.post(`${endpoint}/${key}`)
88 | .then(res => res.status === 200);
89 |
90 | return {
91 | ip,
92 | apps,
93 | active,
94 | activeApp,
95 | info,
96 | icon,
97 | launch,
98 | keypress: keyhelper(endpoints.get('keypress')),
99 | keydown: keyhelper(endpoints.get('keydown')),
100 | keyup: keyhelper(endpoints.get('keyup'))
101 | };
102 | };
103 |
--------------------------------------------------------------------------------
/test/nodeku.test.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const assert = require('assert');
5 | const test = require('ava');
6 | const req = require('superagent');
7 | const proxyquire = require('proxyquire');
8 | const utils = require('./helpers/utils');
9 |
10 | /* Mocks and fixtures */
11 | const ssdpMock = require('./helpers/ssdp-mock');
12 | const ReqMockConfig = require('./helpers/superagent-mock-config');
13 |
14 | require('superagent-mock')(req, ReqMockConfig, utils.logger);
15 |
16 | const device = proxyquire('../lib/device', {
17 | got: req
18 | });
19 | const Nodeku = proxyquire('../lib/discovery', {
20 | 'node-ssdp': ssdpMock,
21 | './device': device
22 | });
23 |
24 | function isDeepEqual(apps, legend) {
25 | return apps.every(app => {
26 | return !assert.deepEqual(Object.keys(app), legend);
27 | });
28 | }
29 |
30 | function wrapper(description, func) {
31 | test(description, async t => {
32 | const device = await Nodeku();
33 | t.truthy(device);
34 | func(t, device);
35 | });
36 | }
37 |
38 | test('Nodeku', t => {
39 | t.is(typeof Nodeku, 'function', 'is ready');
40 | });
41 |
42 | wrapper('-method: .ip()', (t, device) => {
43 | t.true(device.hasOwnProperty('ip'), 'exists');
44 | t.is(typeof device.ip, 'function', 'is a function');
45 | t.deepEqual(device.ip(), '192.168.1.17:8060', 'ip address retreived');
46 | });
47 |
48 | wrapper('-method: .apps()', (t, device) => {
49 | t.true(device.hasOwnProperty('apps'), 'exists');
50 | t.is(typeof device.apps, 'function', 'is a function');
51 |
52 | return device
53 | .apps()
54 | .then(apps => {
55 | t.true(Array.isArray(apps), 'returns a list');
56 |
57 | const containsOnlyObjects = apps.every(app => app === Object(app));
58 | t.true(containsOnlyObjects, 'list contains maps');
59 |
60 | const objectsHaveCorrectProps = isDeepEqual(apps, ['id', 'name', 'type', 'version']);
61 | t.true(objectsHaveCorrectProps, 'maps has correct props');
62 | });
63 | });
64 |
65 | wrapper('-method: .active()', (t, device) => {
66 | return device
67 | .active()
68 | .then(app => {
69 | t.true(Array.isArray(app), 'returns list');
70 |
71 | const objectsHaveCorrectProps = isDeepEqual(app, ['id', 'name', 'type', 'version']);
72 | t.true(objectsHaveCorrectProps, 'maps has correct props');
73 | });
74 | });
75 |
76 | wrapper('-method: .activeApp()', (t, device) => {
77 | return device
78 | .activeApp()
79 | .then(app => {
80 | t.true(Array.isArray(app), 'returns list');
81 |
82 | const objectHasCorrectProps = !assert.deepEqual(
83 | Object.keys(app), ['id', 'name', 'type', 'version']);
84 | t.true(objectHasCorrectProps, 'map has correct props');
85 | });
86 | });
87 |
88 | wrapper('-method: .info()', (t, device) => {
89 | return device
90 | .info()
91 | .then(info => {
92 | t.true(info === Object(info), 'returns a map');
93 | t.is(Object.keys(info).length, 29, 'has 29 props');
94 | });
95 | });
96 |
97 | wrapper('-method: .keypress(\'Home\')', (t, device) => {
98 | return device
99 | .keypress('Home')
100 | .then(res => t.truthy(res));
101 | });
102 |
103 | wrapper('-method: .icon()', (t, device) => {
104 | return device
105 | .icon('12')
106 | .then(img => {
107 | t.true(img instanceof Buffer);
108 | });
109 | });
110 |
111 | wrapper('-method: .launch()', (t, device) => {
112 | return device
113 | .apps()
114 | .then(apps => {
115 | const randomIndex = Math.floor(Math.random() * apps.size);
116 | const appToLaunch = apps.splice(randomIndex, 1)[0];
117 | return device.launch(appToLaunch.id);
118 | })
119 | .then(t.done);
120 | });
121 |
--------------------------------------------------------------------------------