├── .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 | Build Status 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 | --------------------------------------------------------------------------------