diff --git a/packages/socket-mode/.nycrc.json b/packages/socket-mode/.nycrc.json
new file mode 100644
index 000000000..6c61b19dc
--- /dev/null
+++ b/packages/socket-mode/.nycrc.json
@@ -0,0 +1,14 @@
+{
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "**/*.spec.js"
+ ],
+ "reporter": ["lcov"],
+ "extension": [
+ ".ts"
+ ],
+ "all": false,
+ "cache": true
+}
diff --git a/packages/socket-mode/README.md b/packages/socket-mode/README.md
index a67114c46..2ec524a4c 100644
--- a/packages/socket-mode/README.md
+++ b/packages/socket-mode/README.md
@@ -11,7 +11,7 @@ $ npm install @slack/socket-mode
## Usage
These examples show the most common features of `Socket Mode`. You'll find even more extensive [documentation on the
-package's website](https://slack.dev/node-slack-sdk/socket-mode) and our [api site](https://api.slack.com/socket-mode).
+package's website](https://slack.dev/node-slack-sdk/socket-mode) and our [api site][socket-mode].
@@ -19,9 +19,9 @@ package's website](https://slack.dev/node-slack-sdk/socket-mode) and our [api si
### Initialize the client
-This package is designed to support [**Socket Mode**](https://api.slack.com/socket-mode), which allows your app to receive events from Slack over a WebSocket connection.
+This package is designed to support [**Socket Mode**][socket-mode], which allows your app to receive events from Slack over a WebSocket connection.
-The package exports a `SocketModeClient` class. Your app will create an instance of the class for each workspace it communicates with. Creating an instance requires an **app-level token** from Slack. Apps connect to the **Socket Mode** API using an **app-level token**, which starts with `xapp`.
+The package exports a `SocketModeClient` class. Your app will create an instance of the class for each workspace it communicates with. Creating an instance requires an [**app-level token**][app-token] from Slack. Apps connect to the **Socket Mode** API using an [**app-level token**][app-token], which starts with `xapp`.
Note: **Socket Mode** requires the `connections:write` scope. Navigate to your [app configuration](https://api.slack.com/apps) and go to the **OAuth and Permissions** section to add the scope.
@@ -83,7 +83,7 @@ socketModeClient.on('message', (event) => {
### Send a message
-To respond to events and send messages back into Slack, we recommend using the `@slack/web-api` package with a `bot token`.
+To respond to events and send messages back into Slack, we recommend using the `@slack/web-api` package with a [bot token](https://api.slack.com/authentication/token-types#bot).
```javascript
const { SocketModeClient } = require('@slack/socket-mode');
@@ -140,7 +140,6 @@ In the table below, the client's states are listed, which are also the names of
| `connecting` | | The client is in the process of connecting to the platform. |
| `authenticated` | `(connectData)` - the response from `apps.connections.open` | The client has authenticated with the platform. This is a sub-state of `connecting`. |
| `connected` | | The client is connected to the platform and incoming events will start being emitted. |
-| `ready` | | The client is ready to send outgoing messages. This is a sub-state of `connected` |
| `disconnecting` | | The client is no longer connected to the platform and cleaning up its resources. It will soon transition to `disconnected`. |
| `reconnecting` | | The client is no longer connected to the platform and cleaning up its resources. It will soon transition to `connecting`. |
| `disconnected` | `(error)` | The client is not connected to the platform. This is a steady state - no attempt to connect is occurring. The `error` argument will be `undefined` when the client initiated the disconnect (normal). |
@@ -182,10 +181,11 @@ All the log levels, in order of most to least information are: `DEBUG`, `INFO`,
Sending log output somewhere besides the console
-You can also choose to have logs sent to a custom logger using the `logger` option. A custom logger needs to implement specific methods (known as the `Logger` interface):
+You can also choose to have logs sent to a custom logger using the `logger` option. A custom logger needs to implement specific methods (known as the `Logger` interface, see the [`@slack/logger` package](https://www.npmjs.com/package/@slack/logger) for details). A minimal interface should implement the following methods:
| Method | Parameters | Return type |
|--------------|-------------------|-------------|
+| `getLevel()` | n/a | `LogLevel` |
| `setLevel()` | `level: LogLevel` | `void` |
| `setName()` | `name: string` | `void` |
| `debug()` | `...msgs: any[]` | `void` |
@@ -207,6 +207,7 @@ const socketModeClient = new SocketModeClient({
info(...msgs): { logWritable.write('info: ' + JSON.stringify(msgs)); },
warn(...msgs): { logWritable.write('warn: ' + JSON.stringify(msgs)); },
error(...msgs): { logWritable.write('error: ' + JSON.stringify(msgs)); },
+ getLevel(): { return 'info'; },
setLevel(): { },
setName(): { },
},
@@ -222,7 +223,7 @@ const socketModeClient = new SocketModeClient({
## Requirements
-This package supports Node v14 and higher. It's highly recommended to use [the latest LTS version of
+This package supports Node v18 and higher. It's highly recommended to use [the latest LTS version of
node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version.
## Getting Help
@@ -230,3 +231,6 @@ node](https://github.com/nodejs/Release#release-schedule), and the documentation
If you get stuck, we're here to help. The following are the best ways to get assistance working through your issue:
* [Issue Tracker](http://github.com/slackapi/node-slack-sdk/issues) for questions, feature requests, bug reports and general discussion related to these packages. Try searching before you create a new issue.
+
+[socket-mode]: https://api.slack.com/apis/connections/socket
+[app-token]: https://api.slack.com/authentication/token-types#app
diff --git a/packages/socket-mode/package.json b/packages/socket-mode/package.json
index 983153ab1..d5177f667 100644
--- a/packages/socket-mode/package.json
+++ b/packages/socket-mode/package.json
@@ -24,8 +24,8 @@
"dist/**/*"
],
"engines": {
- "node": ">=12.13.0",
- "npm": ">=6.12.0"
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
},
"repository": "slackapi/node-slack-sdk",
"homepage": "https://slack.dev/node-slack-sdk/socket-mode",
@@ -40,40 +40,40 @@
"build": "npm run build:clean && tsc",
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"lint": "eslint --ext .ts src",
- "test": "npm run lint && npm run build && nyc mocha --config .mocharc.json src/*.spec.js",
+ "mocha": "mocha --config .mocharc.json src/*.spec.js",
+ "test:integration": "mocha --config .mocharc.json test/integration.spec.js",
+ "test": "npm run lint && npm run build && nyc --reporter=text-summary npm run mocha && npm run test:integration",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
},
"dependencies": {
- "@slack/logger": "^3.0.0",
- "@slack/web-api": "^6.11.2",
- "@types/node": ">=12.0.0",
- "@types/p-queue": "^2.3.2",
- "@types/ws": "^7.4.7",
- "eventemitter3": "^3.1.0",
+ "@slack/logger": "^4",
+ "@slack/web-api": "^7.0.1",
+ "@types/node": ">=18",
+ "@types/ws": "^8",
+ "eventemitter3": "^5",
"finity": "^0.5.4",
- "p-cancelable": "^1.1.0",
- "p-queue": "^2.4.2",
- "ws": "^7.5.3"
+ "ws": "^8"
},
"devDependencies": {
- "@types/chai": "^4.3.5",
- "@types/mocha": "^10.0.1",
- "@typescript-eslint/eslint-plugin": "^6.4.1",
- "@typescript-eslint/parser": "^6.4.0",
- "chai": "^4.3.8",
- "eslint": "^8.47.0",
- "eslint-config-airbnb-base": "^15.0.0",
- "eslint-config-airbnb-typescript": "^17.1.0",
- "eslint-plugin-import": "^2.28.1",
+ "@types/chai": "^4",
+ "@types/mocha": "^10",
+ "@types/sinon": "^17",
+ "@typescript-eslint/eslint-plugin": "^6",
+ "@typescript-eslint/parser": "^6",
+ "chai": "^4",
+ "eslint": "^8",
+ "eslint-config-airbnb-base": "^15",
+ "eslint-config-airbnb-typescript": "^17",
+ "eslint-plugin-import": "^2",
"eslint-plugin-import-newlines": "^1.3.4",
- "eslint-plugin-jsdoc": "^46.5.0",
- "eslint-plugin-node": "^11.1.0",
- "mocha": "^10.2.0",
- "nyc": "^15.1.0",
+ "eslint-plugin-jsdoc": "^48",
+ "eslint-plugin-node": "^11",
+ "mocha": "^10",
+ "nyc": "^15",
"shx": "^0.3.2",
- "sinon": "^15.2.0",
+ "sinon": "^17",
"source-map-support": "^0.5.21",
- "ts-node": "^10.8.1",
- "typescript": "^4.1.0"
+ "ts-node": "^10",
+ "typescript": "5.3.3"
}
}
diff --git a/packages/socket-mode/src/SocketModeClient.spec.js b/packages/socket-mode/src/SocketModeClient.spec.js
index dfba56a3f..325eced9b 100644
--- a/packages/socket-mode/src/SocketModeClient.spec.js
+++ b/packages/socket-mode/src/SocketModeClient.spec.js
@@ -19,7 +19,7 @@ describe('SocketModeClient', () => {
});
describe('slash_commands messages', () => {
- const message = {
+ const message = Buffer.from(JSON.stringify({
"envelope_id": "1d3c79ab-0ffb-41f3-a080-d19e85f53649",
"payload": {
"token": "verification-token",
@@ -37,9 +37,9 @@ describe('SocketModeClient', () => {
},
"type": "slash_commands",
"accepts_response_payload": true
- };
+ }));
- it('should be sent to two listeners', async () => {
+ it('should be sent to both slash_commands and slack_event listeners', async () => {
const client = new SocketModeClient({ appToken: 'xapp-' });
let commandListenerCalled = false;
client.on("slash_commands", async (args) => {
@@ -52,7 +52,7 @@ describe('SocketModeClient', () => {
&& args.retry_num === undefined
&& args.retry_reason === undefined;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.isTrue(commandListenerCalled);
assert.isTrue(slackEventListenerCalled);
@@ -64,7 +64,7 @@ describe('SocketModeClient', () => {
client.on("slash_commands", async ({ envelope_id }) => {
passedEnvelopeId = envelope_id;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.equal(passedEnvelopeId, '1d3c79ab-0ffb-41f3-a080-d19e85f53649');
});
@@ -74,14 +74,14 @@ describe('SocketModeClient', () => {
client.on("slack_event", async ({ envelope_id }) => {
passedEnvelopeId = envelope_id;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.equal(passedEnvelopeId, '1d3c79ab-0ffb-41f3-a080-d19e85f53649');
});
});
describe('events_api messages', () => {
- const message = {
+ const message = Buffer.from(JSON.stringify({
"envelope_id": "cda4159a-72a5-4744-aba3-4d66eb52682b",
"payload": {
"token": "verification-token",
@@ -133,12 +133,12 @@ describe('SocketModeClient', () => {
"accepts_response_payload": false,
"retry_attempt": 2,
"retry_reason": "timeout"
- };
+ }));
- it('should be sent to two listeners', async () => {
+ it('should be sent to the specific and generic event listeners, and should not trip an unrelated event listener', async () => {
const client = new SocketModeClient({ appToken: 'xapp-' });
let otherListenerCalled = false;
- client.on("app_home_opend", async () => {
+ client.on("app_home_opened", async () => {
otherListenerCalled = true;
});
let eventsApiListenerCalled = false;
@@ -154,7 +154,7 @@ describe('SocketModeClient', () => {
&& args.retry_num === 2
&& args.retry_reason === 'timeout';
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.isFalse(otherListenerCalled);
assert.isTrue(eventsApiListenerCalled);
@@ -167,7 +167,7 @@ describe('SocketModeClient', () => {
client.on("app_mention", async ({ envelope_id }) => {
passedEnvelopeId = envelope_id;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.equal(passedEnvelopeId, 'cda4159a-72a5-4744-aba3-4d66eb52682b');
});
@@ -177,14 +177,14 @@ describe('SocketModeClient', () => {
client.on("slack_event", async ({ envelope_id }) => {
passedEnvelopeId = envelope_id;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.equal(passedEnvelopeId, 'cda4159a-72a5-4744-aba3-4d66eb52682b');
});
});
describe('interactivity messages', () => {
- const message = {
+ const message = Buffer.from(JSON.stringify({
"envelope_id": "57d6a792-4d35-4d0b-b6aa-3361493e1caf",
"payload": {
"type": "shortcut",
@@ -206,9 +206,9 @@ describe('SocketModeClient', () => {
},
"type": "interactive",
"accepts_response_payload": false
- };
+ }));
- it('should be sent to two listeners', async () => {
+ it('should be sent to the specific and generic event type listeners, and should not trip an unrelated event listener', async () => {
const client = new SocketModeClient({ appToken: 'xapp-' });
let otherListenerCalled = false;
client.on("slash_commands", async () => {
@@ -222,7 +222,7 @@ describe('SocketModeClient', () => {
client.on("slack_event", async (args) => {
slackEventListenerCalled = args.ack !== undefined && args.body !== undefined;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.isFalse(otherListenerCalled);
assert.isTrue(interactiveListenerCalled);
@@ -235,7 +235,7 @@ describe('SocketModeClient', () => {
client.on("interactive", async ({ envelope_id }) => {
passedEnvelopeId = envelope_id;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.equal(passedEnvelopeId, '57d6a792-4d35-4d0b-b6aa-3361493e1caf');
});
@@ -245,7 +245,7 @@ describe('SocketModeClient', () => {
client.on("slack_event", async ({ envelope_id }) => {
passedEnvelopeId = envelope_id;
});
- await client.onWebSocketMessage({ data: JSON.stringify(message) });
+ await client.onWebSocketMessage(message);
await sleep(30);
assert.equal(passedEnvelopeId, '57d6a792-4d35-4d0b-b6aa-3361493e1caf');
});
diff --git a/packages/socket-mode/src/SocketModeClient.ts b/packages/socket-mode/src/SocketModeClient.ts
index b3af15a11..67b899c36 100644
--- a/packages/socket-mode/src/SocketModeClient.ts
+++ b/packages/socket-mode/src/SocketModeClient.ts
@@ -134,8 +134,8 @@ export class SocketModeClient extends EventEmitter {
/**
* Start a Socket Mode session app.
- * It may take a few milliseconds before being connected.
- * This method must be called before any messages can be sent or received.
+ * This method must be called before any messages can be sent or received,
+ * or to disconnect the client via the `disconnect` method.
*/
public start(): Promise {
this.logger.debug('Starting a Socket Mode client ...');
@@ -143,12 +143,12 @@ export class SocketModeClient extends EventEmitter {
this.stateMachine.handle(Event.Start);
// Return a promise that resolves with the connection information
return new Promise((resolve, reject) => {
- this.once(ConnectingState.Authenticated, (result) => {
+ this.once(State.Connected, (result) => {
this.removeListener(State.Disconnected, reject);
resolve(result);
});
this.once(State.Disconnected, (err) => {
- this.removeListener(ConnectingState.Authenticated, resolve);
+ this.removeListener(State.Connected, resolve);
reject(err);
});
});
@@ -433,7 +433,7 @@ export class SocketModeClient extends EventEmitter {
private async retrieveWSSURL(): Promise {
try {
this.logger.debug('Going to retrieve a new WSS URL ...');
- return await this.webClient.apps.connections.open();
+ return await this.webClient.apps.connections.open({});
} catch (error) {
this.logger.error(`Failed to retrieve a new WSS URL for reconnection (error: ${error})`);
throw error;
@@ -525,17 +525,17 @@ export class SocketModeClient extends EventEmitter {
websocket.addEventListener('open', (event) => {
this.stateMachine.handle(Event.WebSocketOpen, event);
});
- websocket.addEventListener('close', (event) => {
- this.stateMachine.handle(Event.WebSocketClose, event);
- });
websocket.addEventListener('error', (event) => {
this.logger.error(`A WebSocket error occurred: ${event.message}`);
this.emit('error', websocketErrorWithOriginal(event.error));
});
- websocket.addEventListener('message', this.onWebSocketMessage.bind(this));
+ websocket.on('message', this.onWebSocketMessage.bind(this));
+ websocket.on('close', (code: number, _data: Buffer) => {
+ this.stateMachine.handle(Event.WebSocketClose, code);
+ });
// Confirm WebSocket connection is still active
- websocket.addEventListener('ping', ((data: Buffer) => {
+ websocket.on('ping', ((data: Buffer) => {
if (this.pingPongLoggingEnabled) {
this.logger.debug(`Received ping from Slack server (data: ${data})`);
}
@@ -544,7 +544,7 @@ export class SocketModeClient extends EventEmitter {
// we cast this function to any as a workaround
}) as any); // eslint-disable-line @typescript-eslint/no-explicit-any
- websocket.addEventListener('pong', ((data: Buffer) => {
+ websocket.on('pong', ((data: Buffer) => {
if (this.pingPongLoggingEnabled) {
this.logger.debug(`Received pong from Slack server (data: ${data})`);
}
@@ -694,8 +694,12 @@ export class SocketModeClient extends EventEmitter {
* `onmessage` handler for the client's WebSocket.
* This will parse the payload and dispatch the relevant events for each incoming message.
*/
- protected async onWebSocketMessage({ data }: { data: string }): Promise {
- this.logger.debug(`Received a message on the WebSocket: ${data}`);
+ protected async onWebSocketMessage(data: WebSocket.RawData, isBinary: boolean): Promise {
+ if (isBinary) {
+ this.logger.debug('Unexpected binary message received, ignoring.');
+ return;
+ }
+ this.logger.debug(`Received a message on the WebSocket: ${data.toString()}`);
// Parse message into slack event
let event: {
@@ -710,12 +714,12 @@ export class SocketModeClient extends EventEmitter {
};
try {
- event = JSON.parse(data);
+ event = JSON.parse(data.toString());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (parseError: any) {
// Prevent application from crashing on a bad message, but log an error to bring attention
- this.logger.error(
- `Unable to parse an incoming WebSocket message: ${parseError.message}`,
+ this.logger.debug(
+ `Unable to parse an incoming WebSocket message (will ignore): ${parseError.message}, ${data.toString()}`,
);
return;
}
diff --git a/packages/socket-mode/test/integration.spec.js b/packages/socket-mode/test/integration.spec.js
new file mode 100644
index 000000000..c61504c6b
--- /dev/null
+++ b/packages/socket-mode/test/integration.spec.js
@@ -0,0 +1,161 @@
+const { assert } = require('chai');
+const { SocketModeClient} = require('../src/SocketModeClient');
+const { LogLevel } = require('../src/logger');
+const { WebSocketServer} = require('ws');
+const { createServer } = require('http');
+const sinon = require('sinon');
+
+const HTTP_PORT = 12345;
+const WSS_PORT = 23456;
+// Basic HTTP server to 'fake' behaviour of `apps.connections.open` endpoint
+const server = createServer((req, res) => {
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({
+ ok: true,
+ url: `ws://localhost:${WSS_PORT}/`,
+ }));
+});
+
+// Basic WS server to fake slack WS endpoint
+let wss = null;
+let exposed_ws_connection = null;
+
+// Socket mode client pointing to the above two posers
+let client = null;
+
+describe('Integration tests with a WebSocket server', () => {
+ beforeEach(() => {
+ server.listen(HTTP_PORT);
+ wss = new WebSocketServer({ port: WSS_PORT });
+ wss.on('connection', (ws) => {
+ ws.on('error', (err) => {
+ assert.fail(err);
+ });
+ // Send `Event.ServerHello`
+ ws.send(JSON.stringify({type: 'hello'}));
+ exposed_ws_connection = ws;
+ });
+ });
+ afterEach(() => {
+ server.close();
+ wss.close();
+ exposed_ws_connection = null;
+ client = null;
+ });
+ describe('establishing connection, receiving valid messages', () => {
+ beforeEach(() => {
+ client = new SocketModeClient({ appToken: 'whatever', logLevel: LogLevel.ERROR, clientOptions: {
+ slackApiUrl: `http://localhost:${HTTP_PORT}/`
+ }});
+ });
+ it('connects to a server via `start()` and gracefully disconnects via `disconnect()`', async () => {
+ await client.start();
+ await client.disconnect();
+ });
+ it('can listen on random event types and receive payload properties', async () => {
+ client.on('connected', () => {
+ exposed_ws_connection.send(JSON.stringify({
+ type: 'integration-test',
+ envelope_id: 12345,
+ }));
+ });
+ await client.start();
+ await new Promise((res, _rej) => {
+ client.on('integration-test', (evt) => {
+ assert.equal(evt.envelope_id, 12345);
+ res();
+ });
+ });
+ await client.disconnect();
+ });
+ });
+ describe('failure modes / unexpected messages sent to client', () => {
+ let debugLoggerSpy = sinon.stub();
+ const noop = () => {};
+ beforeEach(() => {
+ client = new SocketModeClient({ appToken: 'whatever', clientOptions: {
+ slackApiUrl: `http://localhost:${HTTP_PORT}/`
+ }, logger: {
+ debug: debugLoggerSpy,
+ info: noop,
+ error: noop,
+ getLevel: () => 'debug',
+ }});
+ });
+ afterEach(() => {
+ debugLoggerSpy.resetHistory();
+ });
+ it('should ignore binary messages', async () => {
+ client.on('connected', () => {
+ exposed_ws_connection.send(null);
+ });
+ await client.start();
+ await sleep(10);
+ assert.isTrue(debugLoggerSpy.calledWith(sinon.match('Unexpected binary message received')));
+ await client.disconnect();
+ });
+ it('should debug-log that a malformed JSON message was received', async () => {
+ client.on('connected', () => {
+ exposed_ws_connection.send('');
+ });
+ await client.start();
+ await sleep(10);
+ assert.isTrue(debugLoggerSpy.calledWith(sinon.match('Unable to parse an incoming WebSocket message')));
+ await client.disconnect();
+ });
+ });
+ describe('lifecycle events', () => {
+ beforeEach(() => {
+ client = new SocketModeClient({ appToken: 'whatever', logLevel: LogLevel.ERROR, clientOptions: {
+ slackApiUrl: `http://localhost:${HTTP_PORT}/`
+ }});
+ });
+ it('raises connecting event during `start()`', async () => {
+ let raised = false;
+ client.on('connecting', () => { raised = true; });
+ await client.start();
+ assert.isTrue(raised);
+ await client.disconnect();
+ });
+ it('raises authenticated event during `start()`', async () => {
+ let raised = false;
+ client.on('authenticated', () => { raised = true; });
+ await client.start();
+ assert.isTrue(raised);
+ await client.disconnect();
+ });
+ it('raises connected event during `start()`', async () => {
+ let raised = false;
+ client.on('connected', () => { raised = true; });
+ await client.start();
+ assert.isTrue(raised);
+ await client.disconnect();
+ });
+ it('raises disconnecting event during `disconnect()`', async () => {
+ let raised = false;
+ client.on('disconnecting', () => { raised = true; });
+ await client.start();
+ await client.disconnect();
+ assert.isTrue(raised);
+ });
+ it.skip('raises reconnecting event after `disconnect()`', async () => {
+ // TODO: doesnt seem to work, maybe need to set up client with reconnecting configuration
+ let raised = false;
+ client.on('reconnecting', () => { raised = true; });
+ await client.start();
+ await client.disconnect();
+ assert.isTrue(raised);
+ });
+ it('raises disconnected event after `disconnect()`', async () => {
+ let raised = false;
+ client.on('disconnected', () => { raised = true; });
+ await client.start();
+ await client.disconnect();
+ assert.isTrue(raised);
+ });
+ });
+});
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}