js: Add fully working core API in JavaScript
This commit is contained in:
parent
6face51e52
commit
12f60f3a52
274
js/src/mopidy.js
274
js/src/mopidy.js
@ -1,2 +1,276 @@
|
||||
/*global bane:false, when:false*/
|
||||
|
||||
function Mopidy(settings) {
|
||||
var mopidy = this;
|
||||
|
||||
mopidy._webSocket = null;
|
||||
mopidy._pendingRequests = {};
|
||||
mopidy._backoffDelayMin = 1000;
|
||||
mopidy._backoffDelayMax = 64000;
|
||||
mopidy._backoffDelay = mopidy._backoffDelayMin;
|
||||
|
||||
mopidy._settings = settings || {};
|
||||
mopidy._settings.webSocketUrl =
|
||||
mopidy._settings.webSocketUrl ||
|
||||
"ws://" + document.location.host + "/mopidy/ws/";
|
||||
if (mopidy._settings.autoConnect !== false) {
|
||||
mopidy._settings.autoConnect = true;
|
||||
}
|
||||
|
||||
bane.createEventEmitter(mopidy);
|
||||
mopidy._delegateEvents();
|
||||
|
||||
if (mopidy._settings.autoConnect) {
|
||||
mopidy._connect();
|
||||
}
|
||||
}
|
||||
|
||||
Mopidy.prototype._delegateEvents = function () {
|
||||
var mopidy = this;
|
||||
|
||||
// Remove existing event handlers
|
||||
mopidy.off("websocket:close");
|
||||
mopidy.off("websocket:error");
|
||||
mopidy.off("websocket:incomingMessage");
|
||||
mopidy.off("websocket:open");
|
||||
mopidy.off("state:offline");
|
||||
|
||||
// Register basic set of event handlers
|
||||
mopidy.on("websocket:close", mopidy._cleanup);
|
||||
mopidy.on("websocket:error", mopidy._handleWebSocketError);
|
||||
mopidy.on("websocket:incomingMessage", mopidy._handleMessage);
|
||||
mopidy.on("websocket:open", mopidy._resetBackoffDelay);
|
||||
mopidy.on("websocket:open", mopidy._getApiSpec);
|
||||
mopidy.on("state:offline", mopidy._reconnect);
|
||||
};
|
||||
|
||||
Mopidy.prototype._connect = function () {
|
||||
var mopidy = this;
|
||||
|
||||
if (mopidy._webSocket) {
|
||||
if (mopidy._webSocket.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
} else {
|
||||
mopidy._webSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
mopidy._webSocket = mopidy._settings.webSocket ||
|
||||
new WebSocket(mopidy._settings.webSocketUrl);
|
||||
|
||||
mopidy._webSocket.onclose = function (close) {
|
||||
mopidy.emit("websocket:close", close);
|
||||
};
|
||||
|
||||
mopidy._webSocket.onerror = function (error) {
|
||||
mopidy.emit("websocket:error", error);
|
||||
};
|
||||
|
||||
mopidy._webSocket.onopen = function () {
|
||||
mopidy.emit("websocket:open");
|
||||
};
|
||||
|
||||
mopidy._webSocket.onmessage = function (message) {
|
||||
mopidy.emit("websocket:incomingMessage", message);
|
||||
};
|
||||
};
|
||||
|
||||
Mopidy.prototype._cleanup = function (closeEvent) {
|
||||
var mopidy = this;
|
||||
|
||||
Object.keys(mopidy._pendingRequests).forEach(function (requestId) {
|
||||
var resolver = mopidy._pendingRequests[requestId];
|
||||
delete mopidy._pendingRequests[requestId];
|
||||
resolver.reject({
|
||||
message: "WebSocket closed",
|
||||
closeEvent: closeEvent
|
||||
});
|
||||
});
|
||||
|
||||
mopidy.emit("state:offline");
|
||||
};
|
||||
|
||||
Mopidy.prototype._reconnect = function () {
|
||||
var mopidy = this;
|
||||
|
||||
mopidy.emit("reconnectionPending", {
|
||||
timeToAttempt: mopidy._backoffDelay
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
mopidy.emit("reconnecting");
|
||||
mopidy._connect();
|
||||
}, mopidy._backoffDelay);
|
||||
|
||||
mopidy._backoffDelay = mopidy._backoffDelay * 2;
|
||||
if (mopidy._backoffDelay > mopidy._backoffDelayMax) {
|
||||
mopidy._backoffDelay = mopidy._backoffDelayMax;
|
||||
}
|
||||
};
|
||||
|
||||
Mopidy.prototype._resetBackoffDelay = function () {
|
||||
var mopidy = this;
|
||||
|
||||
mopidy._backoffDelay = mopidy._backoffDelayMin;
|
||||
};
|
||||
|
||||
Mopidy.prototype._handleWebSocketError = function (error) {
|
||||
console.warn("WebSocket error:", error.stack || error);
|
||||
};
|
||||
|
||||
Mopidy.prototype._send = function (message) {
|
||||
var mopidy = this;
|
||||
var deferred = when.defer();
|
||||
|
||||
switch (mopidy._webSocket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is still connecting"
|
||||
});
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closing"
|
||||
});
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
deferred.resolver.reject({
|
||||
message: "WebSocket is closed"
|
||||
});
|
||||
break;
|
||||
default:
|
||||
message.jsonrpc = "2.0";
|
||||
message.id = mopidy._nextRequestId();
|
||||
this._pendingRequests[message.id] = deferred.resolver;
|
||||
this._webSocket.send(JSON.stringify(message));
|
||||
mopidy.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) {
|
||||
var mopidy = this;
|
||||
|
||||
try {
|
||||
var data = JSON.parse(message.data);
|
||||
if (data.hasOwnProperty("id")) {
|
||||
mopidy._handleResponse(data);
|
||||
} else if (data.hasOwnProperty("event")) {
|
||||
mopidy._handleEvent(data);
|
||||
} else {
|
||||
console.warn(
|
||||
"Unknown message type received. Message was: " +
|
||||
message.data);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
console.warn(
|
||||
"WebSocket message parsing failed. Message was: " +
|
||||
message.data);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Mopidy.prototype._handleResponse = function (responseMessage) {
|
||||
var mopidy = this;
|
||||
|
||||
if (!mopidy._pendingRequests.hasOwnProperty(responseMessage.id)) {
|
||||
console.warn(
|
||||
"Unexpected response received. Message was:", responseMessage);
|
||||
return;
|
||||
}
|
||||
var resolver = mopidy._pendingRequests[responseMessage.id];
|
||||
delete mopidy._pendingRequests[responseMessage.id];
|
||||
|
||||
if (responseMessage.hasOwnProperty("result")) {
|
||||
resolver.resolve(responseMessage.result);
|
||||
} else if (responseMessage.hasOwnProperty("error")) {
|
||||
resolver.reject(responseMessage.error);
|
||||
console.warn("Server returned error:", responseMessage.error);
|
||||
} else {
|
||||
resolver.reject({
|
||||
message: "Response without 'result' or 'error' received",
|
||||
data: {response: responseMessage}
|
||||
});
|
||||
console.warn(
|
||||
"Response without 'result' or 'error' received. Message was:",
|
||||
responseMessage);
|
||||
}
|
||||
};
|
||||
|
||||
Mopidy.prototype._handleEvent = function (eventMessage) {
|
||||
var mopidy = this;
|
||||
|
||||
var type = eventMessage.event;
|
||||
var data = eventMessage;
|
||||
delete data.event;
|
||||
|
||||
mopidy.emit("event:" + mopidy._snakeToCamel(type), data);
|
||||
};
|
||||
|
||||
Mopidy.prototype._getApiSpec = function () {
|
||||
var mopidy = this;
|
||||
|
||||
mopidy._send({method: "core.describe"})
|
||||
.then(mopidy._createApi.bind(mopidy), mopidy._handleWebSocketError)
|
||||
.then(null, mopidy._handleWebSocketError);
|
||||
};
|
||||
|
||||
Mopidy.prototype._createApi = function (methods) {
|
||||
var mopidy = this;
|
||||
|
||||
var caller = function (method) {
|
||||
return function () {
|
||||
var params = Array.prototype.slice.call(arguments);
|
||||
return mopidy._send({
|
||||
method: method,
|
||||
params: params
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
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 = mopidy;
|
||||
objPath.forEach(function (objName) {
|
||||
objName = mopidy._snakeToCamel(objName);
|
||||
parentObj[objName] = parentObj[objName] || {};
|
||||
parentObj = parentObj[objName];
|
||||
});
|
||||
return parentObj;
|
||||
};
|
||||
|
||||
var createMethod = function (fullMethodName) {
|
||||
var methodPath = getPath(fullMethodName);
|
||||
var methodName = mopidy._snakeToCamel(methodPath.slice(-1)[0]);
|
||||
var object = createObjects(methodPath.slice(0, -1));
|
||||
object[methodName] = caller(fullMethodName);
|
||||
};
|
||||
|
||||
Object.keys(methods).forEach(createMethod);
|
||||
mopidy.emit("state:online");
|
||||
};
|
||||
|
||||
Mopidy.prototype._snakeToCamel = function (name) {
|
||||
return name.replace(/(_[a-z])/g, function (match) {
|
||||
return match.toUpperCase().replace("_", "");
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,4 +1,648 @@
|
||||
/*global when:false, buster:false, assert:false, refute:false, Mopidy:false*/
|
||||
/*global buster:false, assert:false, refute:false, when:false, Mopidy:false*/
|
||||
|
||||
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 = WebSocket;
|
||||
window.WebSocket = fakeWebSocket;
|
||||
|
||||
this.webSocketConstructorStub = this.stub(window, "WebSocket");
|
||||
|
||||
this.webSocket = {
|
||||
close: this.stub(),
|
||||
send: this.stub()
|
||||
};
|
||||
this.mopidy = new Mopidy({webSocket: this.webSocket});
|
||||
},
|
||||
|
||||
tearDown: function () {
|
||||
window.WebSocket = this.realWebSocket;
|
||||
},
|
||||
|
||||
"constructor": {
|
||||
"connects when autoConnect is true": function () {
|
||||
new Mopidy({autoConnect: true});
|
||||
|
||||
assert.calledOnceWith(this.webSocketConstructorStub,
|
||||
"ws://" + document.location.host + "/mopidy/ws/");
|
||||
},
|
||||
|
||||
"does not connect when autoConnect is false": function () {
|
||||
new Mopidy({autoConnect: false});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
},
|
||||
|
||||
"does not connect when passed a WebSocket": function () {
|
||||
new Mopidy({webSocket: {}});
|
||||
|
||||
refute.called(this.webSocketConstructorStub);
|
||||
}
|
||||
},
|
||||
|
||||
"._connect": {
|
||||
"does nothing when the WebSocket is open": function () {
|
||||
this.webSocket.readyState = WebSocket.OPEN;
|
||||
var mopidy = new Mopidy({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.join(promise1, promise2).then(done(function () {
|
||||
assert(false, "Promises should be rejected");
|
||||
}), done(function (error) {
|
||||
assert.equals(error.message, "WebSocket closed");
|
||||
assert.same(error.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._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._backoffDelayMin);
|
||||
}
|
||||
},
|
||||
|
||||
"._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(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(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 = WebSocket.CONNECTING;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is still connecting");
|
||||
}));
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSING": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CLOSING;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(
|
||||
error.message, "WebSocket is closing");
|
||||
}));
|
||||
},
|
||||
|
||||
"immediately rejects request if CLOSED": function (done) {
|
||||
this.mopidy._webSocket.readyState = WebSocket.CLOSED;
|
||||
|
||||
var promise = this.mopidy._send({method: "foo"});
|
||||
|
||||
refute.called(this.mopidy._webSocket.send);
|
||||
promise.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
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(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(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(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(console, "warn");
|
||||
var promise = this.mopidy._send({method: "bar"});
|
||||
var responseError = {message: "Error", 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.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (error) {
|
||||
assert.equals(error, responseError);
|
||||
}));
|
||||
},
|
||||
|
||||
"rejects and logs responses without result or error": function (done) {
|
||||
var stub = this.stub(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.then(done(function () {
|
||||
assert(false);
|
||||
}), done(function (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 () {
|
||||
var methods = {};
|
||||
var sendStub = this.stub(this.mopidy, "_send");
|
||||
sendStub.returns(when.resolve(methods));
|
||||
var _createApiStub = this.stub(this.mopidy, "_createApi");
|
||||
|
||||
this.mopidy._getApiSpec();
|
||||
|
||||
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.isFunction(this.mopidy.hi);
|
||||
},
|
||||
|
||||
"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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user