├── .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 | [![CircleCI](https://circleci.com/gh/sgnl/nodeku/tree/master.svg?style=svg)](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 | --------------------------------------------------------------------------------