All files / channel/src/__tests__ WebSocketMock.ts

80.64% Statements 50/62
50% Branches 15/30
76.92% Functions 10/13
83.33% Lines 50/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 1272x     2x     2x 18x   18x 18x 18x               2x                       35x 35x 35x 35x   35x   35x 35x     19x       3x     35x   35x         17x 17x 17x       16x 16x 15x 15x   16x 16x 16x 16x       2x 2x 2x       5x 5x 5x         35x 35x   35x 35x 34x             7x       9x 8x 1x 1x               9x 9x         9x 9x          
import {utf8Size} from '@jsonjoy.com/util/lib/strings/utf8';
import {WebSocketState} from '../constants';
import type {Subscription} from 'rxjs';
import {toUint8Array} from '@jsonjoy.com/buffers/lib/toUint8Array';
import type {WebSocketMockServerConnection} from './WebSocketMockServerConnection';
 
export class CloseEvent {
  public readonly type = 'close';
  constructor(
    public readonly code: number,
    public readonly reason: string,
    public readonly wasClean: boolean,
  ) {}
}
 
export interface WebSocketMockParams {
  connection?: WebSocketMockServerConnection;
}
 
export class WebSocketMock
  implements
    Pick<
      WebSocket,
      'binaryType' | 'readyState' | 'bufferedAmount' | 'onopen' | 'onclose' | 'onerror' | 'onmessage' | 'close' | 'send'
    >
{
  public static create(params: Partial<WebSocketMockParams>, url: string = 'http://127.0.0.1') {
    const ws = new WebSocketMock(params, url);
    return [ws, ws.controller];
  }
 
  public onclose: ((event: any) => void) | null = null;
  public onerror: ((event: Event) => void) | null = null;
  public onmessage: ((event: Event) => void) | null = null;
  public onopen: ((event: Event) => void) | null = null;
 
  public binaryType: 'arraybuffer' | 'blob' = 'blob';
 
  public _readyState: WebSocketState = WebSocketState.CONNECTING;
  public _bufferedAmount = 0;
 
  public get bufferedAmount(): number {
    return this._bufferedAmount;
  }
 
  public get readyState(): number {
    return this._readyState;
  }
 
  public _connectionSub: Subscription | null = null;
 
  public readonly controller = {
    readyState: WebSocketState.CLOSED,
    bufferedAmount: 0,
 
    open: (): void => {
      this._readyState = WebSocketState.OPEN;
      const event = {type: 'open'} as Event;
      this.onopen?.(event);
    },
 
    close: (code: number, reason: string, wasClean: boolean): void => {
      this._connectionSub?.unsubscribe();
      if (this.params.connection) {
        this.params.connection.outgoing$.complete();
        this.params.connection.incoming$.complete();
      }
      Iif (this._readyState === WebSocketState.CLOSED) throw new Error('Mock WebSocket already closed.');
      this._readyState = WebSocketState.CLOSED;
      const event = new CloseEvent(code, reason, wasClean);
      this.onclose?.(event);
    },
 
    error: (message: string): void => {
      const event = {type: 'error'} as Event;
      this.onerror?.(event);
      this.controller.close(1000, message, false);
    },
 
    message: (message: string | ArrayBuffer | ArrayBufferView): void => {
      Iif (!this.onmessage) return;
      const event = {type: 'message', data: message} as any;
      this.onmessage(event);
    },
  };
 
  constructor(
    public readonly params: Partial<WebSocketMockParams>,
    public readonly url: string = 'http://127.0.0.1',
  ) {
    const {connection} = params;
    if (connection) {
      this._connectionSub = connection.outgoing$.subscribe((data) => {
        this.controller.message(data);
      });
    }
  }
 
  public close(code?: number, reason?: string): void {
    this.controller.close(code ?? 0, reason ?? '', true);
  }
 
  public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
    if (typeof data === 'string') {
      this._bufferedAmount += utf8Size(data);
    } else if (ArrayBuffer.isView(data)) {
      this._bufferedAmount += data.byteLength;
    } else Eif (data && typeof data === 'object') {
      if ((data as any).byteLength !== undefined) {
        this._bufferedAmount += Number((data as any).byteLength);
      } else if ((data as unknown as Blob).size !== undefined) {
        this._bufferedAmount += Number((data as unknown as Blob).size);
      }
    }
    Eif (this.params.connection) {
      Iif (data instanceof Blob) {
        data.bytes().then((buf) => {
          this.params.connection?.incoming$.next(new Uint8Array(buf));
        });
      } else {
        const buf = toUint8Array(data);
        this.params.connection.incoming$.next(buf);
      }
    }
  }
}