diff --git a/docs/api/js.rst b/docs/api/js.rst index 372e7f4e..361c24fd 100644 --- a/docs/api/js.rst +++ b/docs/api/js.rst @@ -66,9 +66,10 @@ After npm completes, you can import Mopidy.js using ``require()``: Getting the library for development on the library ================================================== -If you want to work on the Mopidy.js library itself, you'll find a complete -development setup in the ``js/`` dir in our repo. The instructions in -``js/README.md`` will guide you on your way. +If you want to work on the Mopidy.js library itself, you'll find the source +code and a complete development setup in the `Mopidy.js Git repo +`_. The instructions in ``README.md`` will +guide you on your way. Creating an instance diff --git a/js/Gruntfile.js b/js/Gruntfile.js deleted file mode 100644 index 81221676..00000000 --- a/js/Gruntfile.js +++ /dev/null @@ -1,101 +0,0 @@ -/*global module:false*/ -module.exports = function (grunt) { - - grunt.initConfig({ - pkg: grunt.file.readJSON("package.json"), - meta: { - banner: "/*! Mopidy.js v<%= pkg.version %> - built " + - "<%= grunt.template.today('yyyy-mm-dd') %>\n" + - " * http://www.mopidy.com/\n" + - " * Copyright (c) <%= grunt.template.today('yyyy') %> " + - "Stein Magnus Jodal and contributors\n" + - " * Licensed under the Apache License, Version 2.0 */\n", - files: { - own: ["Gruntfile.js", "src/**/*.js", "test/**/*-test.js"], - main: "src/mopidy.js", - concat: "../mopidy/http/data/mopidy.js", - minified: "../mopidy/http/data/mopidy.min.js" - } - }, - buster: { - all: {} - }, - browserify: { - test_mopidy: { - files: { - "test/lib/mopidy.js": "<%= meta.files.main %>" - }, - options: { - postBundleCB: function (err, src, next) { - next(err, grunt.template.process("<%= meta.banner %>") + src); - }, - standalone: "Mopidy" - } - }, - test_when: { - files: { - "test/lib/when.js": "node_modules/when/when.js" - }, - options: { - standalone: "when" - } - }, - dist: { - files: { - "<%= meta.files.concat %>": "<%= meta.files.main %>" - }, - options: { - postBundleCB: function (err, src, next) { - next(err, grunt.template.process("<%= meta.banner %>") + src); - }, - standalone: "Mopidy" - } - } - }, - jshint: { - options: { - curly: true, - eqeqeq: true, - immed: true, - indent: 4, - latedef: true, - newcap: true, - noarg: true, - sub: true, - quotmark: "double", - undef: true, - unused: true, - eqnull: true, - browser: true, - devel: true, - globals: {} - }, - files: "<%= meta.files.own %>" - }, - uglify: { - options: { - banner: "<%= meta.banner %>" - }, - all: { - files: { - "<%= meta.files.minified %>": ["<%= meta.files.concat %>"] - } - } - }, - watch: { - files: "<%= meta.files.own %>", - tasks: ["default"] - } - }); - - grunt.registerTask("test_build", ["browserify:test_when", "browserify:test_mopidy"]); - grunt.registerTask("test", ["jshint", "test_build", "buster"]); - grunt.registerTask("build", ["test", "browserify:dist", "uglify"]); - grunt.registerTask("default", ["build"]); - - grunt.loadNpmTasks("grunt-buster"); - grunt.loadNpmTasks("grunt-browserify"); - grunt.loadNpmTasks("grunt-contrib-jshint"); - grunt.loadNpmTasks("grunt-contrib-uglify"); - grunt.loadNpmTasks("grunt-contrib-watch"); -}; diff --git a/js/README.md b/js/README.md deleted file mode 100644 index 1b368bf5..00000000 --- a/js/README.md +++ /dev/null @@ -1,121 +0,0 @@ -Mopidy.js -========= - -Mopidy.js is a JavaScript library that is installed as a part of Mopidy's HTTP -frontend or from npm. The library makes Mopidy's core API available from the -browser or a Node.js environment, using JSON-RPC messages over a WebSocket to -communicate with Mopidy. - - -Getting it for browser use --------------------------- - -Regular and minified versions of Mopidy.js, ready for use, is installed -together with Mopidy. When the HTTP frontend is running, the files are -available at: - -- http://localhost:6680/mopidy/mopidy.js -- http://localhost:6680/mopidy/mopidy.min.js - -You may need to adjust hostname and port for your local setup. - -In the source repo, you can find the files at: - -- `mopidy/http/data/mopidy.js` -- `mopidy/http/data/mopidy.min.js` - - -Getting it for Node.js use --------------------------- - -If you want to use Mopidy.js from Node.js instead of a browser, you can install -Mopidy.js using npm: - - npm install mopidy - -After npm completes, you can import Mopidy.js using ``require()``: - - var Mopidy = require("mopidy"); - - -Using the library ------------------ - -See the [Mopidy.js documentation](http://docs.mopidy.com/en/latest/api/js/). - - -Building from source --------------------- - -1. Install [Node.js](http://nodejs.org/) and npm. If you're running Ubuntu: - - sudo apt-get install nodejs-legacy npm - -2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies: - - cd js/ - npm install - -That's it. - -You can now run the tests: - - npm test - -To run tests automatically when you save a file: - - npm start - -To run tests, concatenate, minify the source, and update the JavaScript files -in `mopidy/http/data/`: - - npm run-script build - -To run other [grunt](http://gruntjs.com/) targets which isn't predefined in -`package.json` and thus isn't available through `npm run-script`: - - PATH=./node_modules/.bin:$PATH grunt foo - - -Changelog ---------- - -### 0.4.0 (2014-06-24) - -- Add support for method calls with by-name arguments. The old calling - convention, "by-position-only", is still the default, but this will change in - the future. A warning is printed to the console if you don't explicitly - select a calling convention. See the docs for details. - -### 0.3.0 (2014-06-16) - -- Upgrade to when.js 3, which brings great performance improvements and better - debugging facilities. If you maintain a Mopidy client, you should review the - [differences between when.js 2 and 3](https://github.com/cujojs/when/blob/master/docs/api.md#upgrading-to-30-from-2x) - and the - [when.js debugging guide](https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises). - -- All promise rejection values are now of the Error type. This ensures that all - JavaScript VMs will show a useful stack trace if a rejected promise's value - is used to throw an exception. To allow catch clauses to handle different - errors differently, server side errors are of the type `Mopidy.ServerError`, - and connection related errors are of the type `Mopidy.ConnectionError`. - -### 0.2.0 (2014-01-04) - -- **Backwards incompatible change for Node.js users:** - `var Mopidy = require('mopidy').Mopidy;` must be changed to - `var Mopidy = require('mopidy');` - -- Add support for [Browserify](http://browserify.org/). - -- Upgrade dependencies. - -### 0.1.1 (2013-09-17) - -- Upgrade dependencies. - -### 0.1.0 (2013-03-31) - -- Initial release as a Node.js module to the - [npm registry](https://npmjs.org/). diff --git a/js/buster.js b/js/buster.js deleted file mode 100644 index c5dec850..00000000 --- a/js/buster.js +++ /dev/null @@ -1,15 +0,0 @@ -var config = module.exports; - -config.browser_tests = { - environment: "browser", - libs: ["test/lib/*.js"], - testHelpers: ["test/**/*-helper.js"], - tests: ["test/**/*-test.js"] -}; - -config.node_tests = { - environment: "node", - sources: ["src/**/*.js"], - testHelpers: ["test/**/*-helper.js"], - tests: ["test/**/*-test.js"] -}; diff --git a/js/lib/websocket/browser.js b/js/lib/websocket/browser.js deleted file mode 100644 index e594246c..00000000 --- a/js/lib/websocket/browser.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { Client: window.WebSocket }; diff --git a/js/lib/websocket/package.json b/js/lib/websocket/package.json deleted file mode 100644 index d1e2ac63..00000000 --- a/js/lib/websocket/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "browser": "browser.js", - "main": "server.js" -} diff --git a/js/lib/websocket/server.js b/js/lib/websocket/server.js deleted file mode 100644 index dd24f4be..00000000 --- a/js/lib/websocket/server.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('faye-websocket'); diff --git a/js/package.json b/js/package.json deleted file mode 100644 index b2b63f84..00000000 --- a/js/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "mopidy", - "version": "0.4.0", - "description": "Client lib for controlling a Mopidy music server over a WebSocket", - "keywords": [ - "mopidy", - "music", - "client", - "websocket", - "json-rpc" - ], - "homepage": "http://www.mopidy.com/", - "bugs": "https://github.com/mopidy/mopidy/issues", - "license": "Apache-2.0", - "author": { - "name": "Stein Magnus Jodal", - "email": "stein.magnus@jodal.no", - "url": "http://www.jodal.no" - }, - "contributors": [ - { - "name": "Stein Magnus Jodal", - "email": "stein.magnus@jodal.no", - "url": "http://www.jodal.no" - }, - { - "name": "Paul Connolley", - "email": "paul.connolley@gmail.com" - } - ], - "main": "src/mopidy.js", - "repository": { - "type": "git", - "url": "git://github.com/mopidy/mopidy.git" - }, - "scripts": { - "test": "grunt test", - "build": "grunt build", - "start": "grunt watch" - }, - "dependencies": { - "bane": "~1.1.0", - "faye-websocket": "~0.7.2", - "when": "~3.2.3" - }, - "devDependencies": { - "buster": "~0.7.13", - "browserify": "~3", - "grunt": "~0.4.5", - "grunt-buster": "~0.3.1", - "grunt-browserify": "~1.3.2", - "grunt-contrib-jshint": "~0.10.0", - "grunt-contrib-uglify": "~0.5.0", - "grunt-contrib-watch": "~0.6.1", - "phantomjs": "~1.9.7-8" - }, - "engines": { - "node": "*" - } -} diff --git a/js/src/mopidy.js b/js/src/mopidy.js deleted file mode 100644 index 7e019dd4..00000000 --- a/js/src/mopidy.js +++ /dev/null @@ -1,331 +0,0 @@ -/*global module:true, require:false*/ - -var bane = require("bane"); -var websocket = require("../lib/websocket/"); -var when = require("when"); - -function Mopidy(settings) { - if (!(this instanceof Mopidy)) { - return new Mopidy(settings); - } - - this._console = this._getConsole(settings || {}); - this._settings = this._configure(settings || {}); - - this._backoffDelay = this._settings.backoffDelayMin; - this._pendingRequests = {}; - this._webSocket = null; - - bane.createEventEmitter(this); - this._delegateEvents(); - - if (this._settings.autoConnect) { - this.connect(); - } -} - -Mopidy.ConnectionError = function (message) { - this.name = "ConnectionError"; - this.message = message; -}; -Mopidy.ConnectionError.prototype = new Error(); -Mopidy.ConnectionError.prototype.constructor = Mopidy.ConnectionError; - -Mopidy.ServerError = function (message) { - this.name = "ServerError"; - this.message = message; -}; -Mopidy.ServerError.prototype = new Error(); -Mopidy.ServerError.prototype.constructor = Mopidy.ServerError; - -Mopidy.WebSocket = websocket.Client; - -Mopidy.prototype._getConsole = function (settings) { - if (typeof settings.console !== "undefined") { - return settings.console; - } - - var con = typeof console !== "undefined" && console || {}; - - con.log = con.log || function () {}; - con.warn = con.warn || function () {}; - con.error = con.error || function () {}; - - return con; -}; - -Mopidy.prototype._configure = function (settings) { - var currentHost = (typeof document !== "undefined" && - document.location.host) || "localhost"; - settings.webSocketUrl = settings.webSocketUrl || - "ws://" + currentHost + "/mopidy/ws"; - - if (settings.autoConnect !== false) { - settings.autoConnect = true; - } - - settings.backoffDelayMin = settings.backoffDelayMin || 1000; - settings.backoffDelayMax = settings.backoffDelayMax || 64000; - - if (typeof settings.callingConvention === "undefined") { - this._console.warn( - "Mopidy.js is using the default calling convention. The " + - "default will change in the future. You should explicitly " + - "specify which calling convention you use."); - } - settings.callingConvention = ( - settings.callingConvention || "by-position-only"); - - return settings; -}; - -Mopidy.prototype._delegateEvents = function () { - // Remove existing event handlers - this.off("websocket:close"); - this.off("websocket:error"); - this.off("websocket:incomingMessage"); - this.off("websocket:open"); - this.off("state:offline"); - - // Register basic set of event handlers - this.on("websocket:close", this._cleanup); - this.on("websocket:error", this._handleWebSocketError); - this.on("websocket:incomingMessage", this._handleMessage); - this.on("websocket:open", this._resetBackoffDelay); - this.on("websocket:open", this._getApiSpec); - this.on("state:offline", this._reconnect); -}; - -Mopidy.prototype.connect = function () { - if (this._webSocket) { - if (this._webSocket.readyState === Mopidy.WebSocket.OPEN) { - return; - } else { - this._webSocket.close(); - } - } - - this._webSocket = this._settings.webSocket || - new Mopidy.WebSocket(this._settings.webSocketUrl); - - this._webSocket.onclose = function (close) { - this.emit("websocket:close", close); - }.bind(this); - - this._webSocket.onerror = function (error) { - this.emit("websocket:error", error); - }.bind(this); - - this._webSocket.onopen = function () { - this.emit("websocket:open"); - }.bind(this); - - this._webSocket.onmessage = function (message) { - this.emit("websocket:incomingMessage", message); - }.bind(this); -}; - -Mopidy.prototype._cleanup = function (closeEvent) { - Object.keys(this._pendingRequests).forEach(function (requestId) { - var resolver = this._pendingRequests[requestId]; - delete this._pendingRequests[requestId]; - var error = new Mopidy.ConnectionError("WebSocket closed"); - error.closeEvent = closeEvent; - resolver.reject(error); - }.bind(this)); - - this.emit("state:offline"); -}; - -Mopidy.prototype._reconnect = function () { - this.emit("reconnectionPending", { - timeToAttempt: this._backoffDelay - }); - - setTimeout(function () { - this.emit("reconnecting"); - this.connect(); - }.bind(this), this._backoffDelay); - - this._backoffDelay = this._backoffDelay * 2; - if (this._backoffDelay > this._settings.backoffDelayMax) { - this._backoffDelay = this._settings.backoffDelayMax; - } -}; - -Mopidy.prototype._resetBackoffDelay = function () { - this._backoffDelay = this._settings.backoffDelayMin; -}; - -Mopidy.prototype.close = function () { - this.off("state:offline", this._reconnect); - this._webSocket.close(); -}; - -Mopidy.prototype._handleWebSocketError = function (error) { - this._console.warn("WebSocket error:", error.stack || error); -}; - -Mopidy.prototype._send = function (message) { - switch (this._webSocket.readyState) { - case Mopidy.WebSocket.CONNECTING: - return when.reject( - new Mopidy.ConnectionError("WebSocket is still connecting")); - case Mopidy.WebSocket.CLOSING: - return when.reject( - new Mopidy.ConnectionError("WebSocket is closing")); - case Mopidy.WebSocket.CLOSED: - return when.reject( - new Mopidy.ConnectionError("WebSocket is closed")); - default: - var deferred = when.defer(); - message.jsonrpc = "2.0"; - message.id = this._nextRequestId(); - this._pendingRequests[message.id] = deferred.resolver; - this._webSocket.send(JSON.stringify(message)); - this.emit("websocket:outgoingMessage", message); - return deferred.promise; - } -}; - -Mopidy.prototype._nextRequestId = (function () { - var lastUsed = -1; - return function () { - lastUsed += 1; - return lastUsed; - }; -}()); - -Mopidy.prototype._handleMessage = function (message) { - try { - var data = JSON.parse(message.data); - if (data.hasOwnProperty("id")) { - this._handleResponse(data); - } else if (data.hasOwnProperty("event")) { - this._handleEvent(data); - } else { - this._console.warn( - "Unknown message type received. Message was: " + - message.data); - } - } catch (error) { - if (error instanceof SyntaxError) { - this._console.warn( - "WebSocket message parsing failed. Message was: " + - message.data); - } else { - throw error; - } - } -}; - -Mopidy.prototype._handleResponse = function (responseMessage) { - if (!this._pendingRequests.hasOwnProperty(responseMessage.id)) { - this._console.warn( - "Unexpected response received. Message was:", responseMessage); - return; - } - - var error; - var resolver = this._pendingRequests[responseMessage.id]; - delete this._pendingRequests[responseMessage.id]; - - if (responseMessage.hasOwnProperty("result")) { - resolver.resolve(responseMessage.result); - } else if (responseMessage.hasOwnProperty("error")) { - error = new Mopidy.ServerError(responseMessage.error.message); - error.code = responseMessage.error.code; - error.data = responseMessage.error.data; - resolver.reject(error); - this._console.warn("Server returned error:", responseMessage.error); - } else { - error = new Error("Response without 'result' or 'error' received"); - error.data = {response: responseMessage}; - resolver.reject(error); - this._console.warn( - "Response without 'result' or 'error' received. Message was:", - responseMessage); - } -}; - -Mopidy.prototype._handleEvent = function (eventMessage) { - var type = eventMessage.event; - var data = eventMessage; - delete data.event; - - this.emit("event:" + this._snakeToCamel(type), data); -}; - -Mopidy.prototype._getApiSpec = function () { - return this._send({method: "core.describe"}) - .then(this._createApi.bind(this)) - .catch(this._handleWebSocketError); -}; - -Mopidy.prototype._createApi = function (methods) { - var byPositionOrByName = ( - this._settings.callingConvention === "by-position-or-by-name"); - - var caller = function (method) { - return function () { - var message = {method: method}; - if (arguments.length === 0) { - return this._send(message); - } - if (!byPositionOrByName) { - message.params = Array.prototype.slice.call(arguments); - return this._send(message); - } - if (arguments.length > 1) { - return when.reject(new Error( - "Expected zero arguments, a single array, " + - "or a single object.")); - } - if (!Array.isArray(arguments[0]) && - arguments[0] !== Object(arguments[0])) { - return when.reject(new TypeError( - "Expected an array or an object.")); - } - message.params = arguments[0]; - return this._send(message); - }.bind(this); - }.bind(this); - - var getPath = function (fullName) { - var path = fullName.split("."); - if (path.length >= 1 && path[0] === "core") { - path = path.slice(1); - } - return path; - }; - - var createObjects = function (objPath) { - var parentObj = this; - objPath.forEach(function (objName) { - objName = this._snakeToCamel(objName); - parentObj[objName] = parentObj[objName] || {}; - parentObj = parentObj[objName]; - }.bind(this)); - return parentObj; - }.bind(this); - - var createMethod = function (fullMethodName) { - var methodPath = getPath(fullMethodName); - var methodName = this._snakeToCamel(methodPath.slice(-1)[0]); - var object = createObjects(methodPath.slice(0, -1)); - object[methodName] = caller(fullMethodName); - object[methodName].description = methods[fullMethodName].description; - object[methodName].params = methods[fullMethodName].params; - }.bind(this); - - Object.keys(methods).forEach(createMethod); - this.emit("state:online"); -}; - -Mopidy.prototype._snakeToCamel = function (name) { - return name.replace(/(_[a-z])/g, function (match) { - return match.toUpperCase().replace("_", ""); - }); -}; - -module.exports = Mopidy; diff --git a/js/test/bind-helper.js b/js/test/bind-helper.js deleted file mode 100644 index a5a3e0f4..00000000 --- a/js/test/bind-helper.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * PhantomJS 1.6 does not support Function.prototype.bind, so we polyfill it. - * - * Implementation from: - * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind - */ -if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; - }; -} diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js deleted file mode 100644 index caf4ce21..00000000 --- a/js/test/mopidy-test.js +++ /dev/null @@ -1,964 +0,0 @@ -/*global require:false */ - -if (typeof module === "object" && typeof require === "function") { - var buster = require("buster"); - var Mopidy = require("../src/mopidy"); - var when = require("when"); -} - -var assert = buster.assert; -var refute = buster.refute; - -buster.testCase("Mopidy", { - setUp: function () { - // Sinon.JS doesn't manage to stub PhantomJS' WebSocket implementation, - // so we replace it with a dummy temporarily. - var fakeWebSocket = function () { - return { - send: function () {}, - close: function () {} - }; - }; - fakeWebSocket.CONNECTING = 0; - fakeWebSocket.OPEN = 1; - fakeWebSocket.CLOSING = 2; - fakeWebSocket.CLOSED = 3; - - this.realWebSocket = Mopidy.WebSocket; - Mopidy.WebSocket = fakeWebSocket; - - this.webSocketConstructorStub = this.stub(Mopidy, "WebSocket"); - - this.webSocket = { - close: this.stub(), - send: this.stub() - }; - this.mopidy = new Mopidy({ - callingConvention: "by-position-or-by-name", - webSocket: this.webSocket - }); - }, - - tearDown: function () { - Mopidy.WebSocket = this.realWebSocket; - }, - - "constructor": { - "connects when autoConnect is true": function () { - new Mopidy({ - autoConnect: true, - callingConvention: "by-position-or-by-name" - }); - - var currentHost = typeof document !== "undefined" && - document.location.host || "localhost"; - - assert.calledOnceWith(this.webSocketConstructorStub, - "ws://" + currentHost + "/mopidy/ws"); - }, - - "does not connect when autoConnect is false": function () { - new Mopidy({ - autoConnect: false, - callingConvention: "by-position-or-by-name" - }); - - refute.called(this.webSocketConstructorStub); - }, - - "does not connect when passed a WebSocket": function () { - new Mopidy({ - callingConvention: "by-position-or-by-name", - webSocket: {} - }); - - refute.called(this.webSocketConstructorStub); - }, - - "defaults to by-position-only calling convention": function () { - var console = { - warn: function () {} - }; - var mopidy = new Mopidy({ - console: console, - webSocket: this.webSocket, - }); - - assert.equals( - mopidy._settings.callingConvention, - "by-position-only"); - }, - - "warns if no calling convention explicitly selected": function () { - var console = { - warn: function () {} - }; - var stub = this.stub(console, "warn"); - - new Mopidy({console: console}); - - assert.calledOnceWith( - stub, - "Mopidy.js is using the default calling convention. The " + - "default will change in the future. You should explicitly " + - "specify which calling convention you use."); - }, - - "does not warn if calling convention chosen explicitly": function () { - var console = { - warn: function () {} - }; - var stub = this.stub(console, "warn"); - - new Mopidy({ - callingConvention: "by-position-or-by-name", - console: console - }); - - refute.called(stub); - }, - - "works without 'new' keyword": function () { - var mopidyConstructor = Mopidy; // To trick jshint into submission - - var mopidy = mopidyConstructor({ - callingConvention: "by-position-or-by-name", - webSocket: {} - }); - - assert.isObject(mopidy); - assert(mopidy instanceof Mopidy); - } - }, - - ".connect": { - "connects when autoConnect is false": function () { - var mopidy = new Mopidy({ - autoConnect: false, - callingConvention: "by-position-or-by-name" - }); - refute.called(this.webSocketConstructorStub); - - mopidy.connect(); - - var currentHost = typeof document !== "undefined" && - document.location.host || "localhost"; - - assert.calledOnceWith(this.webSocketConstructorStub, - "ws://" + currentHost + "/mopidy/ws"); - }, - - "does nothing when the WebSocket is open": function () { - this.webSocket.readyState = Mopidy.WebSocket.OPEN; - var mopidy = new Mopidy({ - callingConvention: "by-position-or-by-name", - webSocket: this.webSocket - }); - - mopidy.connect(); - - refute.called(this.webSocket.close); - refute.called(this.webSocketConstructorStub); - } - }, - - "WebSocket events": { - "emits 'websocket:close' when connection is closed": function () { - var spy = this.spy(); - this.mopidy.off("websocket:close"); - this.mopidy.on("websocket:close", spy); - - var closeEvent = {}; - this.webSocket.onclose(closeEvent); - - assert.calledOnceWith(spy, closeEvent); - }, - - "emits 'websocket:error' when errors occurs": function () { - var spy = this.spy(); - this.mopidy.off("websocket:error"); - this.mopidy.on("websocket:error", spy); - - var errorEvent = {}; - this.webSocket.onerror(errorEvent); - - assert.calledOnceWith(spy, errorEvent); - }, - - "emits 'websocket:incomingMessage' when a message arrives": function () { - var spy = this.spy(); - this.mopidy.off("websocket:incomingMessage"); - this.mopidy.on("websocket:incomingMessage", spy); - - var messageEvent = {data: "this is a message"}; - this.webSocket.onmessage(messageEvent); - - assert.calledOnceWith(spy, messageEvent); - }, - - "emits 'websocket:open' when connection is opened": function () { - var spy = this.spy(); - this.mopidy.off("websocket:open"); - this.mopidy.on("websocket:open", spy); - - this.webSocket.onopen(); - - assert.calledOnceWith(spy); - } - }, - - "._cleanup": { - setUp: function () { - this.mopidy.off("state:offline"); - }, - - "is called on 'websocket:close' event": function () { - var closeEvent = {}; - var stub = this.stub(this.mopidy, "_cleanup"); - this.mopidy._delegateEvents(); - - this.mopidy.emit("websocket:close", closeEvent); - - assert.calledOnceWith(stub, closeEvent); - }, - - "rejects all pending requests": function (done) { - var closeEvent = {}; - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - - var promise1 = this.mopidy._send({method: "foo"}); - var promise2 = this.mopidy._send({method: "bar"}); - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 2); - - this.mopidy._cleanup(closeEvent); - - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - when.settle([promise1, promise2]).done( - done(function (descriptors) { - assert.equals(descriptors.length, 2); - descriptors.forEach(function (d) { - assert.equals(d.state, "rejected"); - assert(d.reason instanceof Error); - assert(d.reason instanceof Mopidy.ConnectionError); - assert.equals(d.reason.message, "WebSocket closed"); - assert.same(d.reason.closeEvent, closeEvent); - }); - }) - ); - }, - - "emits 'state:offline' event when done": function () { - var spy = this.spy(); - this.mopidy.on("state:offline", spy); - - this.mopidy._cleanup({}); - - assert.calledOnceWith(spy); - } - }, - - "._reconnect": { - "is called when the state changes to offline": function () { - var stub = this.stub(this.mopidy, "_reconnect"); - this.mopidy._delegateEvents(); - - this.mopidy.emit("state:offline"); - - assert.calledOnceWith(stub); - }, - - "tries to connect after an increasing backoff delay": function () { - var clock = this.useFakeTimers(); - var connectStub = this.stub(this.mopidy, "connect"); - var pendingSpy = this.spy(); - this.mopidy.on("reconnectionPending", pendingSpy); - var reconnectingSpy = this.spy(); - this.mopidy.on("reconnecting", reconnectingSpy); - - refute.called(connectStub); - - this.mopidy._reconnect(); - assert.calledOnceWith(pendingSpy, {timeToAttempt: 1000}); - clock.tick(0); - refute.called(connectStub); - clock.tick(1000); - assert.calledOnceWith(reconnectingSpy); - assert.calledOnce(connectStub); - - pendingSpy.reset(); - reconnectingSpy.reset(); - this.mopidy._reconnect(); - assert.calledOnceWith(pendingSpy, {timeToAttempt: 2000}); - assert.calledOnce(connectStub); - clock.tick(0); - assert.calledOnce(connectStub); - clock.tick(1000); - assert.calledOnce(connectStub); - clock.tick(1000); - assert.calledOnceWith(reconnectingSpy); - assert.calledTwice(connectStub); - - pendingSpy.reset(); - reconnectingSpy.reset(); - this.mopidy._reconnect(); - assert.calledOnceWith(pendingSpy, {timeToAttempt: 4000}); - assert.calledTwice(connectStub); - clock.tick(0); - assert.calledTwice(connectStub); - clock.tick(2000); - assert.calledTwice(connectStub); - clock.tick(2000); - assert.calledOnceWith(reconnectingSpy); - assert.calledThrice(connectStub); - }, - - "tries to connect at least about once per minute": function () { - var clock = this.useFakeTimers(); - var connectStub = this.stub(this.mopidy, "connect"); - var pendingSpy = this.spy(); - this.mopidy.on("reconnectionPending", pendingSpy); - this.mopidy._backoffDelay = this.mopidy._settings.backoffDelayMax; - - refute.called(connectStub); - - this.mopidy._reconnect(); - assert.calledOnceWith(pendingSpy, {timeToAttempt: 64000}); - clock.tick(0); - refute.called(connectStub); - clock.tick(64000); - assert.calledOnce(connectStub); - - pendingSpy.reset(); - this.mopidy._reconnect(); - assert.calledOnceWith(pendingSpy, {timeToAttempt: 64000}); - assert.calledOnce(connectStub); - clock.tick(0); - assert.calledOnce(connectStub); - clock.tick(64000); - assert.calledTwice(connectStub); - } - }, - - "._resetBackoffDelay": { - "is called on 'websocket:open' event": function () { - var stub = this.stub(this.mopidy, "_resetBackoffDelay"); - this.mopidy._delegateEvents(); - - this.mopidy.emit("websocket:open"); - - assert.calledOnceWith(stub); - }, - - "resets the backoff delay to the minimum value": function () { - this.mopidy._backoffDelay = this.mopidy._backoffDelayMax; - - this.mopidy._resetBackoffDelay(); - - assert.equals(this.mopidy._backoffDelay, - this.mopidy._settings.backoffDelayMin); - } - }, - - "close": { - "unregisters reconnection hooks": function () { - this.stub(this.mopidy, "off"); - - this.mopidy.close(); - - assert.calledOnceWith( - this.mopidy.off, "state:offline", this.mopidy._reconnect); - }, - - "closes the WebSocket": function () { - this.mopidy.close(); - - assert.calledOnceWith(this.mopidy._webSocket.close); - } - }, - - "._handleWebSocketError": { - "is called on 'websocket:error' event": function () { - var error = {}; - var stub = this.stub(this.mopidy, "_handleWebSocketError"); - this.mopidy._delegateEvents(); - - this.mopidy.emit("websocket:error", error); - - assert.calledOnceWith(stub, error); - }, - - "without stack logs the error to the console": function () { - var stub = this.stub(this.mopidy._console, "warn"); - var error = {}; - - this.mopidy._handleWebSocketError(error); - - assert.calledOnceWith(stub, "WebSocket error:", error); - }, - - "with stack logs the error to the console": function () { - var stub = this.stub(this.mopidy._console, "warn"); - var error = {stack: "foo"}; - - this.mopidy._handleWebSocketError(error); - - assert.calledOnceWith(stub, "WebSocket error:", error.stack); - } - }, - - "._send": { - "adds JSON-RPC fields to the message": function () { - this.stub(this.mopidy, "_nextRequestId").returns(1); - var stub = this.stub(JSON, "stringify"); - - this.mopidy._send({method: "foo"}); - - assert.calledOnceWith(stub, { - jsonrpc: "2.0", - id: 1, - method: "foo" - }); - }, - - "adds a resolver to the pending requests queue": function () { - this.stub(this.mopidy, "_nextRequestId").returns(1); - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - - this.mopidy._send({method: "foo"}); - - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 1); - assert.isFunction(this.mopidy._pendingRequests[1].resolve); - }, - - "sends message on the WebSocket": function () { - refute.called(this.mopidy._webSocket.send); - - this.mopidy._send({method: "foo"}); - - assert.calledOnce(this.mopidy._webSocket.send); - }, - - "emits a 'websocket:outgoingMessage' event": function () { - var spy = this.spy(); - this.mopidy.on("websocket:outgoingMessage", spy); - this.stub(this.mopidy, "_nextRequestId").returns(1); - - this.mopidy._send({method: "foo"}); - - assert.calledOnceWith(spy, { - jsonrpc: "2.0", - id: 1, - method: "foo" - }); - }, - - "immediately rejects request if CONNECTING": function (done) { - this.mopidy._webSocket.readyState = Mopidy.WebSocket.CONNECTING; - - var promise = this.mopidy._send({method: "foo"}); - - refute.called(this.mopidy._webSocket.send); - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof Mopidy.ConnectionError); - assert.equals( - error.message, "WebSocket is still connecting"); - }) - ); - }, - - "immediately rejects request if CLOSING": function (done) { - this.mopidy._webSocket.readyState = Mopidy.WebSocket.CLOSING; - - var promise = this.mopidy._send({method: "foo"}); - - refute.called(this.mopidy._webSocket.send); - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof Mopidy.ConnectionError); - assert.equals(error.message, "WebSocket is closing"); - }) - ); - }, - - "immediately rejects request if CLOSED": function (done) { - this.mopidy._webSocket.readyState = Mopidy.WebSocket.CLOSED; - - var promise = this.mopidy._send({method: "foo"}); - - refute.called(this.mopidy._webSocket.send); - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof Mopidy.ConnectionError); - assert.equals(error.message, "WebSocket is closed"); - }) - ); - } - }, - - "._nextRequestId": { - "returns an ever increasing ID": function () { - var base = this.mopidy._nextRequestId(); - assert.equals(this.mopidy._nextRequestId(), base + 1); - assert.equals(this.mopidy._nextRequestId(), base + 2); - assert.equals(this.mopidy._nextRequestId(), base + 3); - } - }, - - "._handleMessage": { - "is called on 'websocket:incomingMessage' event": function () { - var messageEvent = {}; - var stub = this.stub(this.mopidy, "_handleMessage"); - this.mopidy._delegateEvents(); - - this.mopidy.emit("websocket:incomingMessage", messageEvent); - - assert.calledOnceWith(stub, messageEvent); - }, - - "passes JSON-RPC responses on to _handleResponse": function () { - var stub = this.stub(this.mopidy, "_handleResponse"); - var message = { - jsonrpc: "2.0", - id: 1, - result: null - }; - var messageEvent = {data: JSON.stringify(message)}; - - this.mopidy._handleMessage(messageEvent); - - assert.calledOnceWith(stub, message); - }, - - "passes events on to _handleEvent": function () { - var stub = this.stub(this.mopidy, "_handleEvent"); - var message = { - event: "track_playback_started", - track: {} - }; - var messageEvent = {data: JSON.stringify(message)}; - - this.mopidy._handleMessage(messageEvent); - - assert.calledOnceWith(stub, message); - }, - - "logs unknown messages": function () { - var stub = this.stub(this.mopidy._console, "warn"); - var messageEvent = {data: JSON.stringify({foo: "bar"})}; - - this.mopidy._handleMessage(messageEvent); - - assert.calledOnceWith(stub, - "Unknown message type received. Message was: " + - messageEvent.data); - }, - - "logs JSON parsing errors": function () { - var stub = this.stub(this.mopidy._console, "warn"); - var messageEvent = {data: "foobarbaz"}; - - this.mopidy._handleMessage(messageEvent); - - assert.calledOnceWith(stub, - "WebSocket message parsing failed. Message was: " + - messageEvent.data); - } - }, - - "._handleResponse": { - "logs unexpected responses": function () { - var stub = this.stub(this.mopidy._console, "warn"); - var responseMessage = { - jsonrpc: "2.0", - id: 1337, - result: null - }; - - this.mopidy._handleResponse(responseMessage); - - assert.calledOnceWith(stub, - "Unexpected response received. Message was:", responseMessage); - }, - - "removes the matching request from the pending queue": function () { - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - this.mopidy._send({method: "bar"}); - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 1); - - this.mopidy._handleResponse({ - jsonrpc: "2.0", - id: Object.keys(this.mopidy._pendingRequests)[0], - result: "baz" - }); - - assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - }, - - "resolves requests which get results back": function (done) { - var promise = this.mopidy._send({method: "bar"}); - var responseResult = {}; - var responseMessage = { - jsonrpc: "2.0", - id: Object.keys(this.mopidy._pendingRequests)[0], - result: responseResult - }; - - this.mopidy._handleResponse(responseMessage); - promise.then(done(function (result) { - assert.equals(result, responseResult); - }), done(function () { - assert(false); - })); - }, - - "rejects and logs requests which get errors back": function (done) { - var stub = this.stub(this.mopidy._console, "warn"); - var promise = this.mopidy._send({method: "bar"}); - var responseError = { - code: -32601, - message: "Method not found", - data: {} - }; - var responseMessage = { - jsonrpc: "2.0", - id: Object.keys(this.mopidy._pendingRequests)[0], - error: responseError - }; - - this.mopidy._handleResponse(responseMessage); - - assert.calledOnceWith(stub, - "Server returned error:", responseError); - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert.equals(error.code, responseError.code); - assert.equals(error.message, responseError.message); - assert.equals(error.data, responseError.data); - }) - ); - }, - - "rejects and logs requests which get errors without data": function (done) { - var stub = this.stub(this.mopidy._console, "warn"); - var promise = this.mopidy._send({method: "bar"}); - var responseError = { - code: -32601, - message: "Method not found" - // 'data' key intentionally missing - }; - var responseMessage = { - jsonrpc: "2.0", - id: Object.keys(this.mopidy._pendingRequests)[0], - error: responseError - }; - - this.mopidy._handleResponse(responseMessage); - - assert.calledOnceWith(stub, - "Server returned error:", responseError); - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof Mopidy.ServerError); - assert.equals(error.code, responseError.code); - assert.equals(error.message, responseError.message); - refute.defined(error.data); - }) - ); - }, - - "rejects and logs responses without result or error": function (done) { - var stub = this.stub(this.mopidy._console, "warn"); - var promise = this.mopidy._send({method: "bar"}); - var responseMessage = { - jsonrpc: "2.0", - id: Object.keys(this.mopidy._pendingRequests)[0] - }; - - this.mopidy._handleResponse(responseMessage); - - assert.calledOnceWith(stub, - "Response without 'result' or 'error' received. Message was:", - responseMessage); - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert.equals( - error.message, - "Response without 'result' or 'error' received"); - assert.equals(error.data.response, responseMessage); - }) - ); - } - }, - - "._handleEvent": { - "emits server side even on Mopidy object": function () { - var spy = this.spy(); - this.mopidy.on(spy); - var track = {}; - var message = { - event: "track_playback_started", - track: track - }; - - this.mopidy._handleEvent(message); - - assert.calledOnceWith(spy, - "event:trackPlaybackStarted", {track: track}); - } - }, - - "._getApiSpec": { - "is called on 'websocket:open' event": function () { - var stub = this.stub(this.mopidy, "_getApiSpec"); - this.mopidy._delegateEvents(); - - this.mopidy.emit("websocket:open"); - - assert.calledOnceWith(stub); - }, - - "gets Api description from server and calls _createApi": function (done) { - var methods = {}; - var sendStub = this.stub(this.mopidy, "_send"); - sendStub.returns(when.resolve(methods)); - var _createApiStub = this.stub(this.mopidy, "_createApi"); - - this.mopidy._getApiSpec().then(done(function () { - assert.calledOnceWith(sendStub, {method: "core.describe"}); - assert.calledOnceWith(_createApiStub, methods); - })); - } - }, - - "._createApi": { - "can create an API with methods on the root object": function () { - refute.defined(this.mopidy.hello); - refute.defined(this.mopidy.hi); - - this.mopidy._createApi({ - hello: { - description: "Says hello", - params: [] - }, - hi: { - description: "Says hi", - params: [] - } - }); - - assert.isFunction(this.mopidy.hello); - assert.equals(this.mopidy.hello.description, "Says hello"); - assert.equals(this.mopidy.hello.params, []); - assert.isFunction(this.mopidy.hi); - assert.equals(this.mopidy.hi.description, "Says hi"); - assert.equals(this.mopidy.hi.params, []); - }, - - "can create an API with methods on a sub-object": function () { - refute.defined(this.mopidy.hello); - - this.mopidy._createApi({ - "hello.world": { - description: "Says hello to the world", - params: [] - } - }); - - assert.defined(this.mopidy.hello); - assert.isFunction(this.mopidy.hello.world); - }, - - "strips off 'core' from method paths": function () { - refute.defined(this.mopidy.hello); - - this.mopidy._createApi({ - "core.hello.world": { - description: "Says hello to the world", - params: [] - } - }); - - assert.defined(this.mopidy.hello); - assert.isFunction(this.mopidy.hello.world); - }, - - "converts snake_case to camelCase": function () { - refute.defined(this.mopidy.mightyGreetings); - - this.mopidy._createApi({ - "mighty_greetings.hello_world": { - description: "Says hello to the world", - params: [] - } - }); - - assert.defined(this.mopidy.mightyGreetings); - assert.isFunction(this.mopidy.mightyGreetings.helloWorld); - }, - - "triggers 'state:online' event when API is ready for use": function () { - var spy = this.spy(); - this.mopidy.on("state:online", spy); - - this.mopidy._createApi({}); - - assert.calledOnceWith(spy); - }, - - "by-position-only calling convention": { - setUp: function () { - this.mopidy = new Mopidy({ - webSocket: this.webSocket, - callingConvention: "by-position-only" - }); - this.mopidy._createApi({ - foo: { - params: ["bar", "baz"] - } - }); - this.sendStub = this.stub(this.mopidy, "_send"); - - }, - - "sends no params if no arguments passed to function": function () { - this.mopidy.foo(); - - assert.calledOnceWith(this.sendStub, {method: "foo"}); - }, - - "sends messages with function arguments unchanged": function () { - this.mopidy.foo(31, 97); - - assert.calledOnceWith(this.sendStub, { - method: "foo", - params: [31, 97] - }); - }, - }, - - "by-position-or-by-name calling convention": { - setUp: function () { - this.mopidy = new Mopidy({ - webSocket: this.webSocket, - callingConvention: "by-position-or-by-name" - }); - this.mopidy._createApi({ - foo: { - params: ["bar", "baz"] - } - }); - this.sendStub = this.stub(this.mopidy, "_send"); - }, - - "must be turned on manually": function () { - assert.equals( - this.mopidy._settings.callingConvention, - "by-position-or-by-name"); - }, - - "sends no params if no arguments passed to function": function () { - this.mopidy.foo(); - - assert.calledOnceWith(this.sendStub, {method: "foo"}); - }, - - "sends by-position if argument is a list": function () { - this.mopidy.foo([31, 97]); - - assert.calledOnceWith(this.sendStub, { - method: "foo", - params: [31, 97] - }); - }, - - "sends by-name if argument is an object": function () { - this.mopidy.foo({bar: 31, baz: 97}); - - assert.calledOnceWith(this.sendStub, { - method: "foo", - params: {bar: 31, baz: 97} - }); - }, - - "rejects with error if more than one argument": function (done) { - var promise = this.mopidy.foo([1, 2], {c: 3, d: 4}); - - refute.called(this.sendStub); - - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert.equals( - error.message, - "Expected zero arguments, a single array, " + - "or a single object."); - }) - ); - }, - - "rejects with error if string": function (done) { - var promise = this.mopidy.foo("hello"); - - refute.called(this.sendStub); - - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof TypeError); - assert.equals( - error.message, "Expected an array or an object."); - }) - ); - }, - - "rejects with error if number": function (done) { - var promise = this.mopidy.foo(1337); - - refute.called(this.sendStub); - - promise.done( - done(function () { - assert(false); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof TypeError); - assert.equals( - error.message, "Expected an array or an object."); - }) - ); - } - } - } -});