js: Move Mopidy.js source code to a new Git repo
This commit is contained in:
parent
4eacc911c9
commit
d6e0c8d7e6
@ -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
|
||||
<https://github.com/mopidy/mopidy.js>`_. The instructions in ``README.md`` will
|
||||
guide you on your way.
|
||||
|
||||
|
||||
Creating an instance
|
||||
|
||||
101
js/Gruntfile.js
101
js/Gruntfile.js
@ -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");
|
||||
};
|
||||
121
js/README.md
121
js/README.md
@ -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/).
|
||||
15
js/buster.js
15
js/buster.js
@ -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"]
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
module.exports = { Client: window.WebSocket };
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"browser": "browser.js",
|
||||
"main": "server.js"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
module.exports = require('faye-websocket');
|
||||
@ -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": "*"
|
||||
}
|
||||
}
|
||||
331
js/src/mopidy.js
331
js/src/mopidy.js
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
@ -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.");
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user