Merge pull request #744 from jodal/feature/js-updates

Various updates to Mopidy.js
This commit is contained in:
Thomas Adamcik 2014-06-16 22:26:29 +02:00
commit af238d5259
7 changed files with 264 additions and 131 deletions

View File

@ -74,7 +74,7 @@ development setup in the ``js/`` dir in our repo. The instructions in
Creating an instance Creating an instance
==================== ====================
Once you got Mopidy.js loaded, you need to create an instance of the wrapper: Once you have Mopidy.js loaded, you need to create an instance of the wrapper:
.. code-block:: js .. code-block:: js
@ -100,6 +100,31 @@ later:
// ... do other stuff, like hooking up events ... // ... do other stuff, like hooking up events ...
mopidy.connect(); mopidy.connect();
When creating an instance, you can specify the following settings:
``autoConnect``
Whether or not to connect to the WebSocket on instance creation. Defaults
to true.
``backoffDelayMin``
The minimum number of milliseconds to wait after a connection error before
we try to reconnect. For every failed attempt, the backoff delay is doubled
until it reaches ``backoffDelayMax``. Defaults to 1000.
``backoffDelayMax``
The maximum number of milliseconds to wait after a connection error before
we try to reconnect. Defaults to 64000.
``webSocket``
An existing WebSocket object to be used instead of creating a new
WebSocket. Defaults to undefined.
``webSocketUrl``
URL used when creating new WebSocket objects. Defaults to
``ws://<document.location.host>/mopidy/ws``, or
``ws://localhost/mopidy/ws`` if ``document.location.host`` isn't
available, like it is in the browser environment.
Hooking up to events Hooking up to events
==================== ====================
@ -145,7 +170,8 @@ Once your Mopidy.js object has connected to the Mopidy server and emits the
Any calls you make before the ``state:online`` event is emitted will fail. If Any calls you make before the ``state:online`` event is emitted will fail. If
you've hooked up an errback (more on that a bit later) to the promise returned you've hooked up an errback (more on that a bit later) to the promise returned
from the call, the errback will be called with an error message. from the call, the errback will be called with a ``Mopidy.ConnectionError``
instance.
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
API attributes is *not* available, but that shouldn't be a problem as we've API attributes is *not* available, but that shouldn't be a problem as we've
@ -204,22 +230,34 @@ Instead, typical usage will look like this:
} }
}; };
mopidy.playback.getCurrentTrack().then( mopidy.playback.getCurrentTrack()
printCurrentTrack, console.error.bind(console)); .done(printCurrentTrack);
The first function passed to ``then()``, ``printCurrentTrack``, is the callback The function passed to ``done()``, ``printCurrentTrack``, is the callback
that will be called if the method call succeeds. The second function, that will be called if the method call succeeds. If anything goes wrong,
``console.error``, is the errback that will be called if anything goes wrong. ``done()`` will throw an exception.
If you don't hook up an errback, debugging will be hard as errors will silently
go missing.
For debugging, you may be interested in errors from function without If you want to explicitly handle any errors and avoid an exception being
interesting return values as well. In that case, you can pass ``null`` as the thrown, you can register an error handler function anywhere in a promise
callback: chain. The function will be called with the error object as the only argument:
.. code-block:: js .. code-block:: js
mopidy.playback.next().then(null, console.error.bind(console)); mopidy.playback.getCurrentTrack()
.catch(console.error.bind(console));
.done(printCurrentTrack);
You can also register the error handler at the end of the promise chain by
passing it as the second argument to ``done()``:
.. code-block:: js
mopidy.playback.getCurrentTrack()
.done(printCurrentTrack, console.error.bind(console));
If you don't hook up an error handler function and never call ``done()`` on the
promise object, when.js will log warnings to the console that you have
unhandled errors. In general, unhandled errors will not go silently missing.
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the <http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
@ -283,44 +321,38 @@ Example to get started with
.. code-block:: js .. code-block:: js
var consoleError = console.error.bind(console);
var trackDesc = function (track) { var trackDesc = function (track) {
return track.name + " by " + track.artists[0].name + return track.name + " by " + track.artists[0].name +
" from " + track.album.name; " from " + track.album.name;
}; };
var queueAndPlayFirstPlaylist = function () { var queueAndPlay = function (playlistNum, trackNum) {
mopidy.playlists.getPlaylists().then(function (playlists) { playlistNum = playlistNum || 0;
var playlist = playlists[0]; trackNum = trackNum || 0;
mopidy.playlists.getPlaylists().done(function (playlists) {
var playlist = playlists[playlistNum];
console.log("Loading playlist:", playlist.name); console.log("Loading playlist:", playlist.name);
mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) { mopidy.tracklist.add(playlist.tracks).done(function (tlTracks) {
mopidy.playback.play(tlTracks[0]).then(function () { mopidy.playback.play(tlTracks[trackNum]).done(function () {
mopidy.playback.getCurrentTrack().then(function (track) { mopidy.playback.getCurrentTrack().done(function (track) {
console.log("Now playing:", trackDesc(track)); console.log("Now playing:", trackDesc(track));
}, consoleError); });
}, consoleError); });
}, consoleError); });
}, consoleError); });
}; };
var mopidy = new Mopidy(); // Connect to server var mopidy = new Mopidy(); // Connect to server
mopidy.on(console.log.bind(console)); // Log all events mopidy.on(console.log.bind(console)); // Log all events
mopidy.on("state:online", queueAndPlayFirstPlaylist); mopidy.on("state:online", queueAndPlay);
Approximately the same behavior in a more functional style, using chaining Approximately the same behavior in a more functional style, using chaining
of promisies. of promises.
.. code-block:: js .. code-block:: js
var consoleError = console.error.bind(console); var get = function (key, object) {
return object[key];
var getFirst = function (list) {
return list[0];
};
var extractTracks = function (playlist) {
return playlist.tracks;
}; };
var printTypeAndName = function (model) { var printTypeAndName = function (model) {
@ -339,33 +371,36 @@ Example to get started with
// By returning any arguments we get, the function can be inserted // By returning any arguments we get, the function can be inserted
// anywhere in the chain. // anywhere in the chain.
var args = arguments; var args = arguments;
return mopidy.playback.getCurrentTrack().then(function (track) { return mopidy.playback.getCurrentTrack()
console.log("Now playing:", trackDesc(track)); .done(function (track) {
return args; console.log("Now playing:", trackDesc(track));
}); return args;
});
}; };
var queueAndPlayFirstPlaylist = function () { var queueAndPlay = function (playlistNum, trackNum) {
playlistNum = playlistNum || 0;
trackNum = trackNum || 0;
mopidy.playlists.getPlaylists() mopidy.playlists.getPlaylists()
// => list of Playlists // => list of Playlists
.then(getFirst, consoleError) .fold(get, playlistNum)
// => Playlist // => Playlist
.then(printTypeAndName, consoleError) .then(printTypeAndName)
// => Playlist // => Playlist
.then(extractTracks, consoleError) .fold(get, 'tracks')
// => list of Tracks // => list of Tracks
.then(mopidy.tracklist.add, consoleError) .then(mopidy.tracklist.add)
// => list of TlTracks // => list of TlTracks
.then(getFirst, consoleError) .fold(get, trackNum)
// => TlTrack // => TlTrack
.then(mopidy.playback.play, consoleError) .then(mopidy.playback.play)
// => null // => null
.then(printNowPlaying, consoleError); .done(printNowPlaying, console.error.bind(console));
}; };
var mopidy = new Mopidy(); // Connect to server var mopidy = new Mopidy(); // Connect to server
mopidy.on(console.log.bind(console)); // Log all events mopidy.on(console.log.bind(console)); // Log all events
mopidy.on("state:online", queueAndPlayFirstPlaylist); mopidy.on("state:online", queueAndPlay);
9. The web page should now queue and play your first playlist every time your 9. The web page should now queue and play your first playlist every time your
load it. See the browser's console for output from the function, any errors, load it. See the browser's console for output from the function, any errors,

View File

@ -78,6 +78,20 @@ Feature release.
Mopidy's HTTP server among other Zeroconf-published HTTP servers on the Mopidy's HTTP server among other Zeroconf-published HTTP servers on the
local network. local network.
- Update Mopidy.js to use when.js 3. 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>`_.
This version has been released to npm as Mopidy.js v0.3.0.
- All of Mopidy.js' 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``.
**MPD frontend** **MPD frontend**
- Proper command tokenization for MPD requests. This replaces the old regex - Proper command tokenization for MPD requests. This replaces the old regex

View File

@ -26,7 +26,7 @@ module.exports = function (grunt) {
}, },
options: { options: {
postBundleCB: function (err, src, next) { postBundleCB: function (err, src, next) {
next(null, grunt.template.process("<%= meta.banner %>") + src); next(err, grunt.template.process("<%= meta.banner %>") + src);
}, },
standalone: "Mopidy" standalone: "Mopidy"
} }
@ -45,7 +45,7 @@ module.exports = function (grunt) {
}, },
options: { options: {
postBundleCB: function (err, src, next) { postBundleCB: function (err, src, next) {
next(null, grunt.template.process("<%= meta.banner %>") + src); next(err, grunt.template.process("<%= meta.banner %>") + src);
}, },
standalone: "Mopidy" standalone: "Mopidy"
} }

View File

@ -47,13 +47,9 @@ See the [Mopidy.js documentation](http://docs.mopidy.com/en/latest/api/js/).
Building from source Building from source
-------------------- --------------------
1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're 1. Install [Node.js](http://nodejs.org/) and npm. If you're running Ubuntu:
running Ubuntu:
sudo apt-get install python-software-properties sudo apt-get install nodejs-legacy npm
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs
2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies: 2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies:
@ -84,6 +80,20 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in
Changelog Changelog
--------- ---------
### 0.3.0 (UNRELEASED)
- 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) ### 0.2.0 (2014-01-04)
- **Backwards incompatible change for Node.js users:** - **Backwards incompatible change for Node.js users:**

View File

@ -1,6 +1,6 @@
{ {
"name": "mopidy", "name": "mopidy",
"version": "0.2.0", "version": "0.3.0",
"description": "Client lib for controlling a Mopidy music server over a WebSocket", "description": "Client lib for controlling a Mopidy music server over a WebSocket",
"homepage": "http://www.mopidy.com/", "homepage": "http://www.mopidy.com/",
"author": { "author": {
@ -16,17 +16,18 @@
"dependencies": { "dependencies": {
"bane": "~1.1.0", "bane": "~1.1.0",
"faye-websocket": "~0.7.2", "faye-websocket": "~0.7.2",
"when": "~2.7.1" "when": "~3.2.3"
}, },
"devDependencies": { "devDependencies": {
"buster": "~0.7.8", "buster": "~0.7.13",
"grunt": "~0.4.2", "browserify": "~3",
"grunt": "~0.4.5",
"grunt-buster": "~0.3.1", "grunt-buster": "~0.3.1",
"grunt-browserify": "~1.3.0", "grunt-browserify": "~1.3.2",
"grunt-contrib-jshint": "~0.8.0", "grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-uglify": "~0.2.7", "grunt-contrib-uglify": "~0.5.0",
"grunt-contrib-watch": "~0.5.3", "grunt-contrib-watch": "~0.6.1",
"phantomjs": "~1.9.2-6" "phantomjs": "~1.9.7-8"
}, },
"scripts": { "scripts": {
"test": "grunt test", "test": "grunt test",

View File

@ -24,13 +24,27 @@ function Mopidy(settings) {
} }
} }
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.WebSocket = websocket.Client;
Mopidy.prototype._configure = function (settings) { Mopidy.prototype._configure = function (settings) {
var currentHost = (typeof document !== "undefined" && var currentHost = (typeof document !== "undefined" &&
document.location.host) || "localhost"; document.location.host) || "localhost";
settings.webSocketUrl = settings.webSocketUrl || settings.webSocketUrl = settings.webSocketUrl ||
"ws://" + currentHost + "/mopidy/ws/"; "ws://" + currentHost + "/mopidy/ws";
if (settings.autoConnect !== false) { if (settings.autoConnect !== false) {
settings.autoConnect = true; settings.autoConnect = true;
@ -102,10 +116,9 @@ Mopidy.prototype._cleanup = function (closeEvent) {
Object.keys(this._pendingRequests).forEach(function (requestId) { Object.keys(this._pendingRequests).forEach(function (requestId) {
var resolver = this._pendingRequests[requestId]; var resolver = this._pendingRequests[requestId];
delete this._pendingRequests[requestId]; delete this._pendingRequests[requestId];
resolver.reject({ var error = new Mopidy.ConnectionError("WebSocket closed");
message: "WebSocket closed", error.closeEvent = closeEvent;
closeEvent: closeEvent resolver.reject(error);
});
}.bind(this)); }.bind(this));
this.emit("state:offline"); this.emit("state:offline");
@ -141,33 +154,25 @@ Mopidy.prototype._handleWebSocketError = function (error) {
}; };
Mopidy.prototype._send = function (message) { Mopidy.prototype._send = function (message) {
var deferred = when.defer();
switch (this._webSocket.readyState) { switch (this._webSocket.readyState) {
case Mopidy.WebSocket.CONNECTING: case Mopidy.WebSocket.CONNECTING:
deferred.resolver.reject({ return when.reject(
message: "WebSocket is still connecting" new Mopidy.ConnectionError("WebSocket is still connecting"));
});
break;
case Mopidy.WebSocket.CLOSING: case Mopidy.WebSocket.CLOSING:
deferred.resolver.reject({ return when.reject(
message: "WebSocket is closing" new Mopidy.ConnectionError("WebSocket is closing"));
});
break;
case Mopidy.WebSocket.CLOSED: case Mopidy.WebSocket.CLOSED:
deferred.resolver.reject({ return when.reject(
message: "WebSocket is closed" new Mopidy.ConnectionError("WebSocket is closed"));
});
break;
default: default:
var deferred = when.defer();
message.jsonrpc = "2.0"; message.jsonrpc = "2.0";
message.id = this._nextRequestId(); message.id = this._nextRequestId();
this._pendingRequests[message.id] = deferred.resolver; this._pendingRequests[message.id] = deferred.resolver;
this._webSocket.send(JSON.stringify(message)); this._webSocket.send(JSON.stringify(message));
this.emit("websocket:outgoingMessage", message); this.emit("websocket:outgoingMessage", message);
return deferred.promise;
} }
return deferred.promise;
}; };
Mopidy.prototype._nextRequestId = (function () { Mopidy.prototype._nextRequestId = (function () {
@ -208,19 +213,22 @@ Mopidy.prototype._handleResponse = function (responseMessage) {
return; return;
} }
var error;
var resolver = this._pendingRequests[responseMessage.id]; var resolver = this._pendingRequests[responseMessage.id];
delete this._pendingRequests[responseMessage.id]; delete this._pendingRequests[responseMessage.id];
if (responseMessage.hasOwnProperty("result")) { if (responseMessage.hasOwnProperty("result")) {
resolver.resolve(responseMessage.result); resolver.resolve(responseMessage.result);
} else if (responseMessage.hasOwnProperty("error")) { } else if (responseMessage.hasOwnProperty("error")) {
resolver.reject(responseMessage.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); this._console.warn("Server returned error:", responseMessage.error);
} else { } else {
resolver.reject({ error = new Error("Response without 'result' or 'error' received");
message: "Response without 'result' or 'error' received", error.data = {response: responseMessage};
data: {response: responseMessage} resolver.reject(error);
});
this._console.warn( this._console.warn(
"Response without 'result' or 'error' received. Message was:", "Response without 'result' or 'error' received. Message was:",
responseMessage); responseMessage);

View File

@ -48,7 +48,7 @@ buster.testCase("Mopidy", {
document.location.host || "localhost"; document.location.host || "localhost";
assert.calledOnceWith(this.webSocketConstructorStub, assert.calledOnceWith(this.webSocketConstructorStub,
"ws://" + currentHost + "/mopidy/ws/"); "ws://" + currentHost + "/mopidy/ws");
}, },
"does not connect when autoConnect is false": function () { "does not connect when autoConnect is false": function () {
@ -84,7 +84,7 @@ buster.testCase("Mopidy", {
document.location.host || "localhost"; document.location.host || "localhost";
assert.calledOnceWith(this.webSocketConstructorStub, assert.calledOnceWith(this.webSocketConstructorStub,
"ws://" + currentHost + "/mopidy/ws/"); "ws://" + currentHost + "/mopidy/ws");
}, },
"does nothing when the WebSocket is open": function () { "does nothing when the WebSocket is open": function () {
@ -169,12 +169,18 @@ buster.testCase("Mopidy", {
this.mopidy._cleanup(closeEvent); this.mopidy._cleanup(closeEvent);
assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0);
when.join(promise1, promise2).then(done(function () { when.settle([promise1, promise2]).done(
assert(false, "Promises should be rejected"); done(function (descriptors) {
}), done(function (error) { assert.equals(descriptors.length, 2);
assert.equals(error.message, "WebSocket closed"); descriptors.forEach(function (d) {
assert.same(error.closeEvent, closeEvent); 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 () { "emits 'state:offline' event when done": function () {
@ -388,12 +394,17 @@ buster.testCase("Mopidy", {
var promise = this.mopidy._send({method: "foo"}); var promise = this.mopidy._send({method: "foo"});
refute.called(this.mopidy._webSocket.send); refute.called(this.mopidy._webSocket.send);
promise.then(done(function () { promise.done(
assert(false); done(function () {
}), done(function (error) { assert(false);
assert.equals( }),
error.message, "WebSocket is still connecting"); 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) { "immediately rejects request if CLOSING": function (done) {
@ -402,12 +413,16 @@ buster.testCase("Mopidy", {
var promise = this.mopidy._send({method: "foo"}); var promise = this.mopidy._send({method: "foo"});
refute.called(this.mopidy._webSocket.send); refute.called(this.mopidy._webSocket.send);
promise.then(done(function () { promise.done(
assert(false); done(function () {
}), done(function (error) { assert(false);
assert.equals( }),
error.message, "WebSocket is closing"); 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) { "immediately rejects request if CLOSED": function (done) {
@ -416,12 +431,16 @@ buster.testCase("Mopidy", {
var promise = this.mopidy._send({method: "foo"}); var promise = this.mopidy._send({method: "foo"});
refute.called(this.mopidy._webSocket.send); refute.called(this.mopidy._webSocket.send);
promise.then(done(function () { promise.done(
assert(false); done(function () {
}), done(function (error) { assert(false);
assert.equals( }),
error.message, "WebSocket is closed"); done(function (error) {
})); assert(error instanceof Error);
assert(error instanceof Mopidy.ConnectionError);
assert.equals(error.message, "WebSocket is closed");
})
);
} }
}, },
@ -544,7 +563,11 @@ buster.testCase("Mopidy", {
"rejects and logs requests which get errors back": function (done) { "rejects and logs requests which get errors back": function (done) {
var stub = this.stub(this.mopidy._console, "warn"); var stub = this.stub(this.mopidy._console, "warn");
var promise = this.mopidy._send({method: "bar"}); var promise = this.mopidy._send({method: "bar"});
var responseError = {message: "Error", data: {}}; var responseError = {
code: -32601,
message: "Method not found",
data: {}
};
var responseMessage = { var responseMessage = {
jsonrpc: "2.0", jsonrpc: "2.0",
id: Object.keys(this.mopidy._pendingRequests)[0], id: Object.keys(this.mopidy._pendingRequests)[0],
@ -555,11 +578,49 @@ buster.testCase("Mopidy", {
assert.calledOnceWith(stub, assert.calledOnceWith(stub,
"Server returned error:", responseError); "Server returned error:", responseError);
promise.then(done(function () { promise.done(
assert(false); done(function () {
}), done(function (error) { assert(false);
assert.equals(error, responseError); }),
})); 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) { "rejects and logs responses without result or error": function (done) {
@ -575,14 +636,18 @@ buster.testCase("Mopidy", {
assert.calledOnceWith(stub, assert.calledOnceWith(stub,
"Response without 'result' or 'error' received. Message was:", "Response without 'result' or 'error' received. Message was:",
responseMessage); responseMessage);
promise.then(done(function () { promise.done(
assert(false); done(function () {
}), done(function (error) { assert(false);
assert.equals( }),
error.message, done(function (error) {
"Response without 'result' or 'error' received"); assert(error instanceof Error);
assert.equals(error.data.response, responseMessage); assert.equals(
})); error.message,
"Response without 'result' or 'error' received");
assert.equals(error.data.response, responseMessage);
})
);
} }
}, },