├── .babelrc
├── .npmignore
├── .gitignore
├── src
├── helpers
│ ├── dedupe.js
│ ├── normalize-send.js
│ ├── byte-length.js
│ ├── global-object.js
│ ├── logger.js
│ ├── array-helpers.js
│ ├── delay.js
│ ├── protocol-verification.js
│ ├── url-verification.js
│ └── proxy-factory.js
├── index.js
├── event
│ ├── prototype.js
│ ├── event.js
│ ├── close.js
│ ├── message.js
│ ├── factory.js
│ └── target.js
├── constants.js
├── algorithms
│ └── close.js
├── network-bridge.js
├── server.js
├── websocket.js
└── socket-io.js
├── tests
├── functional
│ ├── js-dom.html
│ ├── js-dom.test.js
│ ├── loader.test.js
│ ├── close-algorithm.test.js
│ ├── socket-io.test.js
│ └── websockets.test.js
├── .eslintrc
├── issues
│ ├── 157.test.js
│ ├── 19.test.js
│ ├── 64.test.js
│ ├── 13.test.js
│ ├── 65.test.js
│ ├── 242.test.js
│ └── 143.test.js
└── unit
│ ├── protocol-verification.test.js
│ ├── url-verification.test.js
│ ├── websocket.test.js
│ ├── event-target.test.js
│ ├── socket-io.test.js
│ ├── factory.test.js
│ ├── server.test.js
│ └── network-bridge.test.js
├── rollup.config.js
├── .eslintrc
├── .github
└── workflows
│ └── ci.yml
├── LICENSE.txt
├── package.json
├── CHANGELOG.md
├── index.d.ts
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "latest"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | tests
2 | .github
3 | .git
4 | .idea
5 | src
6 |
7 | .babelrc
8 | .eslintrc
9 | .npmignore
10 | rollup.config.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Folders to exclude
2 | dist
3 | node_modules
4 |
5 | # Individual files to exclude
6 | .DS_Store
7 | npm-debug.log
8 | yarn-error.log
9 | *.orig
10 |
--------------------------------------------------------------------------------
/src/helpers/dedupe.js:
--------------------------------------------------------------------------------
1 | export default arr =>
2 | arr.reduce((deduped, b) => {
3 | if (deduped.indexOf(b) > -1) return deduped;
4 | return deduped.concat(b);
5 | }, []);
6 |
--------------------------------------------------------------------------------
/src/helpers/normalize-send.js:
--------------------------------------------------------------------------------
1 | export default function normalizeSendData(data) {
2 | if (Object.prototype.toString.call(data) !== '[object Blob]' && !(data instanceof ArrayBuffer)) {
3 | data = String(data);
4 | }
5 |
6 | return data;
7 | }
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import MockServer from './server';
2 | import MockSocketIO from './socket-io';
3 | import MockWebSocket from './websocket';
4 |
5 | export const Server = MockServer;
6 | export const WebSocket = MockWebSocket;
7 | export const SocketIO = MockSocketIO;
8 |
--------------------------------------------------------------------------------
/tests/functional/js-dom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | JSDOM Test
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/helpers/byte-length.js:
--------------------------------------------------------------------------------
1 | export default function lengthInUtf8Bytes(str) {
2 | // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence.
3 | const m = encodeURIComponent(str).match(/%[89ABab]/g);
4 | return str.length + (m ? m.length : 0);
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/global-object.js:
--------------------------------------------------------------------------------
1 | export default function retrieveGlobalObject() {
2 | if (typeof window !== 'undefined') {
3 | return window;
4 | }
5 |
6 | return typeof process === 'object' && typeof require === 'function' && typeof global === 'object' ? global : this;
7 | }
8 |
--------------------------------------------------------------------------------
/src/helpers/logger.js:
--------------------------------------------------------------------------------
1 | export default function log(method, message) {
2 | /* eslint-disable no-console */
3 | if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'test') {
4 | console[method].call(null, message);
5 | }
6 | /* eslint-enable no-console */
7 | }
8 |
--------------------------------------------------------------------------------
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "it": true,
4 | "describe": true,
5 | "afterEach": true
6 | },
7 | "rules": {
8 | "no-unused-vars": 0,
9 | "no-use-before-define": 0,
10 | "no-new": 0,
11 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/event/prototype.js:
--------------------------------------------------------------------------------
1 | export default class EventPrototype {
2 | // Noops
3 | stopPropagation() {}
4 | stopImmediatePropagation() {}
5 |
6 | // if no arguments are passed then the type is set to "undefined" on
7 | // chrome and safari.
8 | initEvent(type = 'undefined', bubbles = false, cancelable = false) {
9 | this.type = `${type}`;
10 | this.bubbles = Boolean(bubbles);
11 | this.cancelable = Boolean(cancelable);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers/array-helpers.js:
--------------------------------------------------------------------------------
1 | export function reject(array = [], callback) {
2 | const results = [];
3 | array.forEach(itemInArray => {
4 | if (!callback(itemInArray)) {
5 | results.push(itemInArray);
6 | }
7 | });
8 |
9 | return results;
10 | }
11 |
12 | export function filter(array = [], callback) {
13 | const results = [];
14 | array.forEach(itemInArray => {
15 | if (callback(itemInArray)) {
16 | results.push(itemInArray);
17 | }
18 | });
19 |
20 | return results;
21 | }
22 |
--------------------------------------------------------------------------------
/src/helpers/delay.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This delay allows the thread to finish assigning its on* methods
3 | * before invoking the delay callback. This is purely a timing hack.
4 | * http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html
5 | *
6 | * @param {callback: function} the callback which will be invoked after the timeout
7 | * @parma {context: object} the context in which to invoke the function
8 | */
9 | export default function delay(callback, context) {
10 | setTimeout(timeoutContext => callback.call(timeoutContext), 4, context);
11 | }
12 |
--------------------------------------------------------------------------------
/tests/functional/js-dom.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import jsdom from 'jsdom';
3 |
4 | const { JSDOM } = jsdom;
5 |
6 | test('that mock-socket can be used within JSDOM', async t => {
7 | const options = {
8 | resources: 'usable',
9 | runScripts: 'dangerously'
10 | };
11 |
12 | const dom = await JSDOM.fromFile('tests/functional/js-dom.html', options);
13 |
14 | return new Promise(res => {
15 | setTimeout(() => {
16 | t.truthy(dom.window.Mock.Server);
17 | t.truthy(dom.window.Mock.SocketIO);
18 | t.truthy(dom.window.Mock.WebSocket);
19 | res();
20 | }, 50);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import buble from 'rollup-plugin-buble';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import resolve from 'rollup-plugin-node-resolve';
4 |
5 | export default {
6 | entry: 'src/index.js',
7 | plugins: [buble(), resolve({ jsnext: true, main: true }), commonjs()],
8 | targets: [
9 | { dest: 'dist/mock-socket.cjs.js', format: 'cjs' },
10 | { dest: 'dist/mock-socket.js', format: 'umd', moduleName: 'Mock' },
11 | { dest: 'dist/mock-socket.amd.js', format: 'amd', moduleName: 'Mock' },
12 | { dest: 'dist/mock-socket.es.js', format: 'es' },
13 | { dest: 'dist/mock-socket.es.mjs', format: 'es' }
14 | ]
15 | };
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "parser": "typescript-eslint-parser",
4 | "env": {
5 | "browser": true,
6 | "node": true
7 | },
8 | "rules": {
9 | "max-len": ["error", 120],
10 | "class-methods-use-this": ["error", { "exceptMethods": ["stopPropagation", "stopImmediatePropagation"] }],
11 | "consistent-return": 0,
12 | "prefer-rest-params": 0,
13 | "no-param-reassign": 0,
14 | "comma-dangle": ["error", "never"],
15 | "arrow-parens": 0,
16 | "import/no-extraneous-dependencies": 0, // all deps are resolved with "node-resolve"
17 | "no-underscore-dangle": ["error", { "allowAfterThis": true }]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/issues/157.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 |
5 | test.cb('websocket onmessage fired before onopen', t => {
6 | const socketUrl = 'ws://localhost:8080';
7 | const mockServer = new Server(socketUrl);
8 | const mockSocket = new WebSocket(socketUrl);
9 |
10 | let onOpenCalled = false;
11 |
12 | mockServer.on('connection', socket => {
13 | socket.send('test message');
14 | });
15 |
16 | mockSocket.onopen = () => {
17 | onOpenCalled = true;
18 | };
19 |
20 | mockSocket.onmessage = () => {
21 | t.true(onOpenCalled, 'on open was called before onmessage');
22 | t.end();
23 | };
24 | });
25 |
--------------------------------------------------------------------------------
/tests/issues/19.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 |
5 | test.cb('that server on(message) argument should be a string and not an object', t => {
6 | const socketUrl = 'ws://localhost:8080';
7 | const mockServer = new Server(socketUrl);
8 | const mockSocket = new WebSocket(socketUrl);
9 |
10 | mockServer.on('connection', socket => {
11 | socket.on('message', message => {
12 | t.is(typeof message, 'string', 'message should be a string and not an object');
13 | mockServer.close();
14 | t.end();
15 | });
16 | });
17 |
18 | mockSocket.onopen = function open() {
19 | this.send('1');
20 | };
21 | });
22 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
3 | */
4 | export const CLOSE_CODES = {
5 | CLOSE_NORMAL: 1000,
6 | CLOSE_GOING_AWAY: 1001,
7 | CLOSE_PROTOCOL_ERROR: 1002,
8 | CLOSE_UNSUPPORTED: 1003,
9 | CLOSE_NO_STATUS: 1005,
10 | CLOSE_ABNORMAL: 1006,
11 | UNSUPPORTED_DATA: 1007,
12 | POLICY_VIOLATION: 1008,
13 | CLOSE_TOO_LARGE: 1009,
14 | MISSING_EXTENSION: 1010,
15 | INTERNAL_ERROR: 1011,
16 | SERVICE_RESTART: 1012,
17 | TRY_AGAIN_LATER: 1013,
18 | TLS_HANDSHAKE: 1015
19 | };
20 |
21 | export const ERROR_PREFIX = {
22 | CONSTRUCTOR_ERROR: "Failed to construct 'WebSocket':",
23 | CLOSE_ERROR: "Failed to execute 'close' on 'WebSocket':",
24 | EVENT: {
25 | CONSTRUCT: "Failed to construct 'Event':",
26 | MESSAGE: "Failed to construct 'MessageEvent':",
27 | CLOSE: "Failed to construct 'CloseEvent':"
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/tests/issues/64.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import IO from '../../src/socket-io';
4 |
5 | test.cb('mock sockets invokes each handler', t => {
6 | const socketUrl = 'ws://roomy';
7 | const server = new Server(socketUrl);
8 | const socket = new IO(socketUrl);
9 |
10 | let handler1Called = false;
11 | let handler2Called = false;
12 |
13 | socket.on('custom-event', () => {
14 | t.true(true);
15 | handler1Called = true;
16 | });
17 |
18 | socket.on('custom-event', () => {
19 | t.true(true);
20 | handler2Called = true;
21 | });
22 |
23 | socket.on('connect', () => {
24 | socket.join('room');
25 | server.to('room').emit('custom-event');
26 | });
27 |
28 | setTimeout(() => {
29 | t.is(handler1Called, true);
30 | t.is(handler2Called, true);
31 | server.close();
32 | t.end();
33 | }, 500);
34 | });
35 |
--------------------------------------------------------------------------------
/src/helpers/protocol-verification.js:
--------------------------------------------------------------------------------
1 | import { ERROR_PREFIX } from '../constants';
2 |
3 | export default function protocolVerification(protocols = []) {
4 | if (!Array.isArray(protocols) && typeof protocols !== 'string') {
5 | throw new SyntaxError(`${ERROR_PREFIX.CONSTRUCTOR_ERROR} The subprotocol '${protocols.toString()}' is invalid.`);
6 | }
7 |
8 | if (typeof protocols === 'string') {
9 | protocols = [protocols];
10 | }
11 |
12 | const uniq = protocols
13 | .map(p => ({ count: 1, protocol: p }))
14 | .reduce((a, b) => {
15 | a[b.protocol] = (a[b.protocol] || 0) + b.count;
16 | return a;
17 | }, {});
18 |
19 | const duplicates = Object.keys(uniq).filter(a => uniq[a] > 1);
20 |
21 | if (duplicates.length > 0) {
22 | throw new SyntaxError(`${ERROR_PREFIX.CONSTRUCTOR_ERROR} The subprotocol '${duplicates[0]}' is duplicated.`);
23 | }
24 |
25 | return protocols;
26 | }
27 |
--------------------------------------------------------------------------------
/tests/unit/protocol-verification.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import protocolVerification from '../../src/helpers/protocol-verification';
3 |
4 | test('a non array or string protocol throws an error', t => {
5 | const error = t.throws(() => {
6 | protocolVerification(false);
7 | }, SyntaxError);
8 |
9 | t.is(error.message, "Failed to construct 'WebSocket': The subprotocol 'false' is invalid.");
10 | });
11 |
12 | test('if a protocol is duplicated it throws an error', t => {
13 | const error = t.throws(() => {
14 | protocolVerification(['foo', 'bar', 'foo']);
15 | }, SyntaxError);
16 |
17 | t.is(error.message, "Failed to construct 'WebSocket': The subprotocol 'foo' is duplicated.");
18 | });
19 |
20 | test('no protocol returns an empty array', t => {
21 | t.deepEqual(protocolVerification(), []);
22 | });
23 |
24 | test('passing an unique array of protocols returns an array of those protocols', t => {
25 | t.deepEqual(protocolVerification(['foo', 'bar']), ['foo', 'bar']);
26 | });
27 |
--------------------------------------------------------------------------------
/src/event/event.js:
--------------------------------------------------------------------------------
1 | import EventPrototype from './prototype';
2 | import { ERROR_PREFIX } from '../constants';
3 |
4 | export default class Event extends EventPrototype {
5 | constructor(type, eventInitConfig = {}) {
6 | super();
7 |
8 | if (!type) {
9 | throw new TypeError(`${ERROR_PREFIX.EVENT_ERROR} 1 argument required, but only 0 present.`);
10 | }
11 |
12 | if (typeof eventInitConfig !== 'object') {
13 | throw new TypeError(`${ERROR_PREFIX.EVENT_ERROR} parameter 2 ('eventInitDict') is not an object.`);
14 | }
15 |
16 | const { bubbles, cancelable } = eventInitConfig;
17 |
18 | this.type = `${type}`;
19 | this.timeStamp = Date.now();
20 | this.target = null;
21 | this.srcElement = null;
22 | this.returnValue = true;
23 | this.isTrusted = false;
24 | this.eventPhase = 0;
25 | this.defaultPrevented = false;
26 | this.currentTarget = null;
27 | this.cancelable = cancelable ? Boolean(cancelable) : false;
28 | this.cancelBubble = false;
29 | this.bubbles = bubbles ? Boolean(bubbles) : false;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | # filtering branches here, prevents duplicate builds from pull_request and push
7 | branches:
8 | - master
9 | - 'v*'
10 | # always run CI for tags
11 | tags:
12 | - '*'
13 |
14 | # early issue detection, run CI weekly on Sundays
15 | schedule:
16 | - cron: '0 6 * * 0'
17 |
18 | jobs:
19 | test:
20 | name: Test
21 | runs-on: ${{ matrix.os }}
22 | strategy:
23 | matrix:
24 | os: [ubuntu-latest, macOS-latest]
25 | node_version: ['8', '10', '12']
26 |
27 | steps:
28 | - uses: actions/checkout@v1
29 |
30 | - name: Use Node.js ${{ matrix.node_version }}
31 | uses: actions/setup-node@v1
32 | with:
33 | version: ${{ matrix.node_version }}
34 |
35 | - name: install yarn
36 | run: npm install -g yarn
37 |
38 | - name: install dependencies
39 | run: yarn install --frozen-lockfile
40 |
41 | - name: build
42 | run: yarn build
43 |
44 | - name: test
45 | run: yarn test
--------------------------------------------------------------------------------
/tests/issues/13.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 |
5 | test.cb('mock sockets sends double messages', t => {
6 | const socketUrl = 'ws://localhost:8080';
7 | const mockServer = new Server(socketUrl);
8 | const mockSocketA = new WebSocket(socketUrl);
9 | const mockSocketB = new WebSocket(socketUrl);
10 |
11 | let numMessagesSent = 0;
12 | let numMessagesReceived = 0;
13 | let connectionsCreated = 0;
14 |
15 | const serverMessageHandler = function handlerFunc() {
16 | numMessagesReceived += 1;
17 | };
18 |
19 | mockServer.on('connection', server => {
20 | connectionsCreated += 1;
21 | server.on('message', serverMessageHandler);
22 | });
23 |
24 | mockSocketA.onopen = function open() {
25 | numMessagesSent += 1;
26 | this.send('1');
27 | };
28 |
29 | mockSocketB.onopen = function open() {
30 | numMessagesSent += 1;
31 | this.send('2');
32 | };
33 |
34 | setTimeout(() => {
35 | t.is(numMessagesReceived, numMessagesSent);
36 | mockServer.close();
37 | t.end();
38 | }, 500);
39 | });
40 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Travis Hoover
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/functional/loader.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import systemjs from 'systemjs';
3 |
4 | test('amd modules are loaded', async t => {
5 | const mockSocket = await systemjs.import('./dist/mock-socket.amd.js');
6 |
7 | t.truthy(mockSocket.Server);
8 | t.truthy(mockSocket.WebSocket);
9 | t.truthy(mockSocket.SocketIO);
10 | });
11 |
12 | test('umd modules are loaded', async t => {
13 | const mockSocket = await systemjs.import('./dist/mock-socket.js');
14 |
15 | t.truthy(mockSocket.Server);
16 | t.truthy(mockSocket.WebSocket);
17 | t.truthy(mockSocket.SocketIO);
18 | });
19 |
20 | test('cjs modules are loaded', async t => {
21 | const mockSocket = await systemjs.import('./dist/mock-socket.cjs.js');
22 |
23 | t.truthy(mockSocket.Server);
24 | t.truthy(mockSocket.WebSocket);
25 | t.truthy(mockSocket.SocketIO);
26 | });
27 |
28 | // TODO: install traceur (https://github.com/systemjs/plugin-traceur)
29 | test.skip('es modules are loaded', async t => {
30 | const mockSocket = await systemjs.import('./dist/mock-socket.es.js');
31 |
32 | t.truthy(mockSocket.Server);
33 | t.truthy(mockSocket.WebSocket);
34 | t.truthy(mockSocket.SocketIO);
35 | });
36 |
--------------------------------------------------------------------------------
/src/helpers/url-verification.js:
--------------------------------------------------------------------------------
1 | import URL from 'url-parse';
2 | import { ERROR_PREFIX } from '../constants';
3 |
4 | export default function urlVerification(url) {
5 | const urlRecord = new URL(url);
6 | const { pathname, protocol, hash } = urlRecord;
7 |
8 | if (!url) {
9 | throw new TypeError(`${ERROR_PREFIX.CONSTRUCTOR_ERROR} 1 argument required, but only 0 present.`);
10 | }
11 |
12 | if (!pathname) {
13 | urlRecord.pathname = '/';
14 | }
15 |
16 | if (protocol === '') {
17 | throw new SyntaxError(`${ERROR_PREFIX.CONSTRUCTOR_ERROR} The URL '${urlRecord.toString()}' is invalid.`);
18 | }
19 |
20 | if (protocol !== 'ws:' && protocol !== 'wss:') {
21 | throw new SyntaxError(
22 | `${ERROR_PREFIX.CONSTRUCTOR_ERROR} The URL's scheme must be either 'ws' or 'wss'. '${protocol}' is not allowed.`
23 | );
24 | }
25 |
26 | if (hash !== '') {
27 | /* eslint-disable max-len */
28 | throw new SyntaxError(
29 | `${
30 | ERROR_PREFIX.CONSTRUCTOR_ERROR
31 | } The URL contains a fragment identifier ('${hash}'). Fragment identifiers are not allowed in WebSocket URLs.`
32 | );
33 | /* eslint-enable max-len */
34 | }
35 |
36 | return urlRecord.toString();
37 | }
38 |
--------------------------------------------------------------------------------
/src/event/close.js:
--------------------------------------------------------------------------------
1 | import EventPrototype from './prototype';
2 | import { ERROR_PREFIX } from '../constants';
3 |
4 | export default class CloseEvent extends EventPrototype {
5 | constructor(type, eventInitConfig = {}) {
6 | super();
7 |
8 | if (!type) {
9 | throw new TypeError(`${ERROR_PREFIX.EVENT.CLOSE} 1 argument required, but only 0 present.`);
10 | }
11 |
12 | if (typeof eventInitConfig !== 'object') {
13 | throw new TypeError(`${ERROR_PREFIX.EVENT.CLOSE} parameter 2 ('eventInitDict') is not an object`);
14 | }
15 |
16 | const { bubbles, cancelable, code, reason, wasClean } = eventInitConfig;
17 |
18 | this.type = `${type}`;
19 | this.timeStamp = Date.now();
20 | this.target = null;
21 | this.srcElement = null;
22 | this.returnValue = true;
23 | this.isTrusted = false;
24 | this.eventPhase = 0;
25 | this.defaultPrevented = false;
26 | this.currentTarget = null;
27 | this.cancelable = cancelable ? Boolean(cancelable) : false;
28 | this.cancelBubble = false;
29 | this.bubbles = bubbles ? Boolean(bubbles) : false;
30 | this.code = typeof code === 'number' ? parseInt(code, 10) : 0;
31 | this.reason = `${reason || ''}`;
32 | this.wasClean = wasClean ? Boolean(wasClean) : false;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/event/message.js:
--------------------------------------------------------------------------------
1 | import EventPrototype from './prototype';
2 | import { ERROR_PREFIX } from '../constants';
3 |
4 | export default class MessageEvent extends EventPrototype {
5 | constructor(type, eventInitConfig = {}) {
6 | super();
7 |
8 | if (!type) {
9 | throw new TypeError(`${ERROR_PREFIX.EVENT.MESSAGE} 1 argument required, but only 0 present.`);
10 | }
11 |
12 | if (typeof eventInitConfig !== 'object') {
13 | throw new TypeError(`${ERROR_PREFIX.EVENT.MESSAGE} parameter 2 ('eventInitDict') is not an object`);
14 | }
15 |
16 | const { bubbles, cancelable, data, origin, lastEventId, ports } = eventInitConfig;
17 |
18 | this.type = `${type}`;
19 | this.timeStamp = Date.now();
20 | this.target = null;
21 | this.srcElement = null;
22 | this.returnValue = true;
23 | this.isTrusted = false;
24 | this.eventPhase = 0;
25 | this.defaultPrevented = false;
26 | this.currentTarget = null;
27 | this.cancelable = cancelable ? Boolean(cancelable) : false;
28 | this.canncelBubble = false;
29 | this.bubbles = bubbles ? Boolean(bubbles) : false;
30 | this.origin = `${origin}`;
31 | this.ports = typeof ports === 'undefined' ? null : ports;
32 | this.data = typeof data === 'undefined' ? null : data;
33 | this.lastEventId = `${lastEventId || ''}`;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/algorithms/close.js:
--------------------------------------------------------------------------------
1 | import WebSocket from '../websocket';
2 | import delay from '../helpers/delay';
3 | import networkBridge from '../network-bridge';
4 | import { createCloseEvent, createEvent } from '../event/factory';
5 |
6 | export function closeWebSocketConnection(context, code, reason) {
7 | context.readyState = WebSocket.CLOSING;
8 |
9 | const server = networkBridge.serverLookup(context.url);
10 | const closeEvent = createCloseEvent({
11 | type: 'close',
12 | target: context.target,
13 | code,
14 | reason
15 | });
16 |
17 | delay(() => {
18 | networkBridge.removeWebSocket(context, context.url);
19 |
20 | context.readyState = WebSocket.CLOSED;
21 | context.dispatchEvent(closeEvent);
22 |
23 | if (server) {
24 | server.dispatchEvent(closeEvent, server);
25 | }
26 | }, context);
27 | }
28 |
29 | export function failWebSocketConnection(context, code, reason) {
30 | context.readyState = WebSocket.CLOSING;
31 |
32 | const server = networkBridge.serverLookup(context.url);
33 | const closeEvent = createCloseEvent({
34 | type: 'close',
35 | target: context.target,
36 | code,
37 | reason,
38 | wasClean: false
39 | });
40 |
41 | const errorEvent = createEvent({
42 | type: 'error',
43 | target: context.target
44 | });
45 |
46 | delay(() => {
47 | networkBridge.removeWebSocket(context, context.url);
48 |
49 | context.readyState = WebSocket.CLOSED;
50 | context.dispatchEvent(errorEvent);
51 | context.dispatchEvent(closeEvent);
52 |
53 | if (server) {
54 | server.dispatchEvent(closeEvent, server);
55 | }
56 | }, context);
57 | }
58 |
--------------------------------------------------------------------------------
/src/helpers/proxy-factory.js:
--------------------------------------------------------------------------------
1 | import { CLOSE_CODES } from '../constants';
2 | import { closeWebSocketConnection } from '../algorithms/close';
3 | import normalizeSendData from './normalize-send';
4 | import { createMessageEvent } from '../event/factory';
5 |
6 | const proxies = new WeakMap();
7 |
8 | export default function proxyFactory(target) {
9 | if (proxies.has(target)) {
10 | return proxies.get(target);
11 | }
12 |
13 | const proxy = new Proxy(target, {
14 | get(obj, prop) {
15 | if (prop === 'close') {
16 | return function close(options = {}) {
17 | const code = options.code || CLOSE_CODES.CLOSE_NORMAL;
18 | const reason = options.reason || '';
19 |
20 | closeWebSocketConnection(proxy, code, reason);
21 | };
22 | }
23 |
24 | if (prop === 'send') {
25 | return function send(data) {
26 | data = normalizeSendData(data);
27 |
28 | target.dispatchEvent(
29 | createMessageEvent({
30 | type: 'message',
31 | data,
32 | origin: this.url,
33 | target
34 | })
35 | );
36 | };
37 | }
38 |
39 | const toSocketName = type => (type === 'message' ? `server::${type}` : type);
40 | if (prop === 'on') {
41 | return function onWrapper(type, cb) {
42 | target.addEventListener(toSocketName(type), cb);
43 | };
44 | }
45 | if (prop === 'off') {
46 | return function offWrapper(type, cb) {
47 | target.removeEventListener(toSocketName(type), cb);
48 | };
49 | }
50 |
51 | if (prop === 'target') {
52 | return target;
53 | }
54 |
55 | return obj[prop];
56 | }
57 | });
58 | proxies.set(target, proxy);
59 |
60 | return proxy;
61 | }
62 |
--------------------------------------------------------------------------------
/tests/unit/url-verification.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import urlVerification from '../../src/helpers/url-verification';
3 |
4 | test('that no url throws an error', t => {
5 | const error = t.throws(() => {
6 | urlVerification();
7 | }, TypeError);
8 |
9 | t.is(error.message, "Failed to construct 'WebSocket': 1 argument required, but only 0 present.");
10 | });
11 |
12 | test('if the url is invalid it throws an error', t => {
13 | const error = t.throws(() => {
14 | urlVerification('something-that-is-not-a-url');
15 | }, SyntaxError);
16 |
17 | t.is(error.message, "Failed to construct 'WebSocket': The URL 'something-that-is-not-a-url' is invalid.");
18 | });
19 |
20 | test('that if the protocol is not ws: or wss: it throws an error', t => {
21 | const error = t.throws(() => {
22 | urlVerification('http://foobar.com');
23 | }, SyntaxError);
24 |
25 | // eslint-disable-next-line max-len
26 | t.is(
27 | error.message,
28 | "Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. 'http:' is not allowed."
29 | );
30 | });
31 |
32 | test('that if the protocol is not ws: or wss: it throws an error', t => {
33 | const error = t.throws(() => {
34 | urlVerification('http://foobar.com');
35 | }, SyntaxError);
36 |
37 | // eslint-disable-next-line max-len
38 | t.is(
39 | error.message,
40 | "Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. 'http:' is not allowed."
41 | );
42 | });
43 |
44 | test('that if the url contains a fragment it throws an error', t => {
45 | const error = t.throws(() => {
46 | urlVerification('ws://foobar.com/#hash');
47 | }, SyntaxError);
48 |
49 | /* eslint-disable max-len */
50 | t.is(
51 | error.message,
52 | "Failed to construct 'WebSocket': The URL contains a fragment identifier ('#hash'). Fragment identifiers are not allowed in WebSocket URLs."
53 | );
54 | /* eslint-enable max-len */
55 | });
56 |
57 | test('a valid url is returned', t => {
58 | t.is(urlVerification('ws://foobar.com'), 'ws://foobar.com/');
59 | t.is(urlVerification('ws://foobar.com/bar'), 'ws://foobar.com/bar');
60 | });
61 |
--------------------------------------------------------------------------------
/src/event/factory.js:
--------------------------------------------------------------------------------
1 | import Event from './event';
2 | import MessageEvent from './message';
3 | import CloseEvent from './close';
4 | import { CLOSE_CODES } from '../constants';
5 |
6 | /*
7 | * Creates an Event object and extends it to allow full modification of
8 | * its properties.
9 | *
10 | * @param {object} config - within config you will need to pass type and optionally target
11 | */
12 | function createEvent(config) {
13 | const { type, target } = config;
14 | const eventObject = new Event(type);
15 |
16 | if (target) {
17 | eventObject.target = target;
18 | eventObject.srcElement = target;
19 | eventObject.currentTarget = target;
20 | }
21 |
22 | return eventObject;
23 | }
24 |
25 | /*
26 | * Creates a MessageEvent object and extends it to allow full modification of
27 | * its properties.
28 | *
29 | * @param {object} config - within config: type, origin, data and optionally target
30 | */
31 | function createMessageEvent(config) {
32 | const { type, origin, data, target } = config;
33 | const messageEvent = new MessageEvent(type, {
34 | data,
35 | origin
36 | });
37 |
38 | if (target) {
39 | messageEvent.target = target;
40 | messageEvent.srcElement = target;
41 | messageEvent.currentTarget = target;
42 | }
43 |
44 | return messageEvent;
45 | }
46 |
47 | /*
48 | * Creates a CloseEvent object and extends it to allow full modification of
49 | * its properties.
50 | *
51 | * @param {object} config - within config: type and optionally target, code, and reason
52 | */
53 | function createCloseEvent(config) {
54 | const { code, reason, type, target } = config;
55 | let { wasClean } = config;
56 |
57 | if (!wasClean) {
58 | wasClean = code === CLOSE_CODES.CLOSE_NORMAL || code === CLOSE_CODES.CLOSE_NO_STATUS;
59 | }
60 |
61 | const closeEvent = new CloseEvent(type, {
62 | code,
63 | reason,
64 | wasClean
65 | });
66 |
67 | if (target) {
68 | closeEvent.target = target;
69 | closeEvent.srcElement = target;
70 | closeEvent.currentTarget = target;
71 | }
72 |
73 | return closeEvent;
74 | }
75 |
76 | export { createEvent, createMessageEvent, createCloseEvent };
77 |
--------------------------------------------------------------------------------
/tests/issues/65.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import IO from '../../src/socket-io';
4 |
5 | test.cb('mock socket invokes each handler with unique reference', t => {
6 | const socketUrl = 'ws://roomy';
7 | const server = new Server(socketUrl);
8 | const socket = new IO(socketUrl);
9 |
10 | let handlerInvoked = 0;
11 | const handler3 = function handlerFunc() {
12 | t.true(true);
13 | handlerInvoked += 1;
14 | };
15 |
16 | // Same functions but different scopes/contexts
17 | socket.on('custom-event', handler3.bind(Object.create(null)));
18 | socket.on('custom-event', handler3.bind(Object.create(null)));
19 |
20 | // Same functions with same scope/context (only one should be added)
21 | socket.on('custom-event', handler3);
22 | socket.on('custom-event', handler3); // not expected
23 |
24 | socket.on('connect', () => {
25 | socket.join('room');
26 | server.to('room').emit('custom-event');
27 | });
28 |
29 | setTimeout(() => {
30 | t.is(handlerInvoked, 3, 'handler invoked too many times');
31 | server.close();
32 | t.end();
33 | }, 500);
34 | });
35 |
36 | test.cb('mock socket invokes each handler per socket', t => {
37 | const socketUrl = 'ws://roomy';
38 | const server = new Server(socketUrl);
39 | const socketA = new IO(socketUrl);
40 | const socketB = new IO(socketUrl);
41 |
42 | let handlerInvoked = 0;
43 | const handler3 = function handlerFunc() {
44 | t.true(true);
45 | handlerInvoked += 1;
46 | };
47 |
48 | // Same functions but different scopes/contexts
49 | socketA.on('custom-event', handler3.bind(socketA));
50 | socketB.on('custom-event', handler3.bind(socketB));
51 |
52 | // Same functions with same scope/context (only one should be added)
53 | socketA.on('custom-event', handler3);
54 | socketA.on('custom-event', handler3); // not expected
55 |
56 | socketB.on('custom-event', handler3.bind(socketB)); // expected because bind creates a new method
57 |
58 | socketA.on('connect', () => {
59 | socketA.join('room');
60 | socketB.join('room');
61 | server.to('room').emit('custom-event');
62 | });
63 |
64 | setTimeout(() => {
65 | t.is(handlerInvoked, 4, 'handler invoked too many times');
66 | server.close();
67 | t.end();
68 | }, 500);
69 | });
70 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mock-socket",
3 | "description": "Javascript mocking library for websockets and socket.io",
4 | "version": "9.3.1",
5 | "license": "MIT",
6 | "author": "Travis Hoover",
7 | "main": "./dist/mock-socket",
8 | "directories": {
9 | "test": "tests"
10 | },
11 | "engines": {
12 | "node": ">= 8"
13 | },
14 | "scripts": {
15 | "build": "rm -rf dist && rollup -c rollup.config.js",
16 | "lint": "eslint src tests",
17 | "prepublishOnly": "yarn build",
18 | "test": "ava --serial --verbose"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/thoov/mock-socket.git"
23 | },
24 | "keywords": [
25 | "websockets",
26 | "mock",
27 | "mocksocket",
28 | "sockets"
29 | ],
30 | "bugs": {
31 | "url": "https://github.com/thoov/mock-socket/issues"
32 | },
33 | "homepage": "https://github.com/thoov/mock-socket",
34 | "dependencies": {
35 | },
36 | "devDependencies": {
37 | "ava": "^0.19.1",
38 | "babel-core": "^6.24.1",
39 | "babel-loader": "^7.0.0",
40 | "babel-polyfill": "^6.23.0",
41 | "babel-preset-latest": "^6.24.1",
42 | "eslint": "^4.18.2",
43 | "eslint-config-airbnb-base": "^11.2.0",
44 | "eslint-plugin-import": "^2.0.1",
45 | "husky": "^1.3.1",
46 | "jsdom": "^11.5.1",
47 | "lint-staged": "^3.4.1",
48 | "nyc": "^10.3.2",
49 | "prettier": "^1.3.1",
50 | "rollup": "^0.41.6",
51 | "rollup-plugin-buble": "^0.15.0",
52 | "rollup-plugin-commonjs": "^8.2.6",
53 | "rollup-plugin-node-resolve": "^3.0.0",
54 | "systemjs": "^0.20.12",
55 | "typescript": "^2.9.2",
56 | "typescript-eslint-parser": "^16.0.1",
57 | "url-parse": "^1.5.2"
58 | },
59 | "ava": {
60 | "files": [
61 | "tests/**/*.test.js"
62 | ],
63 | "require": [
64 | "babel-register",
65 | "babel-polyfill"
66 | ],
67 | "babel": "inherit"
68 | },
69 | "husky": {
70 | "hooks": {
71 | "pre-commit": "lint-staged"
72 | }
73 | },
74 | "prettier": {
75 | "singleQuote": true,
76 | "printWidth": 120
77 | },
78 | "lint-staged": {
79 | "*.{js}": [
80 | "prettier --write",
81 | "eslint",
82 | "git add"
83 | ]
84 | },
85 | "volta": {
86 | "node": "10.16.0",
87 | "yarn": "1.17.3"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/issues/242.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 | import Event from '../../src/event/event';
5 |
6 | test('websocket on* methods family returns a single listener', t => {
7 | const socketUrl = 'ws://localhost:8080';
8 | const mockServer = new Server(socketUrl);
9 | const mockSocket = new WebSocket(socketUrl);
10 |
11 | const listener = () => {
12 | /* do nothing */
13 | };
14 |
15 | mockSocket.onopen = listener;
16 | mockSocket.onmessage = listener;
17 | mockSocket.onerror = listener;
18 | mockSocket.onclose = listener;
19 |
20 | t.is(mockSocket.onopen, listener);
21 | t.is(mockSocket.onmessage, listener);
22 | t.is(mockSocket.onerror, listener);
23 | t.is(mockSocket.onclose, listener);
24 |
25 | mockServer.close();
26 | });
27 |
28 | test("websocket on* methods family doesn't delete other listeners", async t => {
29 | const socketUrl = 'ws://localhost:8080';
30 | const mockServer = new Server(socketUrl);
31 | const mockSocket = new WebSocket(socketUrl);
32 |
33 | mockServer.on('connection', socket => {
34 | socket.send('test message');
35 | });
36 |
37 | let onOpenCalled = 0;
38 | let onMessageCalled = 0;
39 | let onErrorCalled = 0;
40 |
41 | let onCloseEventResolve;
42 | let onCloseResolve;
43 | const allClosed = Promise.all([
44 | new Promise(r => {
45 | onCloseEventResolve = r;
46 | }),
47 | new Promise(r => {
48 | onCloseResolve = r;
49 | })
50 | ]);
51 |
52 | mockSocket.addEventListener('open', () => {
53 | onOpenCalled += 1;
54 | });
55 | mockSocket.addEventListener('message', () => {
56 | onMessageCalled += 1;
57 | mockSocket.dispatchEvent(new Event('error'));
58 | });
59 | mockSocket.addEventListener('error', () => {
60 | onErrorCalled += 1;
61 | mockSocket.close();
62 | });
63 | mockSocket.addEventListener('close', () => onCloseEventResolve());
64 |
65 | const throwCb = () => {
66 | throw new Error('this call should have been replaced');
67 | };
68 | mockSocket.onopen = throwCb;
69 | mockSocket.onopen = () => {
70 | onOpenCalled += 1;
71 | };
72 | mockSocket.onmessage = throwCb;
73 | mockSocket.onmessage = () => {
74 | onMessageCalled += 1;
75 | };
76 | mockSocket.onerror = throwCb;
77 | mockSocket.onerror = () => {
78 | onErrorCalled += 1;
79 | };
80 | mockSocket.onclose = throwCb;
81 | mockSocket.onclose = () => onCloseResolve();
82 |
83 | await allClosed;
84 |
85 | t.is(onOpenCalled, 2);
86 | t.is(onMessageCalled, 2);
87 | t.is(onErrorCalled, 2);
88 |
89 | mockServer.close();
90 | });
91 |
--------------------------------------------------------------------------------
/src/event/target.js:
--------------------------------------------------------------------------------
1 | import { reject, filter } from '../helpers/array-helpers';
2 |
3 | /*
4 | * EventTarget is an interface implemented by objects that can
5 | * receive events and may have listeners for them.
6 | *
7 | * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
8 | */
9 | class EventTarget {
10 | constructor() {
11 | this.listeners = {};
12 | }
13 |
14 | /*
15 | * Ties a listener function to an event type which can later be invoked via the
16 | * dispatchEvent method.
17 | *
18 | * @param {string} type - the type of event (ie: 'open', 'message', etc.)
19 | * @param {function} listener - callback function to invoke when an event is dispatched matching the type
20 | * @param {boolean} useCapture - N/A TODO: implement useCapture functionality
21 | */
22 | addEventListener(type, listener /* , useCapture */) {
23 | if (typeof listener === 'function') {
24 | if (!Array.isArray(this.listeners[type])) {
25 | this.listeners[type] = [];
26 | }
27 |
28 | // Only add the same function once
29 | if (filter(this.listeners[type], item => item === listener).length === 0) {
30 | this.listeners[type].push(listener);
31 | }
32 | }
33 | }
34 |
35 | /*
36 | * Removes the listener so it will no longer be invoked via the dispatchEvent method.
37 | *
38 | * @param {string} type - the type of event (ie: 'open', 'message', etc.)
39 | * @param {function} listener - callback function to invoke when an event is dispatched matching the type
40 | * @param {boolean} useCapture - N/A TODO: implement useCapture functionality
41 | */
42 | removeEventListener(type, removingListener /* , useCapture */) {
43 | const arrayOfListeners = this.listeners[type];
44 | this.listeners[type] = reject(arrayOfListeners, listener => listener === removingListener);
45 | }
46 |
47 | /*
48 | * Invokes all listener functions that are listening to the given event.type property. Each
49 | * listener will be passed the event as the first argument.
50 | *
51 | * @param {object} event - event object which will be passed to all listeners of the event.type property
52 | */
53 | dispatchEvent(event, ...customArguments) {
54 | const eventName = event.type;
55 | const listeners = this.listeners[eventName];
56 |
57 | if (!Array.isArray(listeners)) {
58 | return false;
59 | }
60 |
61 | listeners.forEach(listener => {
62 | if (customArguments.length > 0) {
63 | listener.apply(this, customArguments);
64 | } else {
65 | listener.call(this, event);
66 | }
67 | });
68 |
69 | return true;
70 | }
71 | }
72 |
73 | export default EventTarget;
74 |
--------------------------------------------------------------------------------
/tests/unit/websocket.test.js:
--------------------------------------------------------------------------------
1 | import URL from 'url-parse';
2 | import test from 'ava';
3 | import WebSocket from '../../src/websocket';
4 | import EventTarget from '../../src/event/target';
5 |
6 | test.skip('that not passing a url throws an error', t => {
7 | t.throws(() => {
8 | new WebSocket();
9 | }, "Failed to construct 'WebSocket': 1 argument required, but only 0 present");
10 | });
11 |
12 | test('that websockets inherents EventTarget methods with string type url', t => {
13 | const mySocket = new WebSocket('ws://not-real');
14 | t.true(mySocket instanceof EventTarget);
15 | });
16 |
17 | test('that websockets inherents EventTarget methods with URL type url', t => {
18 | const mySocket = new WebSocket(new URL('ws://not-real'));
19 |
20 | t.true(mySocket instanceof EventTarget);
21 | t.is(mySocket.url, 'ws://not-real/');
22 | });
23 |
24 | test('that on(open, message, error, and close) can be set', t => {
25 | const mySocket = new WebSocket('ws://not-real');
26 |
27 | mySocket.onopen = () => {};
28 | mySocket.onmessage = () => {};
29 | mySocket.onclose = () => {};
30 | mySocket.onerror = () => {};
31 |
32 | const listeners = mySocket.listeners;
33 |
34 | t.is(listeners.open.length, 1);
35 | t.is(listeners.message.length, 1);
36 | t.is(listeners.close.length, 1);
37 | t.is(listeners.error.length, 1);
38 | });
39 |
40 | test('that passing protocols into the constructor works', t => {
41 | const mySocket = new WebSocket('ws://not-real', 'foo');
42 | const myOtherSocket = new WebSocket('ws://not-real', ['bar']);
43 |
44 | t.is(mySocket.protocol, 'foo', 'the correct protocol is set when it was passed in as a string');
45 | t.is(myOtherSocket.protocol, 'bar', 'the correct protocol is set when it was passed in as an array');
46 | });
47 |
48 | test('that sending when the socket is in the `CONNECTING` state throws an exception', t => {
49 | const mySocket = new WebSocket('ws://not-real', 'foo');
50 | t.is(mySocket.readyState, WebSocket.CONNECTING);
51 | t.throws(
52 | () => {
53 | mySocket.send('testing');
54 | },
55 | "Failed to execute 'send' on 'WebSocket': Still in CONNECTING state",
56 | 'an exception is thrown when sending while in the `CONNECTING` state'
57 | );
58 | });
59 |
60 | test('that sending when the socket is in the `CLOSING` state does not throw an exception', t => {
61 | const mySocket = new WebSocket('ws://not-real', 'foo');
62 | mySocket.close();
63 | t.is(mySocket.readyState, WebSocket.CLOSING);
64 | t.notThrows(
65 | () => {
66 | mySocket.send('testing');
67 | },
68 | );
69 | });
70 |
71 | test.cb('that sending when the socket is in the `CLOSED` state does not throw an exception', t => {
72 | const mySocket = new WebSocket('ws://not-real', 'foo');
73 | mySocket.close();
74 | mySocket.addEventListener('close', () => {
75 | t.is(mySocket.readyState, WebSocket.CLOSED);
76 | t.notThrows(
77 | () => {
78 | mySocket.send('testing');
79 | },
80 | );
81 | t.end();
82 | }, { once: true });
83 | });
84 |
--------------------------------------------------------------------------------
/tests/unit/event-target.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { createEvent } from '../../src/event/factory';
3 | import EventTarget from '../../src/event/target';
4 |
5 | class Mock extends EventTarget {}
6 | class MockFoo extends EventTarget {}
7 |
8 | test('has all the required methods', t => {
9 | const mock = new Mock();
10 |
11 | t.is(typeof mock.addEventListener, 'function');
12 | t.is(typeof mock.removeEventListener, 'function');
13 | t.is(typeof mock.dispatchEvent, 'function');
14 | });
15 |
16 | test('adding/removing "message" event listeners works', t => {
17 | const mock = new Mock();
18 | const eventObject = createEvent({
19 | type: 'message'
20 | });
21 |
22 | const fooListener = event => {
23 | t.is(event.type, 'message');
24 | };
25 | const barListener = event => {
26 | t.is(event.type, 'message');
27 | };
28 |
29 | mock.addEventListener('message', fooListener);
30 | mock.addEventListener('message', barListener);
31 | mock.dispatchEvent(eventObject);
32 |
33 | mock.removeEventListener('message', fooListener);
34 | mock.dispatchEvent(eventObject);
35 |
36 | mock.removeEventListener('message', barListener);
37 | mock.dispatchEvent(eventObject);
38 | });
39 |
40 | test('events to different object should not share events', t => {
41 | const mock = new Mock();
42 | const mockFoo = new MockFoo();
43 | const eventObject = createEvent({
44 | type: 'message'
45 | });
46 |
47 | const fooListener = event => {
48 | t.is(event.type, 'message');
49 | };
50 | const barListener = event => {
51 | t.is(event.type, 'message');
52 | };
53 |
54 | mock.addEventListener('message', fooListener);
55 | mockFoo.addEventListener('message', barListener);
56 | mock.dispatchEvent(eventObject);
57 | mockFoo.dispatchEvent(eventObject);
58 |
59 | mock.removeEventListener('message', fooListener);
60 | mock.dispatchEvent(eventObject);
61 | mockFoo.dispatchEvent(eventObject);
62 |
63 | mockFoo.removeEventListener('message', barListener);
64 | mock.dispatchEvent(eventObject);
65 | mockFoo.dispatchEvent(eventObject);
66 | });
67 |
68 | test('that adding the same function twice for the same event type is only added once', t => {
69 | const mock = new Mock();
70 | const fooListener = event => {
71 | t.is(event.type, 'message');
72 | };
73 | const barListener = event => {
74 | t.is(event.type, 'message');
75 | };
76 |
77 | mock.addEventListener('message', fooListener);
78 | mock.addEventListener('message', fooListener);
79 | mock.addEventListener('message', barListener);
80 |
81 | t.is(mock.listeners.message.length, 2);
82 | });
83 |
84 | test('that dispatching an event with multiple data arguments works correctly', t => {
85 | const mock = new Mock();
86 | const eventObject = createEvent({
87 | type: 'message'
88 | });
89 |
90 | const fooListener = (...data) => {
91 | t.is(data.length, 3);
92 | t.is(data[0], 'foo');
93 | t.is(data[1], 'bar');
94 | t.is(data[2], 'baz');
95 | };
96 |
97 | mock.addEventListener('message', fooListener);
98 | mock.dispatchEvent(eventObject, 'foo', 'bar', 'baz');
99 | });
100 |
--------------------------------------------------------------------------------
/tests/functional/close-algorithm.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 | import networkBridge from '../../src/network-bridge';
5 | import delay from '../../src/helpers/delay';
6 |
7 | test.beforeEach(() => {
8 | networkBridge.urlMap = {};
9 | });
10 |
11 | test('calling close with a code that is not a number, 1000, < 3000, or > 4999 throws an error', t => {
12 | const server = new Server('ws://localhost:8080');
13 | const mockSocket = new WebSocket('ws://localhost:8080');
14 |
15 | const wrongType = t.throws(() => {
16 | mockSocket.close(false);
17 | });
18 |
19 | t.is(
20 | wrongType.message, // eslint-disable-next-line max-len
21 | "Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999. false is neither."
22 | );
23 |
24 | const numberToSmall = t.throws(() => {
25 | mockSocket.close(2999);
26 | });
27 |
28 | t.is(
29 | numberToSmall.message,
30 | "Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999. 2999 is neither."
31 | );
32 |
33 | const numberToLarge = t.throws(() => {
34 | mockSocket.close(5000);
35 | });
36 |
37 | t.is(
38 | numberToLarge.message,
39 | "Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999. 5000 is neither."
40 | );
41 | });
42 |
43 | test('that if reason is passed to close it must be under 123 bytes else an error is thrown', t => {
44 | const server = new Server('ws://localhost:8080');
45 | const mockSocket = new WebSocket('ws://localhost:8080');
46 |
47 | const longMessageError = t.throws(() => {
48 | mockSocket.close(
49 | 1000,
50 | `
51 | This is a very long message that should be over the 123 byte length so this will trigger an error
52 | This is a very long message that should be over the 123 byte length so this will trigger an error
53 | This is a very long message that should be over the 123 byte length so this will trigger an error
54 | `
55 | );
56 | });
57 |
58 | t.is(
59 | longMessageError.message,
60 | "Failed to execute 'close' on 'WebSocket': The message must not be greater than 123 bytes."
61 | );
62 | });
63 |
64 | test.cb('that if the readyState is CLOSED or CLOSING calling closed does nothing', t => {
65 | const server = new Server('ws://localhost:8080');
66 | const mockSocket = new WebSocket('ws://localhost:8080');
67 |
68 | mockSocket.readyState = WebSocket.CLOSED;
69 |
70 | mockSocket.onerror = () => {
71 | t.fail('this method should not be called');
72 | };
73 |
74 | mockSocket.onclose = () => {
75 | t.fail('this method should not be called');
76 | };
77 |
78 | mockSocket.close();
79 |
80 | delay(() => {
81 | t.end();
82 | });
83 | });
84 |
85 | test.cb('that if the readyState is CONNECTING we fail the connection and close', t => {
86 | const server = new Server('ws://localhost:8080');
87 | const mockSocket = new WebSocket('ws://localhost:8080');
88 |
89 | mockSocket.readyState = WebSocket.CONNECTING;
90 |
91 | mockSocket.onopen = () => {
92 | t.fail('open should not have been called');
93 | };
94 |
95 | mockSocket.onerror = () => {
96 | t.is(mockSocket.readyState, WebSocket.CLOSED);
97 | };
98 |
99 | mockSocket.onclose = () => {
100 | t.is(mockSocket.readyState, WebSocket.CLOSED);
101 | t.end();
102 | };
103 |
104 | mockSocket.close();
105 | });
106 |
--------------------------------------------------------------------------------
/tests/unit/socket-io.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import io from '../../src/socket-io';
3 | import Server from '../../src/server';
4 |
5 | test('it can be instantiated without a url', t => {
6 | const socket = io();
7 | t.truthy(socket);
8 | });
9 |
10 | test('it accepts a url', t => {
11 | const socket = io('http://localhost');
12 | t.truthy(socket);
13 | });
14 |
15 | test('it accepts an opts object parameter', t => {
16 | const protocol = { a: 'apple' };
17 | const socket = io('http://localhost', protocol);
18 | t.truthy(socket);
19 | t.is(socket.protocol, protocol);
20 | });
21 |
22 | test.cb('it includes opts object parameter in server connection callback', t => {
23 | const url = 'ws://not-real/';
24 | const myServer = new Server(url);
25 | const protocol = { a: 'apple' };
26 | const socket = io(url, protocol);
27 | myServer.on('connection', (server, instance) => {
28 | t.is(instance.protocol, protocol);
29 | t.end();
30 | });
31 | });
32 |
33 | test('it can equivalently use a connect method', t => {
34 | const socket = io.connect('http://localhost');
35 | t.truthy(socket);
36 | });
37 |
38 | test.cb.skip('it can broadcast to other connected sockets', t => {
39 | const url = 'ws://not-real/';
40 | const myServer = new Server(url);
41 | const socketFoo = io(url);
42 | const socketBar = io(url);
43 |
44 | myServer.on('connection', (server, socket) => {
45 | socketFoo.broadcast.emit('Testing');
46 | });
47 |
48 | socketFoo.on('Testing', () => {
49 | t.fail(null, null, 'Socket Foo should be excluded from broadcast');
50 | myServer.close();
51 | t.end();
52 | });
53 |
54 | socketBar.on('Testing', socket => {
55 | t.true(true);
56 | myServer.close();
57 | t.end();
58 | });
59 | });
60 |
61 | test.cb.skip('it can broadcast to other connected sockets in a room', t => {
62 | const roomKey = 'room-64';
63 | const url = 'ws://not-real/';
64 |
65 | const myServer = new Server(url);
66 | myServer.on('connection', (server, socket) => {
67 | socketFoo.broadcast.to(roomKey).emit('Testing', socket);
68 | });
69 |
70 | const socketFoo = io(url);
71 | socketFoo.join(roomKey);
72 | socketFoo.on('Testing', () => t.fail(null, null, 'Socket Foo should be excluded from broadcast'));
73 |
74 | const socketBar = io(url);
75 | socketBar.on('Testing', () => t.fail(null, null, 'Socket Bar should be excluded from broadcast'));
76 |
77 | const socketFooBar = io(url);
78 | socketFooBar.join(roomKey);
79 | socketFooBar.on('Testing', socket => {
80 | t.true(true);
81 | myServer.close();
82 | t.end();
83 | });
84 | });
85 |
86 | test('it confirms if event listeners of a specific type have been registered', t => {
87 | const socket = io();
88 | function fooMessageHandler() {}
89 | function barMessageListener() {}
90 |
91 | t.is(socket.hasListeners('foo-message'), false);
92 | t.is(socket.hasListeners('bar-message'), false);
93 |
94 | socket.on('foo-message', fooMessageHandler);
95 | t.is(socket.hasListeners('foo-message'), true);
96 | t.is(socket.hasListeners('bar-message'), false);
97 |
98 | socket.on('bar-message', barMessageListener);
99 | t.is(socket.hasListeners('bar-message'), true);
100 |
101 | socket.off('foo-message', fooMessageHandler);
102 | t.is(socket.hasListeners('foo-message'), false);
103 | t.is(socket.hasListeners('bar-message'), true);
104 |
105 | socket.off('bar-message', barMessageListener);
106 | t.is(socket.hasListeners('bar-message'), false);
107 | });
108 |
--------------------------------------------------------------------------------
/tests/unit/factory.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { createEvent, createMessageEvent, createCloseEvent } from '../../src/event/factory';
3 |
4 | const fakeObject = { foo: 'bar' };
5 |
6 | test('that the create methods throw errors if no type if specified', t => {
7 | t.throws(() => createEvent(), error => error.name === 'TypeError');
8 | t.throws(() => createMessageEvent(), error => error.name === 'TypeError');
9 | });
10 |
11 | test('that createEvent correctly creates an event', t => {
12 | let event = createEvent({
13 | type: 'open'
14 | });
15 |
16 | t.is(event.type, 'open', 'the type property is set');
17 | t.is(event.target, null, 'target is null as no target was passed');
18 | t.is(event.srcElement, null, 'srcElement is null as no target was passed');
19 | t.is(event.currentTarget, null, 'currentTarget is null as no target was passed');
20 |
21 | event = createEvent({
22 | type: 'open',
23 | target: fakeObject
24 | });
25 |
26 | t.deepEqual(event.target, fakeObject, 'target is set to fakeObject');
27 | t.deepEqual(event.srcElement, fakeObject, 'srcElement is set to fakeObject');
28 | t.deepEqual(event.currentTarget, fakeObject, 'currentTarget is set to fakeObject');
29 | });
30 |
31 | test('that createMessageEvent correctly creates an event', t => {
32 | let event = createMessageEvent({
33 | type: 'message',
34 | origin: 'ws://localhost:8080',
35 | data: 'Testing'
36 | });
37 |
38 | t.is(event.type, 'message', 'the type property is set');
39 | t.is(event.data, 'Testing', 'the data property is set');
40 | t.is(event.origin, 'ws://localhost:8080', 'the origin property is set');
41 | t.is(event.target, null, 'target is null as no target was passed');
42 | t.is(event.lastEventId, '', 'lastEventId is an empty string');
43 | t.is(event.srcElement, null, 'srcElement is null as no target was passed');
44 | t.is(event.currentTarget, null, 'currentTarget is null as no target was passed');
45 |
46 | event = createMessageEvent({
47 | type: 'close',
48 | origin: 'ws://localhost:8080',
49 | data: 'Testing',
50 | target: fakeObject
51 | });
52 |
53 | t.is(event.lastEventId, '', 'lastEventId is an empty string');
54 | t.deepEqual(event.target, fakeObject, 'target is set to fakeObject');
55 | t.deepEqual(event.srcElement, fakeObject, 'srcElement is set to fakeObject');
56 | t.deepEqual(event.currentTarget, fakeObject, 'currentTarget is set to fakeObject');
57 | });
58 |
59 | test('that createCloseEvent correctly creates an event', t => {
60 | let event = createCloseEvent({
61 | type: 'close'
62 | });
63 |
64 | t.is(event.code, 0, 'the code property is set');
65 | t.is(event.reason, '', 'the reason property is set');
66 | t.is(event.target, null, 'target is null as no target was passed');
67 | t.is(event.wasClean, false, 'wasClean is false as the code is not 1000');
68 | t.is(event.srcElement, null, 'srcElement is null as no target was passed');
69 | t.is(event.currentTarget, null, 'currentTarget is null as no target was passed');
70 |
71 | event = createCloseEvent({
72 | type: 'close',
73 | code: 1001,
74 | reason: 'my bad',
75 | target: fakeObject
76 | });
77 |
78 | t.is(event.code, 1001, 'the code property is set');
79 | t.is(event.reason, 'my bad', 'the reason property is set');
80 | t.deepEqual(event.target, fakeObject, 'target is set to fakeObject');
81 | t.deepEqual(event.srcElement, fakeObject, 'srcElement is set to fakeObject');
82 | t.deepEqual(event.currentTarget, fakeObject, 'currentTarget is set to fakeObject');
83 |
84 | event = createCloseEvent({
85 | type: 'close',
86 | code: 1000
87 | });
88 |
89 | t.is(event.wasClean, true, 'wasClean is true as the code is 1000');
90 | });
91 |
--------------------------------------------------------------------------------
/tests/issues/143.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 |
5 | test.cb('reassigning websocket onopen listener should replace previous listeners', t => {
6 | const socketUrl = 'ws://localhost:8080';
7 | const mockServer = new Server(socketUrl);
8 | const mockSocket = new WebSocket(socketUrl);
9 |
10 | let firstListenerCalled = false;
11 | let secondListenerCalled = false;
12 |
13 | mockSocket.onopen = () => {
14 | firstListenerCalled = true;
15 | };
16 | mockSocket.onopen = () => {
17 | secondListenerCalled = true;
18 | };
19 |
20 | setTimeout(() => {
21 | t.false(firstListenerCalled, 'The first listener should not be called');
22 | t.true(secondListenerCalled, 'Only the second listener should be called');
23 | mockServer.close();
24 | t.end();
25 | }, 500);
26 | });
27 |
28 | test.cb('reassigning websocket onmessage listener should replace previous listeners', t => {
29 | const socketUrl = 'ws://localhost:8080';
30 | const mockServer = new Server(socketUrl);
31 | const mockSocket = new WebSocket(socketUrl);
32 |
33 | let firstListenerCalled = false;
34 | let secondListenerCalled = false;
35 |
36 | mockSocket.onmessage = () => {
37 | firstListenerCalled = true;
38 | };
39 | mockSocket.onmessage = () => {
40 | secondListenerCalled = true;
41 | };
42 |
43 | mockServer.on('connection', socket => {
44 | socket.send('test message');
45 | t.false(firstListenerCalled, 'The first listener should not be called');
46 | t.true(secondListenerCalled, 'Only the second listener should be called');
47 | mockServer.close();
48 | t.end();
49 | });
50 | });
51 |
52 | test.cb('reassigning websocket onclose listener should replace previous listeners', t => {
53 | const socketUrl = 'ws://localhost:8080';
54 | const mockServer = new Server(socketUrl);
55 | const mockSocket = new WebSocket(socketUrl);
56 |
57 | let firstListenerCalled = false;
58 | let secondListenerCalled = false;
59 |
60 | mockSocket.onclose = () => {
61 | firstListenerCalled = true;
62 | };
63 | mockSocket.onclose = () => {
64 | secondListenerCalled = true;
65 | };
66 |
67 | mockServer.close();
68 |
69 | t.false(firstListenerCalled, 'The first listener should not be called');
70 | t.true(secondListenerCalled, 'Only the second listener should be called');
71 | t.end();
72 | });
73 |
74 | test.cb('reassigning websocket onerror listener should replace previous listeners', t => {
75 | const socketUrl = 'ws://localhost:8080';
76 | const mockServer = new Server(socketUrl);
77 | const mockSocket = new WebSocket(socketUrl);
78 |
79 | let firstListenerCalled = false;
80 | let secondListenerCalled = false;
81 |
82 | mockSocket.onerror = () => {
83 | firstListenerCalled = true;
84 | };
85 | mockSocket.onerror = () => {
86 | secondListenerCalled = true;
87 | };
88 |
89 | mockServer.simulate('error');
90 |
91 | t.false(firstListenerCalled, 'The first listener should not be called');
92 | t.true(secondListenerCalled, 'Only the second listener should be called');
93 | mockServer.close();
94 | t.end();
95 | });
96 |
97 | test.cb('reassigning websocket null listener should clear previous listeners', t => {
98 | const socketUrl = 'ws://localhost:8080';
99 | const mockServer = new Server(socketUrl);
100 | const mockSocket = new WebSocket(socketUrl);
101 |
102 | let listenerCalled = false;
103 |
104 | mockSocket.onerror = () => {
105 | listenerCalled = true;
106 | };
107 | mockSocket.onerror = null;
108 |
109 | mockServer.simulate('error');
110 |
111 | t.false(listenerCalled, 'The first listener should not be called');
112 | t.end();
113 | });
114 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Mock Socket Changelog
2 |
3 | ### v9.3.1 (Sep 11th, 2023)
4 |
5 | - [#383](https://github.com/thoov/mock-socket/pull/383) [BUGFIX] Calling close in the CONNECTING state should not cause onopen to be called #383
6 |
7 | ### v9.3.0 (Sep 5th, 2023)
8 |
9 | - [#382](https://github.com/thoov/mock-socket/pull/382) The send() method now only throws when the socket is in the CONNECTING state
10 |
11 | ### v9.2.1 (Feb 14th, 2023)
12 |
13 | - [#376](https://github.com/thoov/mock-socket/pull/376) Do not normalize data when emitting to socket.io sockets
14 |
15 | ### v9.2.0 (Feb 9th, 2023)
16 |
17 | - [#373](https://github.com/thoov/mock-socket/pull/373) Handle proxy events properly
18 | - [#370](https://github.com/thoov/mock-socket/pull/370),
19 | [#368](https://github.com/thoov/mock-socket/pull/368),
20 | [#313](https://github.com/thoov/mock-socket/pull/313),
21 | [#333](https://github.com/thoov/mock-socket/pull/333),
22 | [#334](https://github.com/thoov/mock-socket/pull/334),
23 | [#369](https://github.com/thoov/mock-socket/pull/369),
24 | [#375](https://github.com/thoov/mock-socket/pull/375) Bump dependencies
25 |
26 | ### v9.1.5 (June 5th, 2022)
27 |
28 | - [#362](https://github.com/thoov/mock-socket/pull/362) [BUGFIX] Event handler getters should return a single function
29 |
30 | ### v9.1.4 (May 25th, 2022)
31 |
32 | - [#298](https://github.com/thoov/mock-socket/pull/298) [BUGFIX] close listener of the socket isn't called when client close
33 |
34 | ### v9.1.3 (April 20th, 2022)
35 |
36 | - [#355](https://github.com/thoov/mock-socket/pull/355) Bump url-parse from 1.5.2 to 1.5.9
37 |
38 | ### v9.1.2 (January 25th, 2022)
39 |
40 | - [#352](https://github.com/thoov/mock-socket/pull/352) fix SocketIO types
41 |
42 | ### v9.1.1 (January 25th, 2022)
43 |
44 | - [#351](https://github.com/thoov/mock-socket/pull/351) add types for SocketIO
45 |
46 | ### v9.1.0 (January 13th, 2022)
47 |
48 | - [#348](https://github.com/thoov/mock-socket/pull/348) [BUGFIX] Address misspelling of cancelBuble
49 | - [#349](https://github.com/thoov/mock-socket/pull/349) add mock options param to prevent stubbing global
50 |
51 | ### v9.0.8 (November 15th, 2021)
52 |
53 | - [#343](https://github.com/thoov/mock-socket/pull/343) trim query params for attachServer method lookup
54 | - [#335](https://github.com/thoov/mock-socket/pull/335) Update type of url parameter of websocket constructor
55 |
56 | ### v9.0.7 (November 1st, 2021)
57 |
58 | - [#342](https://github.com/thoov/mock-socket/pull/342) Accessing to websocket proxy via server clients
59 |
60 | ### v9.0.6 (October 18th, 2021)
61 |
62 | - [#338](https://github.com/thoov/mock-socket/pull/338) [BUGFIX] Use default codes for close event
63 | - [#340](https://github.com/thoov/mock-socket/pull/340) Build optimisations
64 |
65 | ### v9.0.5 (September 30th, 2021)
66 |
67 | - [#312](https://github.com/thoov/mock-socket/pull/312) [BUGFIX] Fix null pointer exceptions
68 | - [#296](https://github.com/thoov/mock-socket/pull/296) [BUGFIX] Add the hasListeners method to client socket
69 | - [#314](https://github.com/thoov/mock-socket/pull/314), \#316, \#318, \#321, \#323, \#330, \#331, \#332 Bump versions
70 | - [#336](https://github.com/thoov/mock-socket/pull/336) Remove src folder from npm
71 |
72 | ### v9.0.4 (September 27th, 2021)
73 |
74 | - [#240](https://github.com/thoov/mock-socket/pull/240) [BUGFIX] Fixed undefined readyState for WebSockets after closing connection in Server
75 | - [#328](https://github.com/thoov/mock-socket/pull/328) [BUGFIX] Use native typed arguments for WebSocket event handlers
76 |
77 | ### v9.0.2 (October 9th, 2019)
78 |
79 | - [#285](https://github.com/thoov/mock-socket/pull/285) [BUGFIX] Removing .git directory from npm package + cleanup
80 |
81 | ### v9.0.1 (October 5th, 2019)
82 |
83 | - [#281](https://github.com/thoov/mock-socket/pull/281) [BUGFIX] Updating the workflow
84 | - [#276](https://github.com/thoov/mock-socket/pull/276) [BUGFIX] Export ES Module format with .mjs extension
85 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for Mock Socket 8.X+
2 | // Project: Mock Socket
3 | // Definitions by: Travis Hoover
4 |
5 | declare module 'mock-socket' {
6 | // support TS under 3.5
7 | type _Omit = Pick>;
8 |
9 | class EventTarget {
10 | listeners: any;
11 | addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
12 | dispatchEvent(evt: Event): boolean;
13 | removeEventListener(type: string, listener?: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
14 | }
15 |
16 | interface WebSocketCallbackMap {
17 | close: () => void;
18 | error: (err: Error) => void;
19 | message: (message: string | Blob | ArrayBuffer | ArrayBufferView) => void;
20 | }
21 |
22 | //
23 | // https://html.spec.whatwg.org/multipage/web-sockets.html#websocket
24 | //
25 | class WebSocket extends EventTarget {
26 | constructor(url: string | URL, protocols?: string|string[]);
27 |
28 | static readonly CONNECTING: 0;
29 | static readonly OPEN: 1;
30 | static readonly CLOSING: 2;
31 | static readonly CLOSED: 3;
32 |
33 | readonly url: string;
34 |
35 | readonly CONNECTING: 0;
36 | readonly OPEN: 1;
37 | readonly CLOSING: 2;
38 | readonly CLOSED: 3;
39 | readonly readyState: number;
40 | readonly bufferedAmount: number;
41 |
42 | onopen: ((event: Event) => void) | null;
43 | onerror: ((event: Event) => void) | null;
44 | onclose: ((event: CloseEvent) => void) | null;
45 | onmessage: ((event: MessageEvent) => void) | null;
46 | readonly extensions: string;
47 | readonly protocol: string;
48 | close(code?: number, reason?: string): void;
49 |
50 | binaryType: BinaryType;
51 | send(data: string | Blob | ArrayBuffer | ArrayBufferView): void;
52 | }
53 |
54 | interface Client extends _Omit {
55 | target: WebSocket;
56 | close(options?: CloseOptions): void;
57 | on(type: K, callback: WebSocketCallbackMap[K]): void;
58 | off(type: K, callback: WebSocketCallbackMap[K]): void;
59 | }
60 |
61 | class Server extends EventTarget {
62 | constructor(url: string, options?: ServerOptions);
63 |
64 | readonly options?: ServerOptions;
65 |
66 | stop(callback?: () => void): void;
67 | mockWebsocket(): void;
68 | restoreWebsocket(): void;
69 |
70 | on(type: string, callback: (socket: Client) => void): void;
71 | off(type: string, callback: (socket: Client) => void): void;
72 | close(options?: CloseOptions): void;
73 | emit(event: string, data: any, options?: EmitOptions): void;
74 |
75 | clients(): Client[];
76 | to(room: any, broadcaster: any, broadcastList?: object): ToReturnObject;
77 | in(any: any): ToReturnObject;
78 | simulate(event: string): void;
79 |
80 | static of(url: string): Server;
81 | }
82 |
83 | interface SocketIOClient extends EventTarget {
84 | binaryType: BinaryType;
85 |
86 | readonly CONNECTING: 0;
87 | readonly OPEN: 1;
88 | readonly CLOSING: 2;
89 | readonly CLOSED: 3;
90 |
91 | readonly url: string;
92 | readonly readyState: number;
93 | readonly protocol: string;
94 | readonly target: this;
95 |
96 | close(): this;
97 | disconnect(): this;
98 | emit(event: string, data: any): this;
99 | send(data: any): this;
100 | on(type: string, callback: (socket: SocketIOClient) => void): this;
101 | off(type: string, callback: (socket: SocketIOClient) => void): void;
102 | hasListeners(type: string): boolean;
103 | join(room: string): void;
104 | leave(room: string): void;
105 | to(room: string): ToReturnObject;
106 | in(room: string): ToReturnObject;
107 |
108 | readonly broadcast: {
109 | emit(event: string, data: any): SocketIOClient;
110 | to(room: string): ToReturnObject;
111 | in(room: string): ToReturnObject;
112 | };
113 | }
114 |
115 | const SocketIO: {
116 | (url: string, protocol?: string | string[]): SocketIOClient;
117 | connect(url: string, protocol?: string | string[]): SocketIOClient;
118 | }
119 |
120 | interface CloseOptions {
121 | code: number;
122 | reason: string;
123 | wasClean: boolean;
124 | }
125 |
126 | interface EmitOptions {
127 | websockets: Client[];
128 | }
129 |
130 | interface ToReturnObject {
131 | to: (chainedRoom: any, chainedBroadcaster: any) => ToReturnObject;
132 | emit(event: Event, data: any): void;
133 | }
134 |
135 | interface ServerOptions {
136 | mock?: boolean;
137 | verifyClient?: () => boolean;
138 | selectProtocol?: (protocols: string[]) => string | null;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/network-bridge.js:
--------------------------------------------------------------------------------
1 | import { reject } from './helpers/array-helpers';
2 |
3 | function trimQueryPartFromURL(url) {
4 | const queryIndex = url.indexOf('?');
5 | return queryIndex >= 0 ? url.slice(0, queryIndex) : url;
6 | }
7 |
8 | /*
9 | * The network bridge is a way for the mock websocket object to 'communicate' with
10 | * all available servers. This is a singleton object so it is important that you
11 | * clean up urlMap whenever you are finished.
12 | */
13 | class NetworkBridge {
14 | constructor() {
15 | this.urlMap = {};
16 | }
17 |
18 | /*
19 | * Attaches a websocket object to the urlMap hash so that it can find the server
20 | * it is connected to and the server in turn can find it.
21 | *
22 | * @param {object} websocket - websocket object to add to the urlMap hash
23 | * @param {string} url
24 | */
25 | attachWebSocket(websocket, url) {
26 | const serverURL = trimQueryPartFromURL(url);
27 | const connectionLookup = this.urlMap[serverURL];
28 |
29 | if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) === -1) {
30 | connectionLookup.websockets.push(websocket);
31 | return connectionLookup.server;
32 | }
33 | }
34 |
35 | /*
36 | * Attaches a websocket to a room
37 | */
38 | addMembershipToRoom(websocket, room) {
39 | const connectionLookup = this.urlMap[trimQueryPartFromURL(websocket.url)];
40 |
41 | if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) !== -1) {
42 | if (!connectionLookup.roomMemberships[room]) {
43 | connectionLookup.roomMemberships[room] = [];
44 | }
45 |
46 | connectionLookup.roomMemberships[room].push(websocket);
47 | }
48 | }
49 |
50 | /*
51 | * Attaches a server object to the urlMap hash so that it can find a websockets
52 | * which are connected to it and so that websockets can in turn can find it.
53 | *
54 | * @param {object} server - server object to add to the urlMap hash
55 | * @param {string} url
56 | */
57 | attachServer(server, url) {
58 | const serverUrl = trimQueryPartFromURL(url);
59 | const connectionLookup = this.urlMap[serverUrl];
60 |
61 | if (!connectionLookup) {
62 | this.urlMap[serverUrl] = {
63 | server,
64 | websockets: [],
65 | roomMemberships: {}
66 | };
67 |
68 | return server;
69 | }
70 | }
71 |
72 | /*
73 | * Finds the server which is 'running' on the given url.
74 | *
75 | * @param {string} url - the url to use to find which server is running on it
76 | */
77 | serverLookup(url) {
78 | const serverURL = trimQueryPartFromURL(url);
79 | const connectionLookup = this.urlMap[serverURL];
80 |
81 | if (connectionLookup) {
82 | return connectionLookup.server;
83 | }
84 | }
85 |
86 | /*
87 | * Finds all websockets which is 'listening' on the given url.
88 | *
89 | * @param {string} url - the url to use to find all websockets which are associated with it
90 | * @param {string} room - if a room is provided, will only return sockets in this room
91 | * @param {class} broadcaster - socket that is broadcasting and is to be excluded from the lookup
92 | */
93 | websocketsLookup(url, room, broadcaster) {
94 | const serverURL = trimQueryPartFromURL(url);
95 | let websockets;
96 | const connectionLookup = this.urlMap[serverURL];
97 |
98 | websockets = connectionLookup ? connectionLookup.websockets : [];
99 |
100 | if (room) {
101 | const members = connectionLookup.roomMemberships[room];
102 | websockets = members || [];
103 | }
104 |
105 | return broadcaster ? websockets.filter(websocket => websocket !== broadcaster) : websockets;
106 | }
107 |
108 | /*
109 | * Removes the entry associated with the url.
110 | *
111 | * @param {string} url
112 | */
113 | removeServer(url) {
114 | delete this.urlMap[trimQueryPartFromURL(url)];
115 | }
116 |
117 | /*
118 | * Removes the individual websocket from the map of associated websockets.
119 | *
120 | * @param {object} websocket - websocket object to remove from the url map
121 | * @param {string} url
122 | */
123 | removeWebSocket(websocket, url) {
124 | const serverURL = trimQueryPartFromURL(url);
125 | const connectionLookup = this.urlMap[serverURL];
126 |
127 | if (connectionLookup) {
128 | connectionLookup.websockets = reject(connectionLookup.websockets, socket => socket === websocket);
129 | }
130 | }
131 |
132 | /*
133 | * Removes a websocket from a room
134 | */
135 | removeMembershipFromRoom(websocket, room) {
136 | const connectionLookup = this.urlMap[trimQueryPartFromURL(websocket.url)];
137 | const memberships = connectionLookup.roomMemberships[room];
138 |
139 | if (connectionLookup && memberships !== null) {
140 | connectionLookup.roomMemberships[room] = reject(memberships, socket => socket === websocket);
141 | }
142 | }
143 | }
144 |
145 | export default new NetworkBridge(); // Note: this is a singleton
146 |
--------------------------------------------------------------------------------
/tests/functional/socket-io.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import io from '../../src/socket-io';
3 | import Server from '../../src/server';
4 |
5 | test.cb('client triggers the server connection event', t => {
6 | const server = new Server('foobar');
7 | const socket = io('foobar');
8 |
9 | server.on('connection', () => {
10 | t.true(true);
11 | socket.disconnect();
12 | server.close();
13 | t.end();
14 | });
15 | });
16 |
17 | test.cb('client triggers the server connect event', t => {
18 | const server = new Server('foobar');
19 | const socket = io('foobar');
20 |
21 | server.on('connect', () => {
22 | t.true(true);
23 | socket.disconnect();
24 | server.close();
25 | t.end();
26 | });
27 | });
28 |
29 | test.cb('server triggers the client connect event', t => {
30 | const server = new Server('foobar');
31 | const socket = io('foobar');
32 |
33 | socket.on('connect', () => {
34 | t.true(true);
35 | socket.disconnect();
36 | server.close();
37 | t.end();
38 | });
39 | });
40 |
41 | test.cb('no connection triggers the client error event', t => {
42 | const socket = io('foobar');
43 |
44 | socket.on('error', () => {
45 | t.true(true);
46 | socket.disconnect();
47 | t.end();
48 | });
49 | });
50 |
51 | test.cb('client and server receive an event', t => {
52 | const server = new Server('foobar');
53 | server.on('client-event', data => {
54 | server.emit('server-response', data);
55 | });
56 |
57 | const socket = io('foobar');
58 | socket.on('server-response', data => {
59 | t.is('payload', data);
60 | socket.disconnect();
61 | server.close();
62 | t.end();
63 | });
64 |
65 | socket.on('connect', () => {
66 | socket.emit('client-event', 'payload');
67 | });
68 | });
69 |
70 | test.cb('Server closing triggers the client disconnect event', t => {
71 | const server = new Server('foobar');
72 | server.on('connect', () => {
73 | server.close();
74 | });
75 |
76 | const socket = io('foobar');
77 | socket.on('disconnect', () => {
78 | t.true(true);
79 | socket.disconnect();
80 | t.end();
81 | });
82 | });
83 |
84 | test.cb('Server receives disconnect when socket is closed', t => {
85 | const server = new Server('foobar');
86 | server.on('disconnect', () => {
87 | t.true(true);
88 | server.close();
89 | t.end();
90 | });
91 |
92 | const socket = io('foobar');
93 | socket.on('connect', () => {
94 | socket.disconnect();
95 | });
96 | });
97 |
98 | test.cb('Client can submit an event without a payload', t => {
99 | const server = new Server('foobar');
100 | server.on('client-event', () => {
101 | t.true(true);
102 | server.close();
103 | t.end();
104 | });
105 |
106 | const socket = io('foobar');
107 | socket.on('connect', () => {
108 | socket.emit('client-event');
109 | });
110 | });
111 |
112 | test.cb('Client also has the send method available', t => {
113 | const server = new Server('foobar');
114 | server.on('message', data => {
115 | t.is(data, 'hullo!');
116 | server.close();
117 | t.end();
118 | });
119 |
120 | const socket = io('foobar');
121 | socket.on('connect', () => {
122 | socket.send('hullo!');
123 | });
124 | });
125 |
126 | test.cb('a socket can join and leave a room', t => {
127 | const server = new Server('ws://roomy');
128 | const socket = io('ws://roomy');
129 |
130 | socket.on('good-response', () => {
131 | t.true(true);
132 | server.close();
133 | t.end();
134 | });
135 |
136 | socket.on('connect', () => {
137 | socket.join('room');
138 | server.to('room').emit('good-response');
139 | });
140 | });
141 |
142 | test.cb('a socket can emit to a room', t => {
143 | const server = new Server('ws://roomy');
144 | const socketFoo = io('ws://roomy');
145 | const socketBar = io('ws://roomy');
146 |
147 | socketFoo.on('connect', () => {
148 | socketFoo.join('room');
149 | });
150 | socketFoo.on('room-talk', () => {
151 | t.true(true);
152 | server.close();
153 | t.end();
154 | });
155 |
156 | socketBar.on('connect', () => {
157 | socketBar.join('room');
158 | socketBar.to('room').emit('room-talk');
159 | });
160 | });
161 |
162 | test.cb('Client can emit with multiple arguments', t => {
163 | const server = new Server('foobar');
164 | server.on('client-event', (...data) => {
165 | t.is(data.length, 3);
166 | t.is(data[0], 'foo');
167 | t.is(data[1], 'bar');
168 | t.is(data[2], 'baz');
169 | server.close();
170 | t.end();
171 | });
172 |
173 | const socket = io('foobar');
174 | socket.on('connect', () => {
175 | socket.emit('client-event', 'foo', 'bar', 'baz');
176 | });
177 | });
178 |
179 | test.cb('Server can emit with multiple arguments', t => {
180 | const server = new Server('foobar');
181 | server.on('connection', () => {
182 | server.emit('server-emit', 'foo', 'bar');
183 | });
184 |
185 | const socket = io('foobar');
186 | socket.on('server-emit', (...data) => {
187 | t.is(data.length, 2);
188 | t.is(data[0], 'foo');
189 | t.is(data[1], 'bar');
190 | server.close();
191 | t.end();
192 | });
193 | });
194 |
195 | test.cb('Server can emit to multiple rooms', t => {
196 | const server = new Server('ws://chat');
197 | const socket1 = io('ws://chat');
198 | const socket2 = io('ws://chat');
199 |
200 | let connectedCount = 0;
201 | const checkConnected = () => {
202 | connectedCount += 1;
203 | if (connectedCount === 2) {
204 | server
205 | .to('room1')
206 | .to('room2')
207 | .emit('good-response');
208 | }
209 | };
210 |
211 | let goodResponses = 0;
212 | const checkGoodResponses = socketId => {
213 | goodResponses += 1;
214 | if (goodResponses === 2) {
215 | t.true(true);
216 | server.close();
217 | t.end();
218 | }
219 | };
220 |
221 | socket1.on('good-response', checkGoodResponses.bind(null, 1));
222 | socket2.on('good-response', checkGoodResponses.bind(null, 2));
223 |
224 | socket1.on('connect', () => {
225 | socket1.join('room1');
226 | checkConnected();
227 | });
228 |
229 | socket2.on('connect', () => {
230 | socket2.join('room2');
231 | checkConnected();
232 | });
233 | });
234 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Javascript mocking library for websockets and socket.io
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Contents
16 |
17 | - [Installation](#installation)
18 | - [Basic Usage](#usage)
19 | - [Advanced Usage](#advanced-usage)
20 | - [Typescript Support](#typescript-support)
21 | - [Socket.IO](#socket-io)
22 | - [Contributing](#contributing)
23 | - [Feedback](#feedback)
24 |
25 | ## Installation
26 |
27 | ```shell
28 | npm install mock-socket
29 | ```
30 |
31 | ```js
32 | import { WebSocket, Server } from 'mock-socket';
33 | ```
34 |
35 | ## Usage
36 |
37 | ```js
38 | import test from 'ava';
39 | import { Server } from 'mock-socket';
40 |
41 | class ChatApp {
42 | constructor(url) {
43 | this.messages = [];
44 | this.connection = new WebSocket(url);
45 |
46 | this.connection.onmessage = event => {
47 | this.messages.push(event.data);
48 | };
49 | }
50 |
51 | sendMessage(message) {
52 | this.connection.send(message);
53 | }
54 | }
55 |
56 | test.cb('that chat app can be mocked', t => {
57 | const fakeURL = 'ws://localhost:8080';
58 | const mockServer = new Server(fakeURL);
59 |
60 | mockServer.on('connection', socket => {
61 | socket.on('message', data => {
62 | t.is(data, 'test message from app', 'we have intercepted the message and can assert on it');
63 | socket.send('test message from mock server');
64 | });
65 | });
66 |
67 | const app = new ChatApp(fakeURL);
68 | app.sendMessage('test message from app'); // NOTE: this line creates a micro task
69 |
70 | // NOTE: this timeout is for creating another micro task that will happen after the above one
71 | setTimeout(() => {
72 | t.is(app.messages.length, 1);
73 | t.is(app.messages[0], 'test message from mock server', 'we have stubbed our websocket backend');
74 | mockServer.stop(t.done);
75 | }, 100);
76 | });
77 | ```
78 |
79 | ## Advanced Usage
80 |
81 | ### Stubbing the "global"
82 |
83 | ```js
84 | import { WebSocket, Server } from 'mock-socket';
85 |
86 | /*
87 | * By default the global WebSocket object is stubbed out when
88 | * a new Server instance is created and is restored when you stop
89 | * the server.
90 | * However, you can disable this behavior by passing `mock: false`
91 | * to the options and manually mock the socket when you need it.
92 | */
93 | const server = new Server('ws://localhost:8080', { mock: false });
94 |
95 | /*
96 | * If you need to stub something else out you can like so:
97 | */
98 |
99 | window.WebSocket = WebSocket; // Here we stub out the window object
100 | ```
101 |
102 | ### Server Methods
103 |
104 | ```js
105 | const mockServer = new Server('ws://localhost:8080');
106 |
107 | mockServer.on('connection', socket => {
108 | socket.on('message', () => {});
109 | socket.on('close', () => {});
110 | socket.on('error', () => {});
111 |
112 | socket.send('message');
113 | socket.close();
114 | });
115 |
116 | mockServer.clients(); // array of all connected clients
117 | mockServer.emit('room', 'message');
118 | mockServer.stop(optionalCallback);
119 | ```
120 |
121 | ## Typescript Support
122 |
123 | A [declaration file](https://github.com/thoov/mock-socket/blob/master/index.d.ts) is included by default. If you notice any issues with the types please create an issue or a PR!
124 |
125 | ## Socket IO
126 |
127 | [Socket.IO](https://socket.io/) has **limited support**. Below is a similar example to the one above but modified to show off socket.io support.
128 |
129 | ```js
130 | import test from 'ava';
131 | import { SocketIO, Server } from 'mock-socket';
132 |
133 | class ChatApp {
134 | constructor(url) {
135 | this.messages = [];
136 | this.connection = new io(url);
137 |
138 | this.connection.on('chat-message', data => {
139 | this.messages.push(event.data);
140 | });
141 | }
142 |
143 | sendMessage(message) {
144 | this.connection.emit('chat-message', message);
145 | }
146 | }
147 |
148 | test.cb('that socket.io works', t => {
149 | const fakeURL = 'ws://localhost:8080';
150 | const mockServer = new Server(fakeURL);
151 |
152 | window.io = SocketIO;
153 |
154 | mockServer.on('connection', socket => {
155 | socket.on('chat-message', data => {
156 | t.is(data, 'test message from app', 'we have intercepted the message and can assert on it');
157 | socket.emit('chat-message', 'test message from mock server');
158 | });
159 | });
160 |
161 | const app = new ChatApp(fakeURL);
162 | app.sendMessage('test message from app');
163 |
164 | setTimeout(() => {
165 | t.is(app.messages.length, 1);
166 | t.is(app.messages[0], 'test message from mock server', 'we have subbed our websocket backend');
167 |
168 | mockServer.stop(t.done);
169 | }, 100);
170 | });
171 | ```
172 |
173 | ## Contributing
174 |
175 | The easiest way to work on the project is to clone the repo down via:
176 |
177 | ```shell
178 | git clone git@github.com:thoov/mock-socket.git
179 | cd mock-socket
180 | yarn install
181 | ```
182 |
183 | Then to create a local build via:
184 |
185 | ```shell
186 | yarn build
187 | ```
188 |
189 | Then create a local npm link via:
190 |
191 | ```shell
192 | yarn link
193 | ```
194 |
195 | At this point you can create other projects / apps locally and reference this local build via:
196 |
197 | ```shell
198 | yarn link mock-socket
199 | ```
200 |
201 | from within your other projects folder. Make sure that after any changes you run `yarn build`!
202 |
203 | ### Tests
204 |
205 | This project uses [ava.js](https://github.com/avajs/ava) as its test framework. Tests are located in /tests. To run tests:
206 |
207 | ```shell
208 | yarn test
209 | ```
210 |
211 | ### Linting
212 |
213 | This project uses eslint and a rules set from [airbnb's javascript style guides](https://github.com/airbnb/javascript). To run linting:
214 |
215 | ```shell
216 | yarn lint
217 | ```
218 |
219 | ### Formatting
220 |
221 | This project uses [prettier](https://github.com/prettier/prettier). To run the formatting:
222 |
223 | ```shell
224 | yarn format
225 | ```
226 |
227 | ### Code Coverage
228 |
229 | Code coverage reports are created in /coverage after all of the tests have successfully passed. To run the coverage:
230 |
231 | ```shell
232 | yarn test:coverage
233 | ```
234 |
235 | ## Feedback
236 |
237 | If you have any feedback, encounter any bugs, or just have a question, please feel free to create a [github issue](https://github.com/thoov/mock-socket/issues/new) or send me a tweet at [@thoov](https://twitter.com/thoov).
238 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import URL from 'url-parse';
2 | import WebSocket from './websocket';
3 | import { SocketIO } from './socket-io';
4 | import dedupe from './helpers/dedupe';
5 | import EventTarget from './event/target';
6 | import { CLOSE_CODES } from './constants';
7 | import networkBridge from './network-bridge';
8 | import globalObject from './helpers/global-object';
9 | import normalizeSendData from './helpers/normalize-send';
10 | import { createEvent, createMessageEvent, createCloseEvent } from './event/factory';
11 |
12 | const defaultOptions = {
13 | mock: true,
14 | verifyClient: null,
15 | selectProtocol: null
16 | };
17 |
18 | class Server extends EventTarget {
19 | constructor(url, options = defaultOptions) {
20 | super();
21 | const urlRecord = new URL(url);
22 |
23 | if (!urlRecord.pathname) {
24 | urlRecord.pathname = '/';
25 | }
26 |
27 | this.url = urlRecord.toString();
28 |
29 | this.originalWebSocket = null;
30 | const server = networkBridge.attachServer(this, this.url);
31 |
32 | if (!server) {
33 | this.dispatchEvent(createEvent({ type: 'error' }));
34 | throw new Error('A mock server is already listening on this url');
35 | }
36 |
37 | this.options = Object.assign({}, defaultOptions, options);
38 |
39 | if (this.options.mock) {
40 | this.mockWebsocket();
41 | }
42 | }
43 |
44 | /*
45 | * Attaches the mock websocket object to the global object
46 | */
47 | mockWebsocket() {
48 | const globalObj = globalObject();
49 |
50 | this.originalWebSocket = globalObj.WebSocket;
51 | globalObj.WebSocket = WebSocket;
52 | }
53 |
54 | /*
55 | * Removes the mock websocket object from the global object
56 | */
57 | restoreWebsocket() {
58 | const globalObj = globalObject();
59 |
60 | if (this.originalWebSocket !== null) {
61 | globalObj.WebSocket = this.originalWebSocket;
62 | }
63 |
64 | this.originalWebSocket = null;
65 | }
66 |
67 | /**
68 | * Removes itself from the urlMap so another server could add itself to the url.
69 | * @param {function} callback - The callback is called when the server is stopped
70 | */
71 | stop(callback = () => {}) {
72 | if (this.options.mock) {
73 | this.restoreWebsocket();
74 | }
75 |
76 | networkBridge.removeServer(this.url);
77 |
78 | if (typeof callback === 'function') {
79 | callback();
80 | }
81 | }
82 |
83 | /*
84 | * This is the main function for the mock server to subscribe to the on events.
85 | *
86 | * ie: mockServer.on('connection', function() { console.log('a mock client connected'); });
87 | *
88 | * @param {string} type - The event key to subscribe to. Valid keys are: connection, message, and close.
89 | * @param {function} callback - The callback which should be called when a certain event is fired.
90 | */
91 | on(type, callback) {
92 | this.addEventListener(type, callback);
93 | }
94 |
95 | /*
96 | * Remove event listener
97 | */
98 | off(type, callback) {
99 | this.removeEventListener(type, callback);
100 | }
101 |
102 | /*
103 | * Closes the connection and triggers the onclose method of all listening
104 | * websockets. After that it removes itself from the urlMap so another server
105 | * could add itself to the url.
106 | *
107 | * @param {object} options
108 | */
109 | close(options = {}) {
110 | const { code, reason, wasClean } = options;
111 | const listeners = networkBridge.websocketsLookup(this.url);
112 |
113 | // Remove server before notifications to prevent immediate reconnects from
114 | // socket onclose handlers
115 | networkBridge.removeServer(this.url);
116 |
117 | listeners.forEach(socket => {
118 | socket.readyState = WebSocket.CLOSED;
119 | socket.dispatchEvent(
120 | createCloseEvent({
121 | type: 'close',
122 | target: socket.target,
123 | code: code || CLOSE_CODES.CLOSE_NORMAL,
124 | reason: reason || '',
125 | wasClean
126 | })
127 | );
128 | });
129 |
130 | this.dispatchEvent(createCloseEvent({ type: 'close' }), this);
131 | }
132 |
133 | /*
134 | * Sends a generic message event to all mock clients.
135 | */
136 | emit(event, data, options = {}) {
137 | let { websockets } = options;
138 |
139 | if (!websockets) {
140 | websockets = networkBridge.websocketsLookup(this.url);
141 | }
142 |
143 | let normalizedData;
144 | if (typeof options !== 'object' || arguments.length > 3) {
145 | data = Array.prototype.slice.call(arguments, 1, arguments.length);
146 | normalizedData = data.map(item => normalizeSendData(item));
147 | } else {
148 | normalizedData = normalizeSendData(data);
149 | }
150 |
151 | websockets.forEach(socket => {
152 | const messageData = socket instanceof SocketIO ? data : normalizedData;
153 | if (Array.isArray(messageData)) {
154 | socket.dispatchEvent(
155 | createMessageEvent({
156 | type: event,
157 | data: messageData,
158 | origin: this.url,
159 | target: socket.target
160 | }),
161 | ...messageData
162 | );
163 | } else {
164 | socket.dispatchEvent(
165 | createMessageEvent({
166 | type: event,
167 | data: messageData,
168 | origin: this.url,
169 | target: socket.target
170 | })
171 | );
172 | }
173 | });
174 | }
175 |
176 | /*
177 | * Returns an array of websockets which are listening to this server
178 | * TOOD: this should return a set and not be a method
179 | */
180 | clients() {
181 | return networkBridge.websocketsLookup(this.url);
182 | }
183 |
184 | /*
185 | * Prepares a method to submit an event to members of the room
186 | *
187 | * e.g. server.to('my-room').emit('hi!');
188 | */
189 | to(room, broadcaster, broadcastList = []) {
190 | const self = this;
191 | const websockets = dedupe(broadcastList.concat(networkBridge.websocketsLookup(this.url, room, broadcaster)));
192 |
193 | return {
194 | to: (chainedRoom, chainedBroadcaster) => this.to.call(this, chainedRoom, chainedBroadcaster, websockets),
195 | emit(event, data) {
196 | self.emit(event, data, { websockets });
197 | }
198 | };
199 | }
200 |
201 | /*
202 | * Alias for Server.to
203 | */
204 | in(...args) {
205 | return this.to.apply(null, args);
206 | }
207 |
208 | /*
209 | * Simulate an event from the server to the clients. Useful for
210 | * simulating errors.
211 | */
212 | simulate(event) {
213 | const listeners = networkBridge.websocketsLookup(this.url);
214 |
215 | if (event === 'error') {
216 | listeners.forEach(socket => {
217 | socket.readyState = WebSocket.CLOSED;
218 | socket.dispatchEvent(createEvent({ type: 'error', target: socket.target }));
219 | });
220 | }
221 | }
222 | }
223 |
224 | /*
225 | * Alternative constructor to support namespaces in socket.io
226 | *
227 | * http://socket.io/docs/rooms-and-namespaces/#custom-namespaces
228 | */
229 | Server.of = function of(url) {
230 | return new Server(url);
231 | };
232 |
233 | export default Server;
234 |
--------------------------------------------------------------------------------
/src/websocket.js:
--------------------------------------------------------------------------------
1 | import delay from './helpers/delay';
2 | import logger from './helpers/logger';
3 | import EventTarget from './event/target';
4 | import networkBridge from './network-bridge';
5 | import proxyFactory from './helpers/proxy-factory';
6 | import lengthInUtf8Bytes from './helpers/byte-length';
7 | import { CLOSE_CODES, ERROR_PREFIX } from './constants';
8 | import urlVerification from './helpers/url-verification';
9 | import normalizeSendData from './helpers/normalize-send';
10 | import protocolVerification from './helpers/protocol-verification';
11 | import { createEvent, createMessageEvent, createCloseEvent } from './event/factory';
12 | import { closeWebSocketConnection, failWebSocketConnection } from './algorithms/close';
13 |
14 | /*
15 | * The main websocket class which is designed to mimick the native WebSocket class as close
16 | * as possible.
17 | *
18 | * https://html.spec.whatwg.org/multipage/web-sockets.html
19 | */
20 | class WebSocket extends EventTarget {
21 | constructor(url, protocols) {
22 | super();
23 |
24 | this._onopen = null;
25 | this._onmessage = null;
26 | this._onerror = null;
27 | this._onclose = null;
28 |
29 | this.url = urlVerification(url);
30 | protocols = protocolVerification(protocols);
31 | this.protocol = protocols[0] || '';
32 |
33 | this.binaryType = 'blob';
34 | this.readyState = WebSocket.CONNECTING;
35 |
36 | const client = proxyFactory(this);
37 | const server = networkBridge.attachWebSocket(client, this.url);
38 |
39 | /*
40 | * This delay is needed so that we dont trigger an event before the callbacks have been
41 | * setup. For example:
42 | *
43 | * var socket = new WebSocket('ws://localhost');
44 | *
45 | * If we dont have the delay then the event would be triggered right here and this is
46 | * before the onopen had a chance to register itself.
47 | *
48 | * socket.onopen = () => { // this would never be called };
49 | *
50 | * and with the delay the event gets triggered here after all of the callbacks have been
51 | * registered :-)
52 | */
53 | delay(function delayCallback() {
54 | if (this.readyState !== WebSocket.CONNECTING) {
55 | return;
56 | }
57 | if (server) {
58 | if (
59 | server.options.verifyClient &&
60 | typeof server.options.verifyClient === 'function' &&
61 | !server.options.verifyClient()
62 | ) {
63 | this.readyState = WebSocket.CLOSED;
64 |
65 | logger(
66 | 'error',
67 | `WebSocket connection to '${this.url}' failed: HTTP Authentication failed; no valid credentials available`
68 | );
69 |
70 | networkBridge.removeWebSocket(client, this.url);
71 | this.dispatchEvent(createEvent({ type: 'error', target: this }));
72 | this.dispatchEvent(createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL }));
73 | } else {
74 | if (server.options.selectProtocol && typeof server.options.selectProtocol === 'function') {
75 | const selectedProtocol = server.options.selectProtocol(protocols);
76 | const isFilled = selectedProtocol !== '';
77 | const isRequested = protocols.indexOf(selectedProtocol) !== -1;
78 | if (isFilled && !isRequested) {
79 | this.readyState = WebSocket.CLOSED;
80 |
81 | logger('error', `WebSocket connection to '${this.url}' failed: Invalid Sub-Protocol`);
82 |
83 | networkBridge.removeWebSocket(client, this.url);
84 | this.dispatchEvent(createEvent({ type: 'error', target: this }));
85 | this.dispatchEvent(createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL }));
86 | return;
87 | }
88 | this.protocol = selectedProtocol;
89 | }
90 | this.readyState = WebSocket.OPEN;
91 | this.dispatchEvent(createEvent({ type: 'open', target: this }));
92 | server.dispatchEvent(createEvent({ type: 'connection' }), client);
93 | }
94 | } else {
95 | this.readyState = WebSocket.CLOSED;
96 | this.dispatchEvent(createEvent({ type: 'error', target: this }));
97 | this.dispatchEvent(createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL }));
98 |
99 | logger('error', `WebSocket connection to '${this.url}' failed`);
100 | }
101 | }, this);
102 | }
103 |
104 | get onopen() {
105 | return this._onopen;
106 | }
107 |
108 | get onmessage() {
109 | return this._onmessage;
110 | }
111 |
112 | get onclose() {
113 | return this._onclose;
114 | }
115 |
116 | get onerror() {
117 | return this._onerror;
118 | }
119 |
120 | set onopen(listener) {
121 | this.removeEventListener('open', this._onopen);
122 | this._onopen = listener;
123 | this.addEventListener('open', listener);
124 | }
125 |
126 | set onmessage(listener) {
127 | this.removeEventListener('message', this._onmessage);
128 | this._onmessage = listener;
129 | this.addEventListener('message', listener);
130 | }
131 |
132 | set onclose(listener) {
133 | this.removeEventListener('close', this._onclose);
134 | this._onclose = listener;
135 | this.addEventListener('close', listener);
136 | }
137 |
138 | set onerror(listener) {
139 | this.removeEventListener('error', this._onerror);
140 | this._onerror = listener;
141 | this.addEventListener('error', listener);
142 | }
143 |
144 | send(data) {
145 | if (this.readyState === WebSocket.CONNECTING) {
146 | // TODO: node>=17 replace with DOMException
147 | throw new Error("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state");
148 | }
149 |
150 | // TODO: handle bufferedAmount
151 |
152 | const messageEvent = createMessageEvent({
153 | type: 'server::message',
154 | origin: this.url,
155 | data: normalizeSendData(data)
156 | });
157 |
158 | const server = networkBridge.serverLookup(this.url);
159 |
160 | if (server) {
161 | delay(() => {
162 | this.dispatchEvent(messageEvent, data);
163 | }, server);
164 | }
165 | }
166 |
167 | close(code, reason) {
168 | if (code !== undefined) {
169 | if (typeof code !== 'number' || (code !== 1000 && (code < 3000 || code > 4999))) {
170 | throw new TypeError(
171 | `${ERROR_PREFIX.CLOSE_ERROR} The code must be either 1000, or between 3000 and 4999. ${code} is neither.`
172 | );
173 | }
174 | }
175 |
176 | if (reason !== undefined) {
177 | const length = lengthInUtf8Bytes(reason);
178 |
179 | if (length > 123) {
180 | throw new SyntaxError(`${ERROR_PREFIX.CLOSE_ERROR} The message must not be greater than 123 bytes.`);
181 | }
182 | }
183 |
184 | if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) {
185 | return;
186 | }
187 |
188 | const client = proxyFactory(this);
189 | if (this.readyState === WebSocket.CONNECTING) {
190 | failWebSocketConnection(client, code || CLOSE_CODES.CLOSE_ABNORMAL, reason);
191 | } else {
192 | closeWebSocketConnection(client, code || CLOSE_CODES.CLOSE_NO_STATUS, reason);
193 | }
194 | }
195 | }
196 |
197 | WebSocket.CONNECTING = 0;
198 | WebSocket.prototype.CONNECTING = WebSocket.CONNECTING;
199 | WebSocket.OPEN = 1;
200 | WebSocket.prototype.OPEN = WebSocket.OPEN;
201 | WebSocket.CLOSING = 2;
202 | WebSocket.prototype.CLOSING = WebSocket.CLOSING;
203 | WebSocket.CLOSED = 3;
204 | WebSocket.prototype.CLOSED = WebSocket.CLOSED;
205 |
206 | export default WebSocket;
207 |
--------------------------------------------------------------------------------
/src/socket-io.js:
--------------------------------------------------------------------------------
1 | import URL from 'url-parse';
2 | import delay from './helpers/delay';
3 | import EventTarget from './event/target';
4 | import networkBridge from './network-bridge';
5 | import { CLOSE_CODES } from './constants';
6 | import logger from './helpers/logger';
7 | import { createEvent, createMessageEvent, createCloseEvent } from './event/factory';
8 |
9 | /*
10 | * The socket-io class is designed to mimick the real API as closely as possible.
11 | *
12 | * http://socket.io/docs/
13 | */
14 | export class SocketIO extends EventTarget {
15 | /*
16 | * @param {string} url
17 | */
18 | constructor(url = 'socket.io', protocol = '') {
19 | super();
20 |
21 | this.binaryType = 'blob';
22 | const urlRecord = new URL(url);
23 |
24 | if (!urlRecord.pathname) {
25 | urlRecord.pathname = '/';
26 | }
27 |
28 | this.url = urlRecord.toString();
29 | this.readyState = SocketIO.CONNECTING;
30 | this.protocol = '';
31 | this.target = this;
32 |
33 | if (typeof protocol === 'string' || (typeof protocol === 'object' && protocol !== null)) {
34 | this.protocol = protocol;
35 | } else if (Array.isArray(protocol) && protocol.length > 0) {
36 | this.protocol = protocol[0];
37 | }
38 |
39 | const server = networkBridge.attachWebSocket(this, this.url);
40 |
41 | /*
42 | * Delay triggering the connection events so they can be defined in time.
43 | */
44 | delay(function delayCallback() {
45 | if (server) {
46 | this.readyState = SocketIO.OPEN;
47 | server.dispatchEvent(createEvent({ type: 'connection' }), server, this);
48 | server.dispatchEvent(createEvent({ type: 'connect' }), server, this); // alias
49 | this.dispatchEvent(createEvent({ type: 'connect', target: this }));
50 | } else {
51 | this.readyState = SocketIO.CLOSED;
52 | this.dispatchEvent(createEvent({ type: 'error', target: this }));
53 | this.dispatchEvent(
54 | createCloseEvent({
55 | type: 'close',
56 | target: this,
57 | code: CLOSE_CODES.CLOSE_NORMAL
58 | })
59 | );
60 |
61 | logger('error', `Socket.io connection to '${this.url}' failed`);
62 | }
63 | }, this);
64 |
65 | /**
66 | Add an aliased event listener for close / disconnect
67 | */
68 | this.addEventListener('close', event => {
69 | this.dispatchEvent(
70 | createCloseEvent({
71 | type: 'disconnect',
72 | target: event.target,
73 | code: event.code
74 | })
75 | );
76 | });
77 | }
78 |
79 | /*
80 | * Closes the SocketIO connection or connection attempt, if any.
81 | * If the connection is already CLOSED, this method does nothing.
82 | */
83 | close() {
84 | if (this.readyState !== SocketIO.OPEN) {
85 | return undefined;
86 | }
87 |
88 | const server = networkBridge.serverLookup(this.url);
89 | networkBridge.removeWebSocket(this, this.url);
90 |
91 | this.readyState = SocketIO.CLOSED;
92 | this.dispatchEvent(
93 | createCloseEvent({
94 | type: 'close',
95 | target: this,
96 | code: CLOSE_CODES.CLOSE_NORMAL
97 | })
98 | );
99 |
100 | if (server) {
101 | server.dispatchEvent(
102 | createCloseEvent({
103 | type: 'disconnect',
104 | target: this,
105 | code: CLOSE_CODES.CLOSE_NORMAL
106 | }),
107 | server
108 | );
109 | }
110 |
111 | return this;
112 | }
113 |
114 | /*
115 | * Alias for Socket#close
116 | *
117 | * https://github.com/socketio/socket.io-client/blob/master/lib/socket.js#L383
118 | */
119 | disconnect() {
120 | return this.close();
121 | }
122 |
123 | /*
124 | * Submits an event to the server with a payload
125 | */
126 | emit(event, ...data) {
127 | if (this.readyState !== SocketIO.OPEN) {
128 | throw new Error('SocketIO is already in CLOSING or CLOSED state');
129 | }
130 |
131 | const messageEvent = createMessageEvent({
132 | type: event,
133 | origin: this.url,
134 | data
135 | });
136 |
137 | const server = networkBridge.serverLookup(this.url);
138 |
139 | if (server) {
140 | server.dispatchEvent(messageEvent, ...data);
141 | }
142 |
143 | return this;
144 | }
145 |
146 | /*
147 | * Submits a 'message' event to the server.
148 | *
149 | * Should behave exactly like WebSocket#send
150 | *
151 | * https://github.com/socketio/socket.io-client/blob/master/lib/socket.js#L113
152 | */
153 | send(data) {
154 | this.emit('message', data);
155 | return this;
156 | }
157 |
158 | /*
159 | * For broadcasting events to other connected sockets.
160 | *
161 | * e.g. socket.broadcast.emit('hi!');
162 | * e.g. socket.broadcast.to('my-room').emit('hi!');
163 | */
164 | get broadcast() {
165 | if (this.readyState !== SocketIO.OPEN) {
166 | throw new Error('SocketIO is already in CLOSING or CLOSED state');
167 | }
168 |
169 | const self = this;
170 | const server = networkBridge.serverLookup(this.url);
171 | if (!server) {
172 | throw new Error(`SocketIO can not find a server at the specified URL (${this.url})`);
173 | }
174 |
175 | return {
176 | emit(event, data) {
177 | server.emit(event, data, { websockets: networkBridge.websocketsLookup(self.url, null, self) });
178 | return self;
179 | },
180 | to(room) {
181 | return server.to(room, self);
182 | },
183 | in(room) {
184 | return server.in(room, self);
185 | }
186 | };
187 | }
188 |
189 | /*
190 | * For registering events to be received from the server
191 | */
192 | on(type, callback) {
193 | this.addEventListener(type, callback);
194 | return this;
195 | }
196 |
197 | /*
198 | * Remove event listener
199 | *
200 | * https://github.com/component/emitter#emitteroffevent-fn
201 | */
202 | off(type, callback) {
203 | this.removeEventListener(type, callback);
204 | }
205 |
206 | /*
207 | * Check if listeners have already been added for an event
208 | *
209 | * https://github.com/component/emitter#emitterhaslistenersevent
210 | */
211 | hasListeners(type) {
212 | const listeners = this.listeners[type];
213 | if (!Array.isArray(listeners)) {
214 | return false;
215 | }
216 | return !!listeners.length;
217 | }
218 |
219 | /*
220 | * Join a room on a server
221 | *
222 | * http://socket.io/docs/rooms-and-namespaces/#joining-and-leaving
223 | */
224 | join(room) {
225 | networkBridge.addMembershipToRoom(this, room);
226 | }
227 |
228 | /*
229 | * Get the websocket to leave the room
230 | *
231 | * http://socket.io/docs/rooms-and-namespaces/#joining-and-leaving
232 | */
233 | leave(room) {
234 | networkBridge.removeMembershipFromRoom(this, room);
235 | }
236 |
237 | to(room) {
238 | return this.broadcast.to(room);
239 | }
240 |
241 | in() {
242 | return this.to.apply(null, arguments);
243 | }
244 |
245 | /*
246 | * Invokes all listener functions that are listening to the given event.type property. Each
247 | * listener will be passed the event as the first argument.
248 | *
249 | * @param {object} event - event object which will be passed to all listeners of the event.type property
250 | */
251 | dispatchEvent(event, ...customArguments) {
252 | const eventName = event.type;
253 | const listeners = this.listeners[eventName];
254 |
255 | if (!Array.isArray(listeners)) {
256 | return false;
257 | }
258 |
259 | listeners.forEach(listener => {
260 | if (customArguments.length > 0) {
261 | listener.apply(this, customArguments);
262 | } else {
263 | // Regular WebSockets expect a MessageEvent but Socketio.io just wants raw data
264 | // payload instanceof MessageEvent works, but you can't isntance of NodeEvent
265 | // for now we detect if the output has data defined on it
266 | listener.call(this, event.data ? event.data : event);
267 | }
268 | });
269 | }
270 | }
271 |
272 | SocketIO.CONNECTING = 0;
273 | SocketIO.OPEN = 1;
274 | SocketIO.CLOSING = 2;
275 | SocketIO.CLOSED = 3;
276 |
277 | /*
278 | * Static constructor methods for the IO Socket
279 | */
280 | const IO = function ioConstructor(url, protocol) {
281 | return new SocketIO(url, protocol);
282 | };
283 |
284 | /*
285 | * Alias the raw IO() constructor
286 | */
287 | IO.connect = function ioConnect(url, protocol) {
288 | /* eslint-disable new-cap */
289 | return IO(url, protocol);
290 | /* eslint-enable new-cap */
291 | };
292 |
293 | export default IO;
294 |
--------------------------------------------------------------------------------
/tests/unit/server.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 | import EventTarget from '../../src/event/target';
5 | import networkBridge from '../../src/network-bridge';
6 | import globalObject from '../../src/helpers/global-object';
7 |
8 | test('that server inherents EventTarget methods', t => {
9 | const myServer = new Server('ws://not-real');
10 | t.true(myServer instanceof EventTarget);
11 | myServer.close();
12 | });
13 |
14 | test('that after creating a server it is added to the network bridge', t => {
15 | const myServer = new Server('ws://not-real/');
16 | const urlMap = networkBridge.urlMap['ws://not-real/'];
17 |
18 | t.deepEqual(urlMap.server, myServer, 'server was correctly added to the urlMap');
19 | myServer.close();
20 | t.deepEqual(networkBridge.urlMap, {}, 'the urlMap was cleared after the close call');
21 | });
22 |
23 | test('that callback functions can be added and removed from the listeners object', t => {
24 | const myServer = new Server('ws://not-real/');
25 |
26 | const onMessage = () => {};
27 | const onError = () => {};
28 | myServer.on('message', onMessage);
29 | myServer.on('close', onError);
30 |
31 | t.is(myServer.listeners.message.length, 1);
32 | t.is(myServer.listeners.close.length, 1);
33 |
34 | myServer.off('message', onMessage);
35 | myServer.off('close', onError);
36 |
37 | t.is(myServer.listeners.message.length, 0);
38 | t.is(myServer.listeners.close.length, 0);
39 |
40 | myServer.close();
41 | });
42 |
43 | test('that calling clients() returns the correct clients', t => {
44 | const myServer = new Server('ws://not-real/');
45 | const socketFoo = new WebSocket('ws://not-real/');
46 | const socketBar = new WebSocket('ws://not-real/');
47 |
48 | t.is(myServer.clients().length, 2, 'calling clients returns the 2 websockets');
49 | t.deepEqual(myServer.clients(), [socketFoo, socketBar], 'The clients matches [socketFoo, socketBar]');
50 |
51 | myServer.close();
52 | });
53 |
54 | test.cb('that calling close will trigger the onclose of websockets', t => {
55 | const myServer = new Server('ws://not-real/');
56 | let counter = 0;
57 |
58 | myServer.on('connection', () => {
59 | counter += 1;
60 | if (counter === 2) {
61 | myServer.close({
62 | code: 1005,
63 | reason: 'Some reason'
64 | });
65 | }
66 | });
67 |
68 | const socketFoo = new WebSocket('ws://not-real/');
69 | const socketBar = new WebSocket('ws://not-real/');
70 | socketFoo.onclose = event => {
71 | t.true(true, 'socketFoo onmessage was correctly called');
72 | t.is(event.code, 1005, 'the correct code was recieved');
73 | t.is(event.reason, 'Some reason', 'the correct reason was recieved');
74 | };
75 |
76 | socketBar.onclose = event => {
77 | t.pass(true, 'socketBar onmessage was correctly called');
78 | t.is(event.code, 1005, 'the correct code was recieved');
79 | t.is(event.reason, 'Some reason', 'the correct reason was recieved');
80 | t.end();
81 | };
82 | });
83 |
84 | test.cb('that calling close for each client will trigger the onclose of websockets', t => {
85 | const myServer = new Server('ws://not-real/');
86 | let counter = 0;
87 |
88 | myServer.on('connection', () => {
89 | counter += 1;
90 | if (counter === 2) {
91 | myServer.clients()[0].close({
92 | code: 1000
93 | });
94 | myServer.clients()[1].close({
95 | code: 1005,
96 | reason: 'Some reason'
97 | });
98 | }
99 | });
100 |
101 | const socketFoo = new WebSocket('ws://not-real/');
102 | const socketBar = new WebSocket('ws://not-real/');
103 | socketFoo.onclose = event => {
104 | t.true(true, 'socketFoo onclose was correctly called');
105 | t.is(event.code, 1000, 'the correct code was recieved');
106 | t.is(event.reason, '', 'there is no reason');
107 | };
108 |
109 | socketBar.onclose = event => {
110 | t.pass(true, 'socketBar onclose was correctly called');
111 | t.is(event.code, 1005, 'the correct code was recieved');
112 | t.is(event.reason, 'Some reason', 'the correct reason was recieved');
113 | myServer.close();
114 | t.end();
115 | };
116 | });
117 |
118 | test('a namespaced server is added to the network bridge', t => {
119 | const myServer = Server.of('/my-namespace');
120 | const urlMap = networkBridge.urlMap['/my-namespace'];
121 |
122 | t.deepEqual(urlMap.server, myServer, 'server was correctly added to the urlMap');
123 | myServer.close();
124 | t.deepEqual(networkBridge.urlMap, {}, 'the urlMap was cleared after the close call');
125 | });
126 |
127 | test('that properly mock the global Websocket object and restore when the server closes', t => {
128 | const globalObj = globalObject();
129 | const originalWebSocket = globalObj.WebSocket;
130 | const myServer = new Server('ws://example.com');
131 |
132 | t.deepEqual(globalObj.WebSocket, WebSocket, 'WebSocket class is defined on the globalObject');
133 | t.deepEqual(myServer.originalWebSocket, originalWebSocket, 'the original websocket is stored');
134 |
135 | myServer.stop();
136 |
137 | t.is(myServer.originalWebSocket, null, 'server forgets about the original websocket');
138 | t.deepEqual(globalObj.WebSocket, originalWebSocket, 'the original websocket is returned to the global object');
139 | });
140 |
141 | test('that does not mock the global Websocket object with mock=false', t => {
142 | const globalObj = globalObject();
143 | const originalWebSocket = globalObj.WebSocket;
144 | const myServer = new Server('ws://example.com', { mock: false });
145 |
146 | t.deepEqual(globalObj.WebSocket, originalWebSocket, 'global websocket is not mocked');
147 | t.deepEqual(myServer.originalWebSocket, null, 'server does not store original websocket');
148 |
149 | myServer.mockWebsocket();
150 |
151 | t.deepEqual(globalObj.WebSocket, WebSocket, 'WebSocket class is defined on the globalObject');
152 | t.deepEqual(myServer.originalWebSocket, originalWebSocket, 'the original websocket is stored');
153 |
154 | myServer.restoreWebsocket();
155 | myServer.stop();
156 |
157 | t.is(myServer.originalWebSocket, null, 'server forgets about the original websocket');
158 | t.deepEqual(globalObj.WebSocket, originalWebSocket, 'the original websocket is returned to the global object');
159 | });
160 |
161 | test.cb('that send will normalize data', t => {
162 | const myServer = new Server('ws://not-real/');
163 |
164 | myServer.on('connection', socket => {
165 | socket.send([1, 2]);
166 | });
167 |
168 | const socketFoo = new WebSocket('ws://not-real/');
169 | socketFoo.onmessage = message => {
170 | t.is(message.data, '1,2', 'data non string, non blob/arraybuffers get toStringed');
171 | myServer.close();
172 | t.end();
173 | };
174 | });
175 |
176 | test.cb('that the server socket callback argument is correctly scoped: send method', t => {
177 | const myServer = new Server('ws://not-real/');
178 | let counter = 0;
179 |
180 | myServer.on('connection', socket => {
181 | counter += 1;
182 | socket.send('a message');
183 | });
184 |
185 | const socket1 = new WebSocket('ws://not-real/');
186 | const socket2 = new WebSocket('ws://not-real/');
187 | socket1.onmessage = message => {
188 | t.is(message.data, 'a message');
189 | t.is(counter, 1);
190 | };
191 | socket2.onmessage = message => {
192 | t.is(message.data, 'a message');
193 | t.is(counter, 2);
194 | myServer.close();
195 | t.end();
196 | };
197 | });
198 |
199 | test.cb('that the server socket callback argument is correctly scoped: on method', t => {
200 | const myServer = new Server('ws://not-real/');
201 | let counter = 0;
202 |
203 | myServer.on('connection', socket => {
204 | socket.on('message', data => {
205 | counter += 1;
206 |
207 | t.is(data, `hello${counter}`);
208 | if (counter === 2) {
209 | myServer.close();
210 | t.end();
211 | }
212 | });
213 | });
214 |
215 | const socket1 = new WebSocket('ws://not-real/');
216 | socket1.addEventListener('open', () => {
217 | socket1.send('hello1');
218 | });
219 | const socket2 = new WebSocket('ws://not-real/');
220 | socket2.addEventListener('open', () => {
221 | socket2.send('hello2');
222 | });
223 | });
224 |
225 | test.cb('that the server socket callback argument is correctly scoped: close method', t => {
226 | const myServer = new Server('ws://not-real/');
227 |
228 | myServer.on('connection', socket => {
229 | socket.on('message', data => {
230 | socket.close({ code: parseInt(data, 10) });
231 | });
232 | });
233 |
234 | const socket1 = new WebSocket('ws://not-real/');
235 | socket1.addEventListener('open', () => {
236 | socket1.send('1001');
237 | });
238 | const socket2 = new WebSocket('ws://not-real/');
239 | socket2.addEventListener('open', () => {
240 | socket2.send('1002');
241 | });
242 |
243 | socket1.onclose = event => {
244 | t.is(event.code, 1001);
245 | };
246 |
247 | socket2.onclose = event => {
248 | t.is(event.code, 1002);
249 | myServer.close();
250 | t.end();
251 | };
252 | });
253 |
--------------------------------------------------------------------------------
/tests/unit/network-bridge.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 |
3 | import networkBridge from '../../src/network-bridge';
4 |
5 | const fakeObject = { foo: 'bar' };
6 |
7 | test.beforeEach(() => {
8 | networkBridge.urlMap = {};
9 | });
10 |
11 | test('that url query parameters are ignored', t => {
12 | networkBridge.attachServer(fakeObject, 'wss://not-real/');
13 | networkBridge.attachWebSocket({}, 'wss://not-real/?foo=42');
14 | networkBridge.attachWebSocket({}, 'wss://not-real/?foo=0');
15 | const connection = networkBridge.urlMap['wss://not-real/'];
16 | t.is(connection.websockets.length, 2, 'two websockets have been attached to the same connection');
17 | });
18 |
19 | test('that network bridge has no connections by default', t => {
20 | t.deepEqual(networkBridge.urlMap, {}, 'Url map is empty by default');
21 | });
22 |
23 | test('that network bridge has no connections by default', t => {
24 | const result = networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080');
25 |
26 | t.truthy(!result, 'no server was returned as a server must be added first');
27 | t.deepEqual(networkBridge.urlMap, {}, 'nothing was added to the url map');
28 | });
29 |
30 | test('that attachServer adds a server to url map', t => {
31 | const result = networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
32 | const connection = networkBridge.urlMap['ws://localhost:8080'];
33 |
34 | t.deepEqual(result, fakeObject, 'the server was returned because it was successfully added to the url map');
35 | t.deepEqual(connection.server, fakeObject, 'fakeObject was added to the server property');
36 | t.is(connection.websockets.length, 0, 'websocket property was set to an empty array');
37 | });
38 |
39 | test('that attachServer does nothing if a server is already attached to a given url', t => {
40 | const result = networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
41 | const result2 = networkBridge.attachServer({ hello: 'world' }, 'ws://localhost:8080');
42 | const connection = networkBridge.urlMap['ws://localhost:8080'];
43 |
44 | t.truthy(!result2, 'no server was returned as a server was already listening to that url');
45 | t.deepEqual(result, fakeObject, 'the server was returned because it was successfully added to the url map');
46 | t.deepEqual(connection.server, fakeObject, 'fakeObject was added to the server property');
47 | t.is(connection.websockets.length, 0, 'websocket property was set to an empty array');
48 | });
49 |
50 | test('that attachWebSocket will add a websocket to the url map', t => {
51 | const resultServer = networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
52 | const resultWebSocket = networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080');
53 | const connection = networkBridge.urlMap['ws://localhost:8080'];
54 |
55 | t.deepEqual(resultServer, fakeObject, 'server returned because it was successfully added to the url map');
56 | t.deepEqual(resultWebSocket, fakeObject, 'server returned as the websocket was successfully added to the map');
57 | t.deepEqual(connection.websockets[0], fakeObject, 'fakeObject was added to the websockets array');
58 | t.is(connection.websockets.length, 1, 'websocket property contains only the websocket object');
59 | });
60 |
61 | test('that attachWebSocket will add a websocket with query params to the url map', t => {
62 | const resultServer = networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
63 | const resultWebSocket = networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080?foo=bar');
64 | const connection = networkBridge.urlMap['ws://localhost:8080'];
65 |
66 | t.deepEqual(resultServer, fakeObject, 'server returned because it was successfully added to the url map');
67 | t.deepEqual(resultWebSocket, fakeObject, 'server returned as the websocket was successfully added to the map');
68 | t.deepEqual(connection.websockets[0], fakeObject, 'fakeObject was added to the websockets array');
69 | t.is(connection.websockets.length, 1, 'websocket property contains only the websocket object');
70 | });
71 |
72 | test('that attachWebSocket will add the same websocket only once', t => {
73 | const resultServer = networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
74 | const resultWebSocket = networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080');
75 | const resultWebSocket2 = networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080');
76 | const connection = networkBridge.urlMap['ws://localhost:8080'];
77 |
78 | t.deepEqual(resultServer, fakeObject, 'server returned because it was successfully added to the url map');
79 | t.deepEqual(resultWebSocket, fakeObject, 'server returned as the websocket was successfully added to the map');
80 | t.truthy(!resultWebSocket2, 'nothing added as the websocket already existed inside the url map');
81 | t.deepEqual(connection.websockets[0], fakeObject, 'fakeObject was added to the websockets array');
82 | t.is(connection.websockets.length, 1, 'websocket property contains only the websocket object');
83 | });
84 |
85 | test('that server and websocket lookups return the correct objects', t => {
86 | networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
87 | networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080');
88 |
89 | const serverLookup = networkBridge.serverLookup('ws://localhost:8080');
90 | const websocketLookup = networkBridge.websocketsLookup('ws://localhost:8080');
91 |
92 | t.deepEqual(serverLookup, fakeObject, 'server correctly returned');
93 | t.deepEqual(websocketLookup, [fakeObject], 'websockets correctly returned');
94 | t.deepEqual(websocketLookup.length, 1, 'the correct number of websockets are returned');
95 | });
96 |
97 | test('that server and websocket lookups ignore query params', t => {
98 | networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
99 | networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080?foo=bar');
100 |
101 | const serverLookup = networkBridge.serverLookup('ws://localhost:8080?foo1=1');
102 | const websocketLookup = networkBridge.websocketsLookup('ws://localhost:8080?foo2=2');
103 |
104 | t.deepEqual(serverLookup, fakeObject, 'server correctly returned');
105 | t.deepEqual(websocketLookup, [fakeObject], 'websockets correctly returned');
106 | t.deepEqual(websocketLookup.length, 1, 'the correct number of websockets are returned');
107 | });
108 |
109 | test('that removing server and websockets works correctly', t => {
110 | networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
111 | networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080');
112 |
113 | let websocketLookup = networkBridge.websocketsLookup('ws://localhost:8080');
114 | t.deepEqual(websocketLookup.length, 1, 'the correct number of websockets are returned');
115 |
116 | networkBridge.removeWebSocket(fakeObject, 'ws://localhost:8080');
117 |
118 | websocketLookup = networkBridge.websocketsLookup('ws://localhost:8080');
119 | t.deepEqual(websocketLookup.length, 0, 'the correct number of websockets are returned');
120 |
121 | networkBridge.removeServer('ws://localhost:8080');
122 | t.deepEqual(networkBridge.urlMap, {}, 'Url map is back in its default state');
123 | });
124 |
125 | test('that removing server and websockets works correctly with query params', t => {
126 | networkBridge.attachServer(fakeObject, 'ws://localhost:8080');
127 | networkBridge.attachWebSocket(fakeObject, 'ws://localhost:8080?foo=bar');
128 |
129 | let websocketLookup = networkBridge.websocketsLookup('ws://localhost:8080?anything=else');
130 | t.deepEqual(websocketLookup.length, 1, 'the correct number of websockets are returned');
131 |
132 | networkBridge.removeWebSocket(fakeObject, 'ws://localhost:8080?arbitraryParameter');
133 |
134 | websocketLookup = networkBridge.websocketsLookup('ws://localhost:8080?one=more');
135 | t.deepEqual(websocketLookup.length, 0, 'the correct number of websockets are returned');
136 |
137 | networkBridge.removeServer('ws://localhost:8080?please');
138 | t.deepEqual(networkBridge.urlMap, {}, 'Url map is back in its default state');
139 | });
140 |
141 | test('a socket can join and leave a room', t => {
142 | const fakeSocket = { url: 'ws://roomy' };
143 |
144 | networkBridge.attachServer(fakeObject, 'ws://roomy');
145 | networkBridge.attachWebSocket(fakeSocket, 'ws://roomy');
146 |
147 | let inRoom;
148 | inRoom = networkBridge.websocketsLookup('ws://roomy', 'room');
149 | t.is(inRoom.length, 0, 'there are no sockets in the room to start with');
150 |
151 | networkBridge.addMembershipToRoom(fakeSocket, 'room');
152 |
153 | inRoom = networkBridge.websocketsLookup('ws://roomy', 'room');
154 | t.is(inRoom.length, 1, 'there is 1 socket in the room after joining');
155 | t.deepEqual(inRoom[0], fakeSocket);
156 |
157 | networkBridge.removeMembershipFromRoom(fakeSocket, 'room');
158 |
159 | inRoom = networkBridge.websocketsLookup('ws://roomy', 'room');
160 | t.is(inRoom.length, 0, 'there are no sockets in the room after leaving');
161 | });
162 |
163 | test('a socket with query params can join and leave a room', t => {
164 | const fakeSocket = { url: 'ws://roomy?foo=bar' };
165 |
166 | networkBridge.attachServer(fakeObject, 'ws://roomy');
167 | networkBridge.attachWebSocket(fakeSocket, 'ws://roomy');
168 |
169 | let inRoom;
170 | inRoom = networkBridge.websocketsLookup('ws://roomy', 'room');
171 | t.is(inRoom.length, 0, 'there are no sockets in the room to start with');
172 |
173 | networkBridge.addMembershipToRoom(fakeSocket, 'room');
174 |
175 | inRoom = networkBridge.websocketsLookup('ws://roomy', 'room');
176 | t.is(inRoom.length, 1, 'there is 1 socket in the room after joining');
177 | t.deepEqual(inRoom[0], fakeSocket);
178 |
179 | networkBridge.removeMembershipFromRoom(fakeSocket, 'room');
180 |
181 | inRoom = networkBridge.websocketsLookup('ws://roomy', 'room');
182 | t.is(inRoom.length, 0, 'there are no sockets in the room after leaving');
183 | });
184 |
--------------------------------------------------------------------------------
/tests/functional/websockets.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Server from '../../src/server';
3 | import WebSocket from '../../src/websocket';
4 | import networkBridge from '../../src/network-bridge';
5 |
6 | test.beforeEach(() => {
7 | networkBridge.urlMap = {};
8 | });
9 |
10 | test.cb('that creating a websocket with no server invokes the onerror method', t => {
11 | const mockSocket = new WebSocket('ws://localhost:8080');
12 | mockSocket.onerror = function error(event) {
13 | t.is(event.target.readyState, WebSocket.CLOSED, 'onerror fires as expected');
14 | t.end();
15 | };
16 | });
17 |
18 | test.cb('that onopen is called after successfully connection to the server', t => {
19 | const server = new Server('ws://localhost:8080');
20 | const mockSocket = new WebSocket('ws://localhost:8080');
21 |
22 | mockSocket.onopen = function open(event) {
23 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
24 | t.end();
25 | };
26 | });
27 |
28 | test.cb('that failing the verifyClient check invokes the onerror method', t => {
29 | const server = new Server('ws://localhost:8080', {
30 | verifyClient: () => false
31 | });
32 | const mockSocket = new WebSocket('ws://localhost:8080');
33 |
34 | mockSocket.onerror = function open(event) {
35 | t.is(event.target.readyState, WebSocket.CLOSED, 'onerror fires as expected');
36 | t.end();
37 | };
38 | });
39 |
40 | test.cb('that failing the verifyClient check removes the websocket from the networkBridge', t => {
41 | const server = new Server('ws://localhost:8080', {
42 | verifyClient: () => false
43 | });
44 | const mockSocket = new WebSocket('ws://localhost:8080');
45 |
46 | mockSocket.onclose = function close() {
47 | const urlMap = networkBridge.urlMap['ws://localhost:8080/'];
48 | t.is(urlMap.websockets.length, 0, 'the websocket was removed from the network bridge');
49 | server.close();
50 | t.end();
51 | };
52 | });
53 |
54 | test.cb('that verifyClient is only invoked if it is a function', t => {
55 | const server = new Server('ws://localhost:8080', {
56 | verifyClient: false
57 | });
58 | const mockSocket = new WebSocket('ws://localhost:8080');
59 |
60 | mockSocket.onopen = function open(event) {
61 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
62 | t.end();
63 | };
64 | });
65 |
66 | test.cb('that onmessage is called after the server sends a message', t => {
67 | const testServer = new Server('ws://localhost:8080');
68 |
69 | testServer.on('connection', socket => {
70 | socket.send('Testing');
71 | });
72 |
73 | const mockSocket = new WebSocket('ws://localhost:8080');
74 |
75 | mockSocket.onmessage = function message(event) {
76 | t.is(event.data, 'Testing', 'onmessage fires as expected');
77 | t.end();
78 | };
79 | });
80 |
81 | test.cb('that onclose is called after the client closes the connection', t => {
82 | const testServer = new Server('ws://localhost:8080');
83 |
84 | testServer.on('connection', socket => {
85 | socket.send('Testing');
86 | });
87 |
88 | const mockSocket = new WebSocket('ws://localhost:8080');
89 |
90 | mockSocket.onmessage = function message() {
91 | mockSocket.close();
92 | };
93 |
94 | mockSocket.onclose = function close(event) {
95 | t.is(event.target.readyState, WebSocket.CLOSED, 'onclose fires as expected');
96 | t.end();
97 | };
98 | });
99 |
100 | test.cb('that the server gets called when the client closes the connection', t => {
101 | const testServer = new Server('ws://localhost:8080');
102 |
103 | testServer.on('connection', socket => {
104 | socket.onclose = function close(event) {
105 | t.is(event.target.readyState, WebSocket.CLOSED, 'onclose fires as expected');
106 | t.end();
107 | };
108 | });
109 |
110 | const mockSocket = new WebSocket('ws://localhost:8080');
111 |
112 | mockSocket.onopen = function open() {
113 | mockSocket.close();
114 | };
115 | });
116 |
117 | test.cb('that the server gets called when the client sends a message', t => {
118 | const testServer = new Server('ws://localhost:8080');
119 |
120 | testServer.on('connection', socket => {
121 | socket.on('message', data => {
122 | t.is(data, 'Testing', 'on message fires as expected');
123 | t.end();
124 | });
125 | });
126 |
127 | const mockSocket = new WebSocket('ws://localhost:8080');
128 |
129 | mockSocket.onopen = function open() {
130 | this.send('Testing');
131 | };
132 | });
133 |
134 | test.cb('that the server gets called when the client sends a message using URL with query parameters', t => {
135 | const testServer = new Server('ws://localhost:8080');
136 |
137 | testServer.on('connection', socket => {
138 | socket.on('message', data => {
139 | t.is(data, 'Testing', 'on message fires as expected');
140 | t.end();
141 | });
142 | });
143 |
144 | const mockSocket = new WebSocket('ws://localhost:8080?foo=bar');
145 |
146 | mockSocket.onopen = function open() {
147 | this.send('Testing');
148 | };
149 | });
150 |
151 | test.cb('that close event is handled both for the client and WebSocket when the server shuts down', t => {
152 | t.plan(2);
153 |
154 | const testServer = new Server('ws://localhost:8080');
155 |
156 | testServer.on('connection', socket => {
157 | socket.on('close', event => {
158 | t.is(event.target.readyState, WebSocket.CLOSED, 'close event fires as expected in the client');
159 | t.end();
160 | });
161 |
162 | testServer.close();
163 | });
164 |
165 | const mockSocket = new WebSocket('ws://localhost:8080');
166 |
167 | mockSocket.onclose = event => {
168 | t.is(event.target.readyState, WebSocket.CLOSED, 'close event fires as expected in the WebSocket');
169 | };
170 | });
171 |
172 | test.cb('that error event is handled both for the client and WebSocket when the server emits error', t => {
173 | t.plan(2);
174 |
175 | const testServer = new Server('ws://localhost:8080');
176 |
177 | testServer.on('connection', socket => {
178 | socket.on('error', event => {
179 | t.is(event.target.readyState, WebSocket.CLOSED, 'error event fires as expected on the client');
180 | t.end();
181 | });
182 |
183 | testServer.simulate('error');
184 | });
185 |
186 | const mockSocket = new WebSocket('ws://localhost:8080');
187 |
188 | mockSocket.onerror = event => {
189 | t.is(event.target.readyState, WebSocket.CLOSED, 'error event fires as expected on the WebSocket');
190 | };
191 | });
192 |
193 | test.cb('that the onopen function will only be called once for each client', t => {
194 | const socketUrl = 'ws://localhost:8080';
195 | const mockServer = new Server(socketUrl);
196 | const websocketFoo = new WebSocket(socketUrl);
197 | const websocketBar = new WebSocket(socketUrl);
198 |
199 | websocketFoo.onopen = function open() {
200 | t.true(true, 'mocksocket onopen fires as expected');
201 | };
202 |
203 | websocketBar.onopen = function open() {
204 | t.true(true, 'mocksocket onopen fires as expected');
205 | mockServer.close();
206 | t.end();
207 | };
208 | });
209 |
210 | test.cb('closing a client will only close itself and not other clients', t => {
211 | const server = new Server('ws://localhost:8080');
212 | const websocketFoo = new WebSocket('ws://localhost:8080');
213 | const websocketBar = new WebSocket('ws://localhost:8080');
214 |
215 | websocketFoo.onclose = function close() {
216 | t.true(false, 'mocksocket should not close');
217 | };
218 |
219 | websocketBar.onopen = function open() {
220 | this.close();
221 | };
222 |
223 | websocketBar.onclose = function close() {
224 | t.true(true, 'mocksocket onclose fires as expected');
225 | t.end();
226 | };
227 | });
228 |
229 | test.cb('mock clients can send messages to the right mock server', t => {
230 | const serverFoo = new Server('ws://localhost:8080');
231 | const serverBar = new Server('ws://localhost:8081');
232 | const dataFoo = 'foo';
233 | const dataBar = 'bar';
234 | const socketFoo = new WebSocket('ws://localhost:8080');
235 | const socketBar = new WebSocket('ws://localhost:8081');
236 |
237 | serverFoo.on('connection', server => {
238 | t.true(true, 'mock server on connection fires as expected');
239 |
240 | server.on('message', data => {
241 | t.is(data, dataFoo);
242 | });
243 | });
244 |
245 | serverBar.on('connection', server => {
246 | t.true(true, 'mock server on connection fires as expected');
247 |
248 | server.on('message', data => {
249 | t.is(data, dataBar);
250 | t.end();
251 | });
252 | });
253 |
254 | socketFoo.onopen = function open() {
255 | t.true(true, 'mocksocket onopen fires as expected');
256 | this.send(dataFoo);
257 | };
258 |
259 | socketBar.onopen = function open() {
260 | t.true(true, 'mocksocket onopen fires as expected');
261 | this.send(dataBar);
262 | };
263 | });
264 |
265 | test.cb('that closing a websocket removes it from the network bridge', t => {
266 | const server = new Server('ws://localhost:8080');
267 | const socket = new WebSocket('ws://localhost:8080');
268 |
269 | socket.onopen = function open() {
270 | const urlMap = networkBridge.urlMap['ws://localhost:8080/'];
271 | t.is(urlMap.websockets.length, 1, 'the websocket is in the network bridge');
272 | t.deepEqual(urlMap.websockets[0], this, 'the websocket is in the network bridge');
273 | this.close();
274 | };
275 |
276 | socket.onclose = function close() {
277 | const urlMap = networkBridge.urlMap['ws://localhost:8080/'];
278 | t.is(urlMap.websockets.length, 0, 'the websocket was removed from the network bridge');
279 | server.close();
280 | t.end();
281 | };
282 | });
283 |
284 | test.cb('that it is possible to simulate an error from the server to the clients', t => {
285 | const server = new Server('ws://localhost:8080');
286 | const socket = new WebSocket('ws://localhost:8080');
287 |
288 | socket.onopen = function open() {
289 | server.simulate('error');
290 | };
291 |
292 | socket.onerror = function error() {
293 | t.pass('On error was called after it was simulated');
294 | t.end();
295 | };
296 | });
297 |
298 | test.cb('that failing the selectProtocol check invokes the onerror method', t => {
299 | const server = new Server('ws://localhost:8080', {
300 | selectProtocol: () => false
301 | });
302 | const mockSocket = new WebSocket('ws://localhost:8080');
303 |
304 | mockSocket.onerror = function open(event) {
305 | t.is(event.target.readyState, WebSocket.CLOSED, 'onerror fires as expected');
306 | t.end();
307 | };
308 | });
309 |
310 | test.cb('that failing the selectProtocol check removes the websocket from the networkBridge', t => {
311 | const server = new Server('ws://localhost:8080', {
312 | selectProtocol: () => false
313 | });
314 | const mockSocket = new WebSocket('ws://localhost:8080');
315 |
316 | mockSocket.onclose = function close() {
317 | const urlMap = networkBridge.urlMap['ws://localhost:8080/'];
318 | t.is(urlMap.websockets.length, 0, 'the websocket was removed from the network bridge');
319 | server.close();
320 | t.end();
321 | };
322 | });
323 |
324 | test.cb('that selectProtocol is only invoked if it is a function', t => {
325 | const server = new Server('ws://localhost:8080', {
326 | selectProtocol: false
327 | });
328 | const mockSocket = new WebSocket('ws://localhost:8080');
329 |
330 | mockSocket.onopen = function open(event) {
331 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
332 | t.end();
333 | };
334 | });
335 |
336 | test.cb('that selectProtocol should be invoked with an empty array if unspecified', t => {
337 | const server = new Server('ws://localhost:8080', {
338 | selectProtocol(protocols) {
339 | t.deepEqual(protocols, []);
340 | return '';
341 | }
342 | });
343 | const mockSocket = new WebSocket('ws://localhost:8080');
344 |
345 | mockSocket.onopen = function open(event) {
346 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
347 | t.is(event.target.protocol, '');
348 | t.end();
349 | };
350 | });
351 |
352 | test.cb('that selectProtocol should be invoked with a single protocol', t => {
353 | const server = new Server('ws://localhost:8080', {
354 | selectProtocol(protocols) {
355 | t.deepEqual(protocols, ['text']);
356 | return 'text';
357 | }
358 | });
359 | const mockSocket = new WebSocket('ws://localhost:8080', 'text');
360 |
361 | mockSocket.onopen = function open(event) {
362 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
363 | t.is(event.target.protocol, 'text');
364 | t.end();
365 | };
366 | });
367 |
368 | test.cb('that selectProtocol should be able to select any of the requested protocols', t => {
369 | const server = new Server('ws://localhost:8080', {
370 | selectProtocol(protocols) {
371 | t.deepEqual(protocols, ['text', 'binary']);
372 | return 'binary';
373 | }
374 | });
375 | const mockSocket = new WebSocket('ws://localhost:8080', ['text', 'binary']);
376 |
377 | mockSocket.onopen = function open(event) {
378 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
379 | t.is(event.target.protocol, 'binary');
380 | t.end();
381 | };
382 | });
383 |
384 | test.cb('that client should close if the subprotocol is not of the selected set', t => {
385 | const server = new Server('ws://localhost:8080', {
386 | selectProtocol(protocols) {
387 | t.deepEqual(protocols, ['text']);
388 | return 'unsupported';
389 | }
390 | });
391 | const mockSocket = new WebSocket('ws://localhost:8080', 'text');
392 |
393 | mockSocket.onclose = function close() {
394 | const urlMap = networkBridge.urlMap['ws://localhost:8080/'];
395 | t.is(urlMap.websockets.length, 0, 'the websocket was removed from the network bridge');
396 | server.close();
397 | t.end();
398 | };
399 | });
400 |
401 | test.cb('the server should be able to select _none_ of the protocols from the client', t => {
402 | const server = new Server('ws://localhost:8080', {
403 | selectProtocol(protocols) {
404 | t.deepEqual(protocols, ['text', 'binary']);
405 | return '';
406 | }
407 | });
408 | const mockSocket = new WebSocket('ws://localhost:8080', ['text', 'binary']);
409 |
410 | mockSocket.onopen = function open(event) {
411 | t.is(event.target.readyState, WebSocket.OPEN, 'onopen fires as expected');
412 | t.is(event.target.protocol, '');
413 | t.end();
414 | };
415 | });
416 |
--------------------------------------------------------------------------------