test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. const test = require('ava');
  2. const {spawn} = require('child_process');
  3. const WebSocket = require('ws');
  4. const WebSocketServer = require('ws').Server;
  5. const ReconnectingWebSocket = require('..');
  6. const PORT = 50123;
  7. const PORT_UNRESPONSIVE = 50124;
  8. const URL = `ws://localhost:${PORT}`;
  9. test.beforeEach(() => {
  10. global.WebSocket = WebSocket;
  11. });
  12. test.afterEach(() => {
  13. delete global.WebSocket;
  14. });
  15. test('throws with invalid constructor', t => {
  16. delete global.WebSocket;
  17. t.throws(() => {
  18. new ReconnectingWebSocket(URL, undefined, {WebSocket: 123});
  19. });
  20. });
  21. test('throws with missing constructor', t => {
  22. delete global.WebSocket;
  23. t.throws(() => {
  24. new ReconnectingWebSocket(URL, undefined);
  25. });
  26. });
  27. test('throws if not created with `new`', t => {
  28. t.throws(() => {
  29. ReconnectingWebSocket(URL, undefined);
  30. }, TypeError);
  31. });
  32. test.cb('global WebSocket is used if available', t => {
  33. const ws = new ReconnectingWebSocket(URL, undefined, {
  34. maxRetries: 0,
  35. });
  36. ws.onerror = () => {
  37. t.true(ws._ws instanceof WebSocket);
  38. t.end();
  39. };
  40. });
  41. test.cb('send is ignored when not ready', t => {
  42. const ws = new ReconnectingWebSocket(URL, undefined, {
  43. maxRetries: 0,
  44. });
  45. ws.send('message');
  46. ws.onerror = () => {
  47. t.pass();
  48. t.end();
  49. };
  50. });
  51. test.cb('getters when not ready', t => {
  52. const ws = new ReconnectingWebSocket(URL, undefined, {
  53. maxRetries: 0,
  54. });
  55. t.is(ws.bufferedAmount, 0);
  56. t.is(ws.protocol, '');
  57. t.is(ws.url, '');
  58. t.is(ws.extensions, '');
  59. t.is(ws.binaryType, 'blob');
  60. ws.onerror = () => {
  61. t.pass();
  62. t.end();
  63. };
  64. });
  65. test.cb('debug', t => {
  66. const log = console.log;
  67. console.log = () => t.pass();
  68. const ws = new ReconnectingWebSocket(URL, undefined, {
  69. maxRetries: 0,
  70. debug: true,
  71. });
  72. ws.onerror = () => {
  73. ws._options.debug = false;
  74. console.log = log;
  75. t.end();
  76. };
  77. });
  78. test.cb('pass WebSocket via options', t => {
  79. delete global.WebSocket;
  80. const ws = new ReconnectingWebSocket(URL, undefined, {
  81. WebSocket,
  82. maxRetries: 0,
  83. });
  84. ws.onerror = () => {
  85. t.true(ws._ws instanceof WebSocket);
  86. t.end();
  87. };
  88. });
  89. test('URL provider', async t => {
  90. const url = 'example.com';
  91. const ws = new ReconnectingWebSocket(URL, undefined, {
  92. maxRetries: 0,
  93. });
  94. t.is(await ws._getNextUrl(url), url, 'string');
  95. t.is(await ws._getNextUrl(() => url), url, '() -> string');
  96. t.is(await ws._getNextUrl(() => Promise.resolve(url)), url, '() -> Promise<string>');
  97. try {
  98. await ws._getNextUrl(123);
  99. t.fail();
  100. } catch (e) {
  101. t.pass();
  102. }
  103. try {
  104. await ws._getNextUrl(() => 123);
  105. t.fail();
  106. } catch (e) {
  107. t.pass();
  108. }
  109. });
  110. test('connection status constants', t => {
  111. const ws = new ReconnectingWebSocket(URL, null, {maxRetries: 0});
  112. t.is(ReconnectingWebSocket.CONNECTING, 0);
  113. t.is(ReconnectingWebSocket.OPEN, 1);
  114. t.is(ReconnectingWebSocket.CLOSING, 2);
  115. t.is(ReconnectingWebSocket.CLOSED, 3);
  116. t.is(ws.CONNECTING, 0);
  117. t.is(ws.OPEN, 1);
  118. t.is(ws.CLOSING, 2);
  119. t.is(ws.CLOSED, 3);
  120. ws.close();
  121. });
  122. const maxRetriesTest = (count, t) => {
  123. const ws = new ReconnectingWebSocket(URL, null, {
  124. maxRetries: count,
  125. maxReconnectionDelay: 200,
  126. });
  127. t.plan(count + 1);
  128. ws.addEventListener('error', event => {
  129. t.pass();
  130. if (ws.retryCount === count) {
  131. setTimeout(() => t.end(), 500);
  132. }
  133. if (ws.retryCount > count) {
  134. t.fail(`too many retries: ${ws.retryCount}`);
  135. }
  136. });
  137. };
  138. test.cb('max retries: 0', t => maxRetriesTest(0, t));
  139. test.cb('max retries: 1', t => maxRetriesTest(1, t));
  140. test.cb('max retries: 5', t => maxRetriesTest(5, t));
  141. test.cb('level0 event listeners are kept after reconnect', t => {
  142. const ws = new ReconnectingWebSocket(URL, null, {
  143. maxRetries: 4,
  144. reconnectionDelayFactor: 1.2,
  145. maxReconnectionDelay: 20,
  146. minReconnectionDelay: 10,
  147. });
  148. const handleOpen = () => {};
  149. const handleClose = () => {};
  150. const handleMessage = () => {};
  151. const handleError = () => {
  152. t.is(ws.onopen, handleOpen);
  153. t.is(ws.onclose, handleClose);
  154. t.is(ws.onmessage, handleMessage);
  155. t.is(ws.onerror, handleError);
  156. if (ws.retryCount === 4) {
  157. t.end();
  158. }
  159. };
  160. ws.onopen = handleOpen;
  161. ws.onclose = handleClose;
  162. ws.onmessage = handleMessage;
  163. ws.onerror = handleError;
  164. });
  165. test.cb('level2 event listeners', t => {
  166. const anyProtocol = 'foobar';
  167. const wss = new WebSocketServer({port: PORT});
  168. const ws = new ReconnectingWebSocket(URL, anyProtocol, {});
  169. ws.addEventListener('open', () => {
  170. t.is(ws.protocol, anyProtocol);
  171. t.is(ws.extensions, '');
  172. t.is(ws.bufferedAmount, 0);
  173. ws.close();
  174. });
  175. const fail = () => {
  176. t.fail();
  177. };
  178. ws.addEventListener('unknown1', fail);
  179. ws.addEventListener('open', fail);
  180. ws.addEventListener('open', fail);
  181. ws.removeEventListener('open', fail);
  182. ws.removeEventListener('unknown2', fail);
  183. ws.addEventListener('close', () => {
  184. wss.close();
  185. setTimeout(() => t.end(), 500);
  186. });
  187. });
  188. test.cb('connection timeout', t => {
  189. const proc = spawn('node', [`${__dirname}/unresponsive-server.js`, PORT_UNRESPONSIVE, 5000]);
  190. t.plan(2);
  191. let lock = false;
  192. proc.stdout.on('data', d => {
  193. if (lock) return;
  194. lock = true;
  195. const ws = new ReconnectingWebSocket(`ws://localhost:${PORT_UNRESPONSIVE}`, null, {
  196. minReconnectionDelay: 50,
  197. connectionTimeout: 500,
  198. maxRetries: 1,
  199. });
  200. ws.addEventListener('error', event => {
  201. t.is(event.message, 'TIMEOUT');
  202. if (ws.retryCount === 1) {
  203. setTimeout(() => t.end(), 1000);
  204. }
  205. });
  206. ws.addEventListener('close', event => {
  207. console.log('>>>> CLOSE', event.message);
  208. });
  209. });
  210. });
  211. test.cb('getters', t => {
  212. const anyProtocol = 'foobar';
  213. const wss = new WebSocketServer({port: PORT});
  214. const ws = new ReconnectingWebSocket(URL, anyProtocol, {maxReconnectionDelay: 100});
  215. ws.addEventListener('open', () => {
  216. t.is(ws.protocol, anyProtocol);
  217. t.is(ws.extensions, '');
  218. t.is(ws.bufferedAmount, 0);
  219. t.is(ws.binaryType, 'nodebuffer');
  220. ws.close();
  221. });
  222. ws.addEventListener('close', () => {
  223. wss.close();
  224. setTimeout(() => t.end(), 500);
  225. });
  226. });
  227. test.cb('binaryType', t => {
  228. const wss = new WebSocketServer({port: PORT});
  229. const ws = new ReconnectingWebSocket(URL, undefined, {minReconnectionDelay: 0});
  230. t.is(ws.binaryType, 'blob');
  231. ws.binaryType = 'arraybuffer';
  232. ws.addEventListener('open', () => {
  233. t.is(ws.binaryType, 'arraybuffer', 'assigned after open');
  234. ws.binaryType = 'nodebuffer';
  235. t.is(ws.binaryType, 'nodebuffer');
  236. ws.close();
  237. });
  238. ws.addEventListener('close', () => {
  239. wss.close();
  240. setTimeout(() => t.end(), 500);
  241. });
  242. });
  243. test.cb('calling to close multiple times', t => {
  244. const wss = new WebSocketServer({port: PORT});
  245. const ws = new ReconnectingWebSocket(URL, undefined, {});
  246. ws.addEventListener('open', () => {
  247. ws.close();
  248. ws.close();
  249. ws.close();
  250. });
  251. ws.addEventListener('close', () => {
  252. wss.close();
  253. setTimeout(() => t.end(), 500);
  254. });
  255. });
  256. test.cb('calling to reconnect when not ready', t => {
  257. const wss = new WebSocketServer({port: PORT});
  258. const ws = new ReconnectingWebSocket(URL, undefined, {});
  259. ws.reconnect();
  260. ws.reconnect();
  261. ws.addEventListener('open', () => {
  262. ws.close();
  263. });
  264. ws.addEventListener('close', () => {
  265. wss.close();
  266. setTimeout(() => t.end(), 500);
  267. });
  268. });
  269. test.cb('connect, send, receive, close', t => {
  270. const anyMessageText = 'hello';
  271. const anyProtocol = 'foobar';
  272. const wss = new WebSocketServer({port: PORT});
  273. wss.on('connection', ws => {
  274. ws.on('message', msg => {
  275. ws.send(msg);
  276. });
  277. });
  278. wss.on('error', () => {
  279. t.fail();
  280. });
  281. t.plan(7);
  282. const ws = new ReconnectingWebSocket(URL, anyProtocol, {
  283. minReconnectionDelay: 100,
  284. maxReconnectionDelay: 200,
  285. });
  286. t.is(ws.readyState, ws.CONNECTING);
  287. ws.addEventListener('open', () => {
  288. t.is(ws.protocol, anyProtocol);
  289. t.is(ws.readyState, ws.OPEN);
  290. ws.send(anyMessageText);
  291. });
  292. ws.addEventListener('message', msg => {
  293. t.is(msg.data, anyMessageText);
  294. ws.close(1000, '');
  295. t.is(ws.readyState, ws.CLOSING);
  296. });
  297. ws.addEventListener('close', () => {
  298. t.is(ws.readyState, ws.CLOSED);
  299. t.is(ws.url, URL);
  300. wss.close();
  301. setTimeout(() => t.end(), 1000);
  302. });
  303. });
  304. test.cb('connect, send, receive, reconnect', t => {
  305. const anyMessageText = 'hello';
  306. const anyProtocol = 'foobar';
  307. const wss = new WebSocketServer({port: PORT});
  308. wss.on('connection', ws => {
  309. ws.on('message', msg => {
  310. ws.send(msg);
  311. });
  312. });
  313. const totalRounds = 3;
  314. let currentRound = 0;
  315. // 6 = 3 * 2 open
  316. // 8 = 2 * 3 message + 2 reconnect
  317. // 7 = 2 * 3 close + 1 closed
  318. t.plan(21);
  319. const ws = new ReconnectingWebSocket(URL, anyProtocol, {
  320. minReconnectionDelay: 100,
  321. maxReconnectionDelay: 200,
  322. });
  323. ws.onopen = () => {
  324. currentRound++;
  325. t.is(ws.protocol, anyProtocol);
  326. t.is(ws.readyState, ws.OPEN);
  327. ws.send(anyMessageText);
  328. };
  329. ws.onmessage = msg => {
  330. t.is(msg.data, anyMessageText);
  331. if (currentRound < totalRounds) {
  332. ws.reconnect(1000, 'reconnect');
  333. t.is(ws.retryCount, 0);
  334. } else {
  335. ws.close(1000, 'close');
  336. }
  337. t.is(ws.readyState, ws.CLOSING);
  338. };
  339. ws.addEventListener('close', event => {
  340. t.is(ws.url, URL);
  341. if (currentRound >= totalRounds) {
  342. t.is(ws.readyState, ws.CLOSED);
  343. wss.close();
  344. setTimeout(() => t.end(), 1000);
  345. t.is(event.reason, 'close');
  346. } else {
  347. t.is(event.reason, 'reconnect');
  348. }
  349. });
  350. });
  351. test.cb('immediatly-failed connection should not timeout', t => {
  352. const ws = new ReconnectingWebSocket('ws://thiswillfail.com', null, {
  353. maxRetries: 2,
  354. connectionTimeout: 500,
  355. });
  356. ws.addEventListener('error', err => {
  357. if (err.message === 'TIMEOUT') {
  358. t.fail();
  359. }
  360. if (ws.retryCount === 2) {
  361. setTimeout(() => t.end(), 1500);
  362. }
  363. if (ws.retryCount > 2) {
  364. t.fail();
  365. }
  366. });
  367. });