Repository: mondora/ddp.js Branch: master Commit: 70055210083f Files: 30 Total size: 62.0 KB Directory structure: gitextract_ltd_5_p8/ ├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── ddp.js │ ├── queue.js │ ├── socket.js │ └── utils.js └── test/ ├── e2e/ │ ├── connection.js │ ├── methods.js │ └── subscriptions.js ├── server/ │ ├── .meteor/ │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── methods.js │ └── publications.js └── unit/ ├── ddp.js ├── queue.js ├── socket.js └── utils.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["es2015", "stage-0"], "env": { "test": { "plugins": ["istanbul"] } } } ================================================ FILE: .eslintrc ================================================ { "extends": "eslint:recommended", "parser": "babel-eslint", "env": { "browser": true, "mocha": true, "node": true, "meteor": true }, "rules": { "brace-style": [2, "1tbs"], "comma-spacing": [2, {"before": false, "after": true}], "computed-property-spacing": [2, "never"], "indent": [2, 4], "linebreak-style": [2, "unix"], "new-cap": [2, {"capIsNew": false}], "no-console": [0], "no-multi-spaces": [0], "no-underscore-dangle": [0], "object-curly-spacing": [2, "never"], "one-var": [2, "never"], "quotes": [2, "double"], "semi": [2, "always"], "keyword-spacing": [2], "space-before-blocks": [2, "always"], "space-before-function-paren": [2, "always"], "space-in-parens": [2, "never"], "strict": [2, "never"] } } ================================================ FILE: .gitignore ================================================ node_modules/ npm-debug.log coverage/ .nyc_output lib/ .DS_Store ================================================ FILE: .npmignore ================================================ node_modules/ npm-debug.log coverage/ ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 8 # before_install: # - curl https://install.meteor.com | /bin/sh # before_script: # - npm run start-meteor & sleep 30 deploy: provider: npm email: npm-bot@mondora.com api_key: $NPM_TOKEN on: tags: true skip_cleanup: true script: - npm run lint - npm run coverage - npm run coveralls # - npm run e2e-test ================================================ FILE: CHANGELOG.md ================================================ ## 2.2.1 (October 26, 2017) PR #38: update dependencies ## 2.2.0 (July 2, 2016) PR #29: add possibility to specify a subscription id. ## 2.1.0 (February 23, 2016) Internal API change: made `Socket.emit` synchronous. ## 2.0.1 (February 14, 2016) Fixed npm distribution (`lib/` was not published being in `.gitignore`). ## 2.0.0 (February 14, 2016) ### Breaking changes * Distribute as individual modules in `lib` instead of bundle in `dist`. Should not break node consumers. Could break browserify and webpack consumers. Certainly breaks bower consumers (bower support has been removed) ### New features * Added method to disconnect * Added options to control auto-connect and auto-reconnect behaviour. As it turns out they could indeed be useful, for instance when one wants to simulate a connection scenario (e.g. in stress tests) and needs to have fine-grained control on the lifecycle of the connection. ## 1.1.0 (July 11, 2015) Moved the code to use ES6. In the process, I also refactored it a bit to use less "exotic" patterns, but there _should be_ no breaking changes to the public API. Two enhancements: 1. a `status` property (`connected` / `disconnected`) is now available on the instance 1. it's now possible to call methods `sub`, `unsub`, and `method` right after creating the instance. Calls are queued and performed after the `connected` event ## 1.0.0 (January 11, 2015) The library has been rewritten from scratch and its scope somewhat reduced. The purpose of the rewrite, other than simplification, was to implement better under-the-hood APIs to allow more flexibility. The biggest change is that the library no longer handles method and subscription calls. I.e., it doesn't take anymore callbacks to the `method` and `sub` methods. Rather it returns the `id` of those calls, and lets the consumer handle the `result`, `updated`, `ready`, `nosub` events related to those calls. My plan is to bake this functionality directly into Asteroid, which sometimes need to have lower level access to those events. Some options have been removed, namely `do_not_autoconnect`, `do_not_autoreconnect` and `socket_intercept_function`. The functionalities provided by the first two options can be recreated, but it requires meddling with the library internals (one has to re-define the `_init` method). I figured this wouldn't be a problem since I've never found a use case for them. The third functionality - i.e. intercepting the socket `send` method and doing something with the message that has been sent - is easily recreated by listening to the `message:in`, `message:out` private events of the `_socket` property of a DDP instance. Other private events are available on the property, making it easier to monitor and gather metrics about the WebSocket. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2016 mondora Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![npm version](https://badge.fury.io/js/ddp.js.svg)](https://badge.fury.io/js/ddp.js) [![Build Status](https://travis-ci.org/mondora/ddp.js.svg?branch=master)](https://travis-ci.org/mondora/ddp.js) [![Coverage Status](https://img.shields.io/coveralls/mondora/ddp.js.svg)](https://coveralls.io/r/mondora/ddp.js?branch=master) [![Dependency Status](https://david-dm.org/mondora/ddp.js.svg)](https://david-dm.org/mondora/ddp.js) [![devDependency Status](https://david-dm.org/mondora/ddp.js/dev-status.svg)](https://david-dm.org/mondora/ddp.js#info=devDependencies) # ddp.js A javascript isomorphic/universal ddp client. > ## Warning > `ddp.js@^2.0.0` is only distributed as an `npm` module instead of an UMD > bundle. Also, `bower` has been removed as a method of distribution. If you > need an UMD bundle or `bower` support, I'm open for suggestions to add back > those methods of distribution without polluting this repo. ## What is it for? The purpose of this library is: - to set up and maintain a ddp connection with a ddp server, freeing the developer from having to do it on their own - to give the developer a clear, consistent API to communicate with the ddp server ## Install To install ddp.js using `npm`: npm install ddp.js or using `yarn`: yarn add ddp.js ## Example usage ```js const DDP = require("ddp.js"); const options = { endpoint: "ws://localhost:3000/websocket", SocketConstructor: WebSocket }; const ddp = new DDP(options); ddp.on("connected", () => { console.log("Connected"); }); const subId = ddp.sub("mySubscription"); ddp.on("ready", message => { if (message.subs.includes(subId)) { console.log("mySubscription ready"); } }); ddp.on("added", message => { console.log(message.collection); }); const myLoginParams = { user: { email: "user@example.com" }, password: "hunter2" }; const methodId = ddp.method("login", [myLoginParams]); ddp.on("result", message => { if (message.id === methodId && !message.error) { console.log("Logged in!"); } }); ``` ## Developing After cloning the repository, install `npm` dependencies with `npm install`. Run `npm test` to run unit tests, or `npm run dev` to have `mocha` re-run your tests when source or test files change. To run e2e tests, first [install meteor](https://www.meteor.com/install). Then, start the meteor server with `npm run start-meteor`. Finally, run `npm run e2e-test` to run the e2e test suite, or `npm run e2e-dev` to have `mocha` re-run the suite when source or test files change. ## Public API ### new DDP(options) Creates a new DDP instance. After being constructed, the instance will establish a connection with the DDP server and will try to maintain it open. #### Arguments - `options` **object** *required* Available options are: - `endpoint` **string** *required*: the location of the websocket server. Its format depends on the type of socket you are using. - `SocketConstructor` **function** *required*: the constructor function that will be used to construct the socket. Meteor (currently the only DDP server available) supports websockets and SockJS sockets. So, practically speaking, this means that on the browser you can use either the browser's native WebSocket constructor or the SockJS constructor provided by the SockJS library. On the server you can use whichever library implements the websocket protocol (e.g. faye-websocket). - `autoConnect` **boolean** *optional* [default: `true`]: whether to establish the connection to the server upon instantiation. When `false`, one can manually establish the connection with the `connect` method. - `autoReconnect` **boolean** *optional* [default: `true`]: whether to try to reconnect to the server when the socket connection closes, unless the closing was initiated by a call to the `disconnect` method. - `reconnectInterval` **number** *optional* [default: 10000]: the interval in ms between reconnection attempts. #### Returns A new DDP instance, which is also an `EventEmitter` instance. --- ### DDP.method(name, params) Calls a remote method. #### Arguments - `name` **string** *required*: name of the method to call. - `params` **array** *required*: array of parameters to pass to the remote method. Pass an empty array if you do not wish to pass any parameters. #### Returns The unique `id` (string) corresponding to the method call. #### Example usage Server code: ```js Meteor.methods({ myMethod (param_0, param_1, param_2) { /* ... */ } }); ``` Client code: ```js const methodCallId = ddp.method("myMethod", [param_0, param_1, param_2]); ``` --- ### DDP.sub(name, params) Subscribes to a server publication. #### Arguments - `name` **string** *required*: name of the server publication. - `params` **array** *required*: array of parameters to pass to the server publish function. Pass an empty array if you do not wish to pass any parameters. #### Returns The unique `id` (string) corresponding to the subscription call. #### Example usage Server code: ```js Meteor.publish("myPublication", (param_0, param_1, param_2) { /* ... */ }); ``` Client code: ```js const subscriptionId = ddp.sub("myPublication", [param_0, param_1, param_2]); ``` --- ### DDP.unsub(id) Unsubscribes to a previously-subscribed server publication. #### Arguments - `id` **string** *required*: id of the subscription. #### Returns The `id` corresponding to the subscription call (not of much use, but I return it for consistency). --- ### DDP.connect() Connects to the ddp server. The method is called automatically by the class constructor if the `autoConnect` option is set to `true` (default behaviour). So there generally should be no need for the developer to call the method themselves. #### Arguments None #### Returns None --- ### DDP.disconnect() Disconnects from the ddp server by closing the `WebSocket` connection. You can listen on the `disconnected` event to be notified of the disconnection. #### Arguments None #### Returns None ## Public events ### Connection events - `connected`: emitted with no arguments when the DDP connection is established. - `disconnected`: emitted with no arguments when the DDP connection drops. ### Subscription events All the following events are emitted with one argument, the parsed DDP message. Further details can be found [on the DDP spec page](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md). - `ready` - `nosub` - `added` - `changed` - `removed` ### Method events All the following events are emitted with one argument, the parsed DDP message. Further details can be found [on the DDP spec page](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md). - `result` - `updated` ================================================ FILE: package.json ================================================ { "name": "ddp.js", "version": "2.2.1", "description": "ddp javascript client", "main": "lib/ddp.js", "scripts": { "build": "babel src --out-dir lib", "clean": "rimraf lib coverage", "coverage": "nyc --require babel-register --reporter=lcov --include src --all npm test", "coveralls": "cat ./coverage/lcov.info | coveralls", "dev": "npm test -- --watch", "lint": "eslint src test", "prepare": "npm run clean && npm run build", "test": "mocha --require babel-register --recursive test/unit", "start-meteor": "cd test/server/ && meteor", "e2e-test": "mocha --require babel-register --recursive test/e2e", "e2e-dev": "npm run e2e-test -- --watch" }, "repository": { "type": "git", "url": "https://github.com/mondora/ddp.js" }, "keywords": [ "ddp", "meteor", "asteroid" ], "author": "Paolo Scanferla ", "license": "MIT", "bugs": { "url": "https://github.com/mondora/ddp.js/issues" }, "homepage": "https://github.com/mondora/ddp.js", "devDependencies": { "babel-cli": "^6.26.0", "babel-eslint": "^8.0.1", "babel-plugin-istanbul": "^4.1.5", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-0": "^6.24.1", "babel-register": "^6.26.0", "chai": "^4.1.2", "coveralls": "^3.0.0", "eslint": "^4.9.0", "faye-websocket": "^0.11.1", "mocha": "^4.0.1", "nyc": "^11.2.1", "sinon": "^4.0.2", "sinon-chai": "^2.14.0" }, "dependencies": { "wolfy87-eventemitter": "^5.2.3" } } ================================================ FILE: src/ddp.js ================================================ import EventEmitter from "wolfy87-eventemitter"; import Queue from "./queue"; import Socket from "./socket"; import {contains, uniqueId} from "./utils"; const DDP_VERSION = "1"; const PUBLIC_EVENTS = [ // Subscription messages "ready", "nosub", "added", "changed", "removed", // Method messages "result", "updated", // Error messages "error" ]; const DEFAULT_RECONNECT_INTERVAL = 10000; export default class DDP extends EventEmitter { emit () { setTimeout(super.emit.bind(this, ...arguments), 0); } constructor (options) { super(); this.status = "disconnected"; // Default `autoConnect` and `autoReconnect` to true this.autoConnect = (options.autoConnect !== false); this.autoReconnect = (options.autoReconnect !== false); this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL; this.messageQueue = new Queue(message => { if (this.status === "connected") { this.socket.send(message); return true; } else { return false; } }); this.socket = new Socket(options.SocketConstructor, options.endpoint); this.socket.on("open", () => { // When the socket opens, send the `connect` message // to establish the DDP connection this.socket.send({ msg: "connect", version: DDP_VERSION, support: [DDP_VERSION] }); }); this.socket.on("close", () => { this.status = "disconnected"; this.messageQueue.empty(); this.emit("disconnected"); if (this.autoReconnect) { // Schedule a reconnection setTimeout( this.socket.open.bind(this.socket), this.reconnectInterval ); } }); this.socket.on("message:in", message => { if (message.msg === "connected") { this.status = "connected"; this.messageQueue.process(); this.emit("connected"); } else if (message.msg === "ping") { // Reply with a `pong` message to prevent the server from // closing the connection this.socket.send({msg: "pong", id: message.id}); } else if (contains(PUBLIC_EVENTS, message.msg)) { this.emit(message.msg, message); } }); if (this.autoConnect) { this.connect(); } } connect () { this.socket.open(); } disconnect () { /* * If `disconnect` is called, the caller likely doesn't want the * the instance to try to auto-reconnect. Therefore we set the * `autoReconnect` flag to false. */ this.autoReconnect = false; this.socket.close(); } method (name, params) { const id = uniqueId(); this.messageQueue.push({ msg: "method", id: id, method: name, params: params }); return id; } sub (name, params, id = null) { id || (id = uniqueId()); this.messageQueue.push({ msg: "sub", id: id, name: name, params: params }); return id; } unsub (id) { this.messageQueue.push({ msg: "unsub", id: id }); return id; } } ================================================ FILE: src/queue.js ================================================ export default class Queue { /* * As the name implies, `consumer` is the (sole) consumer of the queue. * It gets called with each element of the queue and its return value * serves as a ack, determining whether the element is removed or not from * the queue, allowing then subsequent elements to be processed. */ constructor (consumer) { this.consumer = consumer; this.queue = []; } push (element) { this.queue.push(element); this.process(); } process () { if (this.queue.length !== 0) { const ack = this.consumer(this.queue[0]); if (ack) { this.queue.shift(); this.process(); } } } empty () { this.queue = []; } } ================================================ FILE: src/socket.js ================================================ import EventEmitter from "wolfy87-eventemitter"; export default class Socket extends EventEmitter { constructor (SocketConstructor, endpoint) { super(); this.SocketConstructor = SocketConstructor; this.endpoint = endpoint; this.rawSocket = null; } send (object) { const message = JSON.stringify(object); this.rawSocket.send(message); // Emit a copy of the object, as the listener might mutate it. this.emit("message:out", JSON.parse(message)); } open () { /* * Makes `open` a no-op if there's already a `rawSocket`. This avoids * memory / socket leaks if `open` is called twice (e.g. by a user * calling `ddp.connect` twice) without properly disposing of the * socket connection. `rawSocket` gets automatically set to `null` only * when it goes into a closed or error state. This way `rawSocket` is * disposed of correctly: the socket connection is closed, and the * object can be garbage collected. */ if (this.rawSocket) { return; } this.rawSocket = new this.SocketConstructor(this.endpoint); /* * Calls to `onopen` and `onclose` directly trigger the `open` and * `close` events on the `Socket` instance. */ this.rawSocket.onopen = () => this.emit("open"); this.rawSocket.onclose = () => { this.rawSocket = null; this.emit("close"); }; /* * Calls to `onerror` trigger the `close` event on the `Socket` * instance, and cause the `rawSocket` object to be disposed of. * Since it's not clear what conditions could cause the error and if * it's possible to recover from it, we prefer to always close the * connection (if it isn't already) and dispose of the socket object. */ this.rawSocket.onerror = () => { // It's not clear what the socket lifecycle is when errors occurr. // Hence, to avoid the `close` event to be emitted twice, before // manually closing the socket we de-register the `onclose` // callback. delete this.rawSocket.onclose; // Safe to perform even if the socket is already closed this.rawSocket.close(); this.rawSocket = null; this.emit("close"); }; /* * Calls to `onmessage` trigger a `message:in` event on the `Socket` * instance only once the message (first parameter to `onmessage`) has * been successfully parsed into a javascript object. */ this.rawSocket.onmessage = message => { var object; try { object = JSON.parse(message.data); } catch (ignore) { // Simply ignore the malformed message and return return; } // Outside the try-catch block as it must only catch JSON parsing // errors, not errors that may occur inside a "message:in" event // handler this.emit("message:in", object); }; } close () { /* * Avoid throwing an error if `rawSocket === null` */ if (this.rawSocket) { this.rawSocket.close(); } } } ================================================ FILE: src/utils.js ================================================ var i = 0; export function uniqueId () { return (i++).toString(); } export function contains (array, element) { return array.indexOf(element) !== -1; } ================================================ FILE: test/e2e/connection.js ================================================ import chai, {expect} from "chai"; import {Client} from "faye-websocket"; import sinon from "sinon"; import sinonChai from "sinon-chai"; import DDP from "../../src/ddp"; chai.use(sinonChai); const options = { endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }; describe("connection", () => { var ddp = null; afterEach(done => { if (ddp) { ddp.on("disconnected", () => done()); ddp.disconnect(); ddp = null; } else { done(); } }); describe("connecting", () => { it("a connection is established on instantiation unless `autoConnect === false`", done => { /* * The test suceeds when the `connected` event is fired, signaling * the establishment of the connection. * If the event is never fired, the test times out and fails. */ ddp = new DDP(options); ddp.on("connected", () => { done(); }); }); it("a connection is not established on instantiation when `autoConnect === false`", done => { /* * The test succeeds if, 1s after the creation of the DDP instance, * a `connected` event has not been fired. */ const ddp = new DDP({ ...options, autoConnect: false }); const connectedHandler = sinon.spy(); ddp.on("connected", connectedHandler); setTimeout(() => { try { expect(connectedHandler).to.have.callCount(0); done(); } catch (e) { done(e); } }, 1000); }); it("a connection can be established manually when `autoConnect === false`", done => { /* * The test suceeds when the `connected` event is fired, signaling * the establishment of the connection. * If the event is never fired, the test times out and fails. */ ddp = new DDP({ ...options, autoConnect: false }); ddp.connect(); ddp.on("connected", () => { done(); }); }); it("manually connecting several times doesn't causes multiple simultaneous connections [CASE: `autoConnect === true`]", done => { /* * The test suceeds if 1s after having called `connect` several * times only one connection has been established. */ ddp = new DDP(options); const connectedSpy = sinon.spy(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.on("connected", connectedSpy); setTimeout(() => { try { expect(connectedSpy).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); it("manually connecting several times doesn't causes multiple simultaneous connections [CASE: `autoConnect === false`]", done => { /* * The test suceeds if 1s after having called `connect` several * times only one connection has been established. */ ddp = new DDP({ ...options, autoConnect: false }); const connectedSpy = sinon.spy(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.connect(); ddp.on("connected", connectedSpy); setTimeout(() => { try { expect(connectedSpy).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); }); describe("disconnecting", () => { it("the connection is closed when calling `disconnect`", done => { /* * The test suceeds when the `disconnected` event is fired, * signaling the termination of the connection. * If the event is never fired, the test times out and fails. */ const ddp = new DDP(options); ddp.on("connected", () => { ddp.disconnect(); }); ddp.on("disconnected", () => { done(); }); }); it("calling `disconnect` several times causes no issues", done => { /* * The test suceeds if: * - calling `disconnect` several times doesn't throw, both before * and after the `disconnected` event has been received * - one and only one `disconnected` event is fired (check after * 1s) */ const ddp = new DDP(options); const disconnectSpy = sinon.spy(() => { try { ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); } catch (e) { done(e); } }); ddp.on("connected", () => { try { ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); ddp.disconnect(); } catch (e) { done(e); } }); ddp.on("disconnected", disconnectSpy); setTimeout(() => { try { expect(disconnectSpy).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); it("the connection is closed when calling `disconnect` and it's not re-established", done => { /* * The test suceeds if, 1s after the `disconnected` event has been * fired, there hasn't been any reconnection. */ const ddp = new DDP({ ...options, reconnectInterval: 10 }); const disconnectOnConnection = sinon.spy(() => { ddp.disconnect(); }); ddp.on("connected", disconnectOnConnection); ddp.on("disconnected", () => { setTimeout(() => { try { expect(disconnectOnConnection).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); }); it("the connection is closed and re-established when the server closes the connection, unless `autoReconnect === true`", done => { /* * The test suceeds when the `connect` event is fired a second time * after the client gets disconnected from the server (occurrence * marked by the `disconnected` event). * If the event is never fired a second time, the test times out * and fails. */ ddp = new DDP({ ...options, reconnectInterval: 10 }); var callCount = 0; ddp.on("connected", () => { callCount += 1; if (callCount === 1) { ddp.method("disconnectMe", []); } if (callCount === 2) { done(); } }); }); it("the connection is closed and _not_ re-established when the server closes the connection and `autoReconnect === false`", done => { /* * The test suceeds if, 1s after the `disconnected` event has been * fired, there hasn't been any reconnection. */ const ddp = new DDP({ ...options, reconnectInterval: 10, autoReconnect: false }); const disconnectMe = sinon.spy(() => { ddp.method("disconnectMe", []); }); ddp.on("connected", disconnectMe); ddp.on("disconnected", () => { setTimeout(() => { try { expect(disconnectMe).to.have.callCount(1); done(); } catch (e) { done(e); } }, 1000); }); }); describe("ddp.js issue #22", () => { /* * We need to test that no `uncaughtException`-s are raised. Since * mocha adds a listener to the `uncaughtException` event which * causes tests to fail in an unexpected manner, we first remove * that listener, and then we restore it. Since it's not clear * _what_ mocha does with that listener, we try to lower the * meddling impact by doing all of our work inside the `it` block. */ it("no issues when sending messages while disconnected / while disconnecting", done => { /* * The test suceeds if, 100ms after the `disconnected` event * has been fired, there haven't been any uncaught exceptions. */ const catcher = sinon.spy(); const listeners = process.listeners("uncaughtException"); process.removeAllListeners("uncaughtException"); process.on("uncaughtException", catcher); const ddp = new DDP({ ...options, autoReconnect: false }); ddp.on("connected", () => { ddp.disconnect(); }); const interval = setInterval(() => { ddp.method("echo", []); }, 1); ddp.on("disconnected", () => { setTimeout(runAssertions, 100); }); const runAssertions = () => { clearInterval(interval); process.removeAllListeners("uncaughtException"); listeners.forEach(listener => { process.on("uncaughtException", listener); }); try { expect(catcher).to.have.callCount(0); done(); } catch (e) { done(e); } }; }); }); }); }); ================================================ FILE: test/e2e/methods.js ================================================ import {expect} from "chai"; import {Client} from "faye-websocket"; import DDP from "../../src/ddp"; describe("methods", () => { describe("calling a method", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("invokes the method on the server and gets a `result` message with the response", done => { /* * The test suceeds when the `result` message for the echo method * call is received, and assertions all succeed. * If the `result` message is never received, the test times out * and fails. Naturally, the test also fails if assertions fail. */ const methodId = ddp.method("echo", [0, 1, 2, 3, 4]); ddp.on("result", message => { if (message.id !== methodId || message.error) { return; } try { expect(message.result).to.deep.equal([0, 1, 2, 3, 4]); done(); } catch (e) { done(e); } }); }); it("gets an `updated` message", done => { /* * The test suceeds when the `updated` message for the echo method * call is received. * If the `updated` message is never received, the test times out * and fails. */ const methodId = ddp.method("echo", [0, 1, 2, 3, 4]); ddp.on("updated", message => { if (message.methods.indexOf(methodId) !== -1) { done(); } }); }); }); }); ================================================ FILE: test/e2e/subscriptions.js ================================================ import {expect} from "chai"; import {Client} from "faye-websocket"; import DDP from "../../src/ddp"; describe("subscriptions", () => { describe("subscribing to a publication", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("sends a sub call to the server and receives server-sent scubscription events", done => { /* * The test suceeds when the `ready` message for the echo * subscription is received, and assertions all succeed. * If the `ready` message is never received, the test times out * and fails. Naturally, the test also fails if assertions fail. */ const subId = ddp.sub("echo", [0, 1, 2, 3, 4]); const collections = {}; ddp.on("added", message => { collections[message.collection] = { ...collections[message.collection], [message.id]: { _id: message.id, ...message.fields } }; }); ddp.on("ready", message => { if (message.subs.indexOf(subId) === -1) { return; } try { expect(collections).to.deep.equal({ echoParameters: { "id_0": {_id: "id_0", param: 0}, "id_1": {_id: "id_1", param: 1}, "id_2": {_id: "id_2", param: 2}, "id_3": {_id: "id_3", param: 3}, "id_4": {_id: "id_4", param: 4} } }); done(); } catch (e) { done(e); } }); }); }); describe("unsubscribing from a publication", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("sends an unsub call to the server and receives a nosub unsubscriptions confirmation event", done => { /* * The test suceeds when the `nosub` message for the echo * subscription is received. If the `nosub` message is never * received, the test times out and fails. */ const subId = ddp.sub("echo", [0, 1, 2, 3, 4]); ddp.on("ready", message => { if (message.subs.indexOf(subId) === -1) { return; } ddp.unsub(subId); }); ddp.on("nosub", message => { if (message.id !== subId) { return; } done(); }); }); }); describe("getting unsubscribed from a publication", () => { const ddp = new DDP({ endpoint: "ws://localhost:3000/websocket", SocketConstructor: Client }); after(done => { ddp.on("disconnected", () => done()); ddp.disconnect(); }); it("receives a nosub unsubscriptions event", function (done) { /* * The test suceeds when the `nosub` message for the echo * subscription is received. If the `nosub` message is never * received, the test times out and fails. * The server will stop the subscription after about 1s, so there * is no need to terminate it with an `unsub`. We will however * increase the test timeout to 3s to account for the delay. */ this.timeout(3000); const subId = ddp.sub("autoTerminating"); var subReady = false; ddp.on("ready", message => { if (message.subs.indexOf(subId) !== -1) { subReady = true; } }); ddp.on("nosub", message => { if (message.id !== subId) { return; } try { // Ensure the subscription got marked as ready. expect(subReady).to.equal(true); done(); } catch (e) { done(e); } }); }); }); }); ================================================ FILE: test/server/.meteor/.finished-upgraders ================================================ # This file contains information which helps Meteor properly upgrade your # app when you run 'meteor update'. You should check it into version control # with your project. notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file notices-for-facebook-graph-api-2 1.2.0-standard-minifiers-package 1.2.0-meteor-platform-split 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package 1.4.0-remove-old-dev-bundle-link 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package ================================================ FILE: test/server/.meteor/.gitignore ================================================ local ================================================ FILE: test/server/.meteor/.id ================================================ # This file contains a token that is unique to your project. # Check it into your repository along with the rest of this directory. # It can be used for purposes such as: # - ensuring you don't accidentally deploy one app on top of another # - providing package authors with aggregated statistics ydh6g2121jjwyqq7agh ================================================ FILE: test/server/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # Check this file (and the other files in this directory) into your repository. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. meteor-base@1.1.0 # Packages every Meteor app needs to have mobile-experience@1.0.5 # Packages for a great mobile UX mongo@1.2.2 # The database Meteor supports right now blaze-html-templates # Compile .html files into Meteor Blaze views session@1.1.7 # Client-side reactive dictionary for your app jquery@1.11.10 # Helpful client-side library tracker@1.1.3 # Meteor's client-side reactive programming library es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. standard-minifier-css standard-minifier-js shell-server dynamic-import ================================================ FILE: test/server/.meteor/platforms ================================================ server browser ================================================ FILE: test/server/.meteor/release ================================================ METEOR@1.5.2.2 ================================================ FILE: test/server/.meteor/versions ================================================ allow-deny@1.0.9 autoupdate@1.3.12 babel-compiler@6.20.0 babel-runtime@1.0.1 base64@1.0.10 binary-heap@1.0.10 blaze@2.3.2 blaze-html-templates@1.1.2 blaze-tools@1.0.10 boilerplate-generator@1.2.0 caching-compiler@1.1.9 caching-html-compiler@1.1.2 callback-hook@1.0.10 check@1.2.5 ddp@1.3.1 ddp-client@2.1.3 ddp-common@1.2.9 ddp-server@2.0.2 deps@1.0.12 diff-sequence@1.0.7 dynamic-import@0.1.3 ecmascript@0.8.3 ecmascript-runtime@0.4.1 ecmascript-runtime-client@0.4.3 ecmascript-runtime-server@0.4.1 ejson@1.0.14 es5-shim@4.6.15 geojson-utils@1.0.10 hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 http@1.2.12 id-map@1.0.9 jquery@1.11.10 launch-screen@1.1.1 livedata@1.0.18 logging@1.1.19 meteor@1.7.2 meteor-base@1.1.0 minifier-css@1.2.16 minifier-js@2.1.4 minimongo@1.3.3 mobile-experience@1.0.5 mobile-status-bar@1.0.14 modules@0.10.0 modules-runtime@0.8.0 mongo@1.2.2 mongo-dev-server@1.0.1 mongo-id@1.0.6 npm-mongo@2.2.33 observe-sequence@1.0.16 ordered-dict@1.0.9 promise@0.9.0 random@1.0.10 reactive-dict@1.1.9 reactive-var@1.0.11 reload@1.1.11 retry@1.0.9 routepolicy@1.0.12 session@1.1.7 shell-server@0.2.4 spacebars@1.0.15 spacebars-compiler@1.1.3 standard-minifier-css@1.3.5 standard-minifier-js@2.1.2 templating@1.3.2 templating-compiler@1.3.3 templating-runtime@1.3.2 templating-tools@1.1.2 tracker@1.1.3 ui@1.0.13 underscore@1.0.10 url@1.1.0 webapp@1.3.19 webapp-hashing@1.0.9 ================================================ FILE: test/server/methods.js ================================================ Meteor.methods({ echo: function () { return _.toArray(arguments); }, disconnectMe: function () { this.connection.close(); } }); ================================================ FILE: test/server/publications.js ================================================ Meteor.publish("echo", function () { var self = this; _.each(arguments, function (param, index) { self.added("echoParameters", "id_" + index, {param: param}); }); self.ready(); }); Meteor.publish("autoTerminating", function () { var self = this; self.added("autoTerminating", "id", {}); self.ready(); setTimeout(function () { self.stop(); }, 1000); }); ================================================ FILE: test/unit/ddp.js ================================================ import chai, {expect} from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); import DDP from "../../src/ddp"; import Socket from "../../src/socket"; class SocketConstructorMock { send () {} close () {} } const options = { SocketConstructor: SocketConstructorMock }; describe("`DDP` class", () => { describe("`constructor` method", () => { beforeEach(() => { sinon.stub(Socket.prototype, "on"); sinon.stub(Socket.prototype, "open"); }); afterEach(() => { Socket.prototype.on.restore(); Socket.prototype.open.restore(); }); it("instantiates a `Socket`", () => { const ddp = new DDP(options); expect(ddp.socket).to.be.an.instanceOf(Socket); }); it("registers handlers for `socket` events", () => { const ddp = new DDP(options); expect(ddp.socket.on).to.have.always.been.calledWithMatch( sinon.match.string, sinon.match.func ); }); it("opens a connection to the server (by calling `socket.open`) unless `options.autoConnect === false`", () => { const ddp = new DDP(options); expect(ddp.socket.open).to.have.callCount(1); }); it("does not open a connection when `options.autoConnect === false`", () => { const ddp = new DDP({ ...options, autoConnect: false }); expect(ddp.socket.open).to.have.callCount(0); }); it("sets the instance `reconnectInterval` to `options.reconnectInterval` if specified", () => { const ddp = new DDP({ ...options, reconnectInterval: 1, autoConnect: false }); expect(ddp.reconnectInterval).to.equal(1); }); it("sets the instance `reconnectInterval` to a default value if `options.reconnectInterval` is not specified", () => { const ddp = new DDP({ ...options, autoConnect: false }); expect(ddp.reconnectInterval).to.equal(10000); }); }); describe("`method` method", () => { it("sends a DDP `method` message", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.method("name", ["param"]); expect(ddp.messageQueue.push).to.have.been.calledWith({ msg: "method", id: id, method: "name", params: ["param"] }); }); it("returns the method's `id`", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.method("name", ["param"]); expect(id).to.be.a("string"); }); }); describe("`sub` method", () => { it("sends a DDP `sub` message", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.sub("name", ["param"]); expect(ddp.messageQueue.push).to.have.been.calledWith({ msg: "sub", id: id, name: "name", params: ["param"] }); }); it("returns the sub's `id`", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.sub("name", ["param"]); expect(id).to.be.a("string"); }); it("generates unique id when not specified", () => { const ddp = new DDP(options); var ids = []; ids.push(ddp.sub("echo", [ 0 ])); ids.push(ddp.sub("echo", [ 0 ])); expect(ids[0]).to.be.a("string"); expect(ids[1]).to.be.a("string"); expect(ids[0]).not.to.equal(ids[1]); }); it("allows manually specifying sub's id", () => { const ddp = new DDP(options); const subId = ddp.sub("echo", [ 0 ], "12345"); expect(subId).to.equal("12345"); }); }); describe("`unsub` method", () => { it("sends a DDP `unsub` message", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.unsub("id"); expect(ddp.messageQueue.push).to.have.been.calledWith({ msg: "unsub", id: id }); }); it("returns the sub's `id`", () => { const ddp = new DDP(options); ddp.messageQueue.push = sinon.spy(); const id = ddp.unsub("id"); expect(id).to.be.a("string"); expect(id).to.equal("id"); }); }); describe("`connect` method", () => { beforeEach(() => { sinon.stub(Socket.prototype, "open"); }); afterEach(() => { Socket.prototype.open.restore(); }); it("opens the WebSocket connection", () => { const ddp = new DDP(options); Socket.prototype.open.reset(); ddp.connect(); expect(ddp.socket.open).to.have.callCount(1); }); }); describe("`disconnect` method", () => { beforeEach(() => { sinon.stub(Socket.prototype, "close"); }); afterEach(() => { Socket.prototype.close.restore(); }); it("closes the WebSocket connection", () => { const ddp = new DDP(options); ddp.disconnect(); expect(ddp.socket.close).to.have.callCount(1); }); it("sets the `autoReconnect` flag to false", () => { const ddp = new DDP(options); ddp.disconnect(); expect(ddp.autoReconnect).to.equal(false); }); }); describe("`socket` `open` handler", () => { beforeEach(() => { sinon.stub(global, "setTimeout").callsFake(fn => fn()); }); afterEach(() => { global.setTimeout.restore(); }); it("sends the `connect` DDP message", () => { const ddp = new DDP(options); ddp.socket.send = sinon.spy(); ddp.socket.emit("open"); expect(ddp.socket.send).to.have.been.calledWith({ msg: "connect", version: "1", support: ["1"] }); }); }); describe("`socket` `close` handler", () => { beforeEach(() => { sinon.stub(global, "setTimeout").callsFake(fn => fn()); }); afterEach(() => { global.setTimeout.restore(); }); it("emits the `disconnected` event", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); ddp.socket.emit("close"); expect(ddp.emit).to.have.been.calledWith("disconnected"); }); it("sets the status to `disconnected`", () => { const ddp = new DDP(options); ddp.status = "connected"; ddp.emit = sinon.spy(); ddp.socket.emit("close"); expect(ddp.status).to.equal("disconnected"); }); it("schedules a reconnection unless `options.autoReconnect === false`", () => { const ddp = new DDP(options); ddp.socket.open = sinon.spy(); ddp.socket.emit("close"); expect(ddp.socket.open).to.have.callCount(1); }); it("doesn't schedule a reconnection when `options.autoReconnect === false`", () => { const ddp = new DDP({ ...options, autoReconnect: false }); ddp.socket.open = sinon.spy(); ddp.socket.emit("close"); expect(ddp.socket.open).to.have.callCount(0); }); }); describe("`socket` `message:in` handler", () => { beforeEach(() => { sinon.stub(global, "setTimeout").callsFake(fn => fn()); }); afterEach(() => { global.setTimeout.restore(); }); it("responds to `ping` DDP messages", () => { const ddp = new DDP(options); ddp.socket.send = sinon.spy(); ddp.socket.emit("message:in", { id: "id", msg: "ping" }); expect(ddp.socket.send).to.have.been.calledWith({ id: "id", msg: "pong" }); }); it("triggers `messageQueue` processing upon connection", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); ddp.messageQueue.process = sinon.spy(); ddp.socket.emit("message:in", {msg: "connected"}); expect(ddp.messageQueue.process).to.have.callCount(1); }); it("sets the status to `connected` upon connection", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); ddp.socket.emit("message:in", {msg: "connected"}); expect(ddp.status).to.equal("connected"); }); it("emits public DDP messages as events", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); const message = { id: "id", msg: "result" }; ddp.socket.emit("message:in", message); expect(ddp.emit).to.have.been.calledWith("result", message); }); it("ignores unknown (or non public) DDP messages", () => { const ddp = new DDP(options); ddp.emit = sinon.spy(); const message = { id: "id", msg: "not-a-ddp-message" }; ddp.socket.emit("message:in", message); expect(ddp.emit).to.have.callCount(0); }); }); describe("`messageQueue` consumer", () => { it("acks if `status` is `connected`", () => { const ddp = new DDP(options); ddp.status = "connected"; const ack = ddp.messageQueue.consumer({}); expect(ack).to.equal(true); }); it("doesn't ack if `status` is `disconnected`", () => { const ddp = new DDP(options); ddp.status = "disconnected"; const ack = ddp.messageQueue.consumer({}); expect(ack).to.equal(false); }); }); }); ================================================ FILE: test/unit/queue.js ================================================ import chai, {expect} from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); import Queue from "../../src/queue"; describe("`Queue` class", () => { describe("`push` method", () => { it("adds an element to the queue", () => { const q = new Queue(); q.process = sinon.spy(); const element = {}; q.push(element); expect(q.queue).to.include(element); }); it("triggers processing", () => { const q = new Queue(); q.process = sinon.spy(); const element = {}; q.push(element); expect(q.process).to.have.callCount(1); }); }); describe("`process` method", () => { it("calls the consumer on each element of the queue", () => { const consumer = sinon.spy(() => true); const q = new Queue(consumer); q.queue = [0, 1, 2]; q.process(); expect(consumer).to.have.been.calledWith(0); expect(consumer).to.have.been.calledWith(1); expect(consumer).to.have.been.calledWith(2); expect(consumer).to.have.callCount(3); }); it("removes elements from the queue", () => { const consumer = sinon.spy(() => true); const q = new Queue(consumer); q.queue = [0, 1, 2]; q.process(); expect(q.queue.length).to.equal(0); }); it("doesn't remove elements from the queue if the consumer doesn't ack", () => { const consumer = sinon.spy(() => false); const q = new Queue(consumer); q.queue = [0, 1, 2]; q.process(); expect(consumer).to.have.been.calledWith(0); expect(consumer).to.have.callCount(1); expect(q.queue.length).to.equal(3); }); }); describe("`empty` method", () => { it("empties the queue", () => { const q = new Queue(); q.process = sinon.spy(); const element = {}; q.push(element); expect(q.queue.length).to.equal(1); q.empty(); expect(q.queue.length).to.equal(0); }); }); }); ================================================ FILE: test/unit/socket.js ================================================ import chai, {expect} from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; chai.use(sinonChai); import Socket from "../../src/socket"; class SocketConstructorMock { close () {} send () {} } describe("`Socket` class", () => { describe("`send` method", () => { it("sends a message through the `rawSocket`", () => { const socket = new Socket(); socket.rawSocket = { send: sinon.spy() }; socket.send({}); expect(socket.rawSocket.send).to.have.callCount(1); }); it("stringifies the object to send", () => { const socket = new Socket(); socket.rawSocket = { send: sinon.spy() }; const object = { a: "a" }; const expectedMessage = JSON.stringify(object); socket.send(object); expect(socket.rawSocket.send).to.have.been.calledWith(expectedMessage); }); it("emits a `message:out` event", () => { const socket = new Socket(); socket.rawSocket = { send: sinon.spy() }; socket.emit = sinon.spy(); const object = { a: "a" }; socket.send(object); expect(socket.emit).to.have.been.calledWith("message:out", object); }); }); describe("`open` method", () => { it("no-op if `rawSocket` is already defined", () => { const socket = new Socket(SocketConstructorMock); const rawSocket = {}; socket.rawSocket = rawSocket; socket.open(); // Test, for instance, that `rawSocket` has not been replaced. expect(socket.rawSocket).to.equal(rawSocket); }); it("instantiates a `SocketConstructor`", () => { const socket = new Socket(SocketConstructorMock); socket.open(); expect(socket.rawSocket).to.be.an.instanceOf(SocketConstructorMock); }); it("registers handlers for `rawSocket` events", () => { const socket = new Socket(SocketConstructorMock); socket.open(); expect(socket.rawSocket.onopen).to.be.a("function"); expect(socket.rawSocket.onclose).to.be.a("function"); expect(socket.rawSocket.onerror).to.be.a("function"); expect(socket.rawSocket.onmessage).to.be.a("function"); }); }); describe("`close` method", () => { it("closes the `rawSocket`", () => { const socket = new Socket(SocketConstructorMock); socket.open(); socket.rawSocket.close = sinon.spy(); socket.close(); expect(socket.rawSocket.close).to.have.callCount(1); }); it("doesn't throw if there's no `rawSocket`", () => { const socket = new Socket(SocketConstructorMock); const peacemaker = () => { socket.close(); }; expect(peacemaker).not.to.throw(); }); }); describe("`rawSocket` `onopen` handler", () => { it("emits an `open` event", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("open", handler); socket.open(); socket.rawSocket.onopen(); expect(handler).to.have.callCount(1); }); }); describe("`rawSocket` `onclose` handler", () => { it("emits a `close` event", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("close", handler); socket.open(); socket.rawSocket.onclose(); expect(handler).to.have.callCount(1); }); it("null-s the `rawSocket` property", () => { const socket = new Socket(SocketConstructorMock); socket.open(); socket.rawSocket.onclose(); expect(socket.rawSocket).to.equal(null); }); }); describe("`rawSocket` `onerror` handler", () => { it("closes `rawSocket` (by calling `rawSocket.close`)", () => { const socket = new Socket(SocketConstructorMock); socket.open(); const rawSocket = socket.rawSocket; rawSocket.close = sinon.spy(); socket.rawSocket.onerror(); expect(rawSocket.close).to.have.callCount(1); }); it("de-registers the `rawSocket.onclose` callback", () => { const socket = new Socket(SocketConstructorMock); socket.open(); const rawSocket = socket.rawSocket; expect(rawSocket).to.have.property("onclose"); socket.rawSocket.onerror(); expect(rawSocket).not.to.have.property("onclose"); }); it("emits a `close` event", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("close", handler); socket.open(); socket.rawSocket.onerror(); expect(handler).to.have.callCount(1); }); it("null-s the `rawSocket` property", () => { const socket = new Socket(SocketConstructorMock); socket.open(); socket.rawSocket.onerror(); expect(socket.rawSocket).to.equal(null); }); }); describe("`rawSocket` `onmessage` handler", () => { it("parses message data into an object", () => { const socket = new Socket(SocketConstructorMock); sinon.stub(JSON, "parse"); socket.open(); socket.rawSocket.onmessage({data: "message"}); expect(JSON.parse).to.have.been.calledWith("message"); JSON.parse.restore(); }); it("ignores malformed messages", () => { const socket = new Socket(SocketConstructorMock); sinon.stub(JSON, "parse").throws(); socket.open(); expect(socket.rawSocket.onmessage).not.to.throw(); JSON.parse.restore(); }); it("emits `message:in` events", () => { const socket = new Socket(SocketConstructorMock); const handler = sinon.spy(); socket.on("message:in", handler); socket.open(); socket.rawSocket.onmessage({data: JSON.stringify({a: "a"})}); expect(handler).to.have.callCount(1); expect(handler).to.have.been.calledWith({a: "a"}); }); }); }); ================================================ FILE: test/unit/utils.js ================================================ import {expect} from "chai"; import {contains, uniqueId} from "../../src/utils"; describe("`utils` object", () => { describe("`contains` function", () => { it("returns true if the first parameter contains the second parameter", () => { const array = ["element"]; const element = "element"; expect(contains(array, element)).to.equal(true); }); it("returns false if the first parameter doesn't contain the second parameter", () => { const array = ["element"]; const element = "different-element"; expect(contains(array, element)).to.equal(false); }); }); describe("`uniqueId` function", () => { it("should return a different string each time it's called", () => { const ret1 = uniqueId(); const ret2 = uniqueId(); expect(ret1).not.to.equal(ret2); }); }); });