Add JS unit tests for WebSockets (#2495)
This commit is contained in:
parent
e4e9bd1a3c
commit
7e832eeb27
|
|
@ -33,12 +33,12 @@ describe("ServerSentEventsTransport", () => {
|
|||
|
||||
await TestEventSource.eventSource.openSet;
|
||||
|
||||
expect(connectComplete).toEqual(false);
|
||||
expect(connectComplete).toBe(false);
|
||||
|
||||
TestEventSource.eventSource.onopen(new TestMessageEvent());
|
||||
|
||||
await connectPromise;
|
||||
expect(connectComplete).toEqual(true);
|
||||
expect(connectComplete).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ describe("ServerSentEventsTransport", () => {
|
|||
await VerifyLogger.run(async (logger) => {
|
||||
await createAndStartSSE(logger, input, () => "secretToken");
|
||||
|
||||
expect(TestEventSource.eventSource.url).toEqual(expected);
|
||||
expect(TestEventSource.eventSource.url).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -66,8 +66,8 @@ describe("ServerSentEventsTransport", () => {
|
|||
|
||||
await sse.send("");
|
||||
|
||||
expect(request!.headers!.Authorization).toEqual("Bearer secretToken");
|
||||
expect(request!.url).toEqual("http://example.com");
|
||||
expect(request!.headers!.Authorization).toBe("Bearer secretToken");
|
||||
expect(request!.url).toBe("http://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ describe("ServerSentEventsTransport", () => {
|
|||
|
||||
await sse.send("send data");
|
||||
|
||||
expect(request!.content).toEqual("send data");
|
||||
expect(request!.content).toBe("send data");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -100,8 +100,8 @@ describe("ServerSentEventsTransport", () => {
|
|||
message.data = "receive data";
|
||||
TestEventSource.eventSource.onmessage(message);
|
||||
|
||||
expect(typeof received!).toEqual("string");
|
||||
expect(received!).toEqual("receive data");
|
||||
expect(typeof received!).toBe("string");
|
||||
expect(received!).toBe("receive data");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -116,8 +116,8 @@ describe("ServerSentEventsTransport", () => {
|
|||
|
||||
await sse.stop();
|
||||
|
||||
expect(closeCalled).toEqual(true);
|
||||
expect(TestEventSource.eventSource.closed).toEqual(true);
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(TestEventSource.eventSource.closed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -136,9 +136,9 @@ describe("ServerSentEventsTransport", () => {
|
|||
errorEvent.data = "error";
|
||||
TestEventSource.eventSource.onerror(errorEvent);
|
||||
|
||||
expect(closeCalled).toEqual(true);
|
||||
expect(TestEventSource.eventSource.closed).toEqual(true);
|
||||
expect(error).toMatchObject({ message: "error" });
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(TestEventSource.eventSource.closed).toBe(true);
|
||||
expect(error).toEqual(new Error("error"));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ describe("ServerSentEventsTransport", () => {
|
|||
|
||||
await expect(sse.send(""))
|
||||
.rejects
|
||||
.toMatchObject({ message: "Cannot send until the transport is connected" });
|
||||
.toEqual(new Error("Cannot send until the transport is connected"));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -171,9 +171,9 @@ describe("ServerSentEventsTransport", () => {
|
|||
errorEvent.data = "some data";
|
||||
TestEventSource.eventSource.onmessage(errorEvent);
|
||||
|
||||
expect(closeCalled).toEqual(true);
|
||||
expect(TestEventSource.eventSource.closed).toEqual(true);
|
||||
expect(error).toMatchObject({ message: "error parsing" });
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(TestEventSource.eventSource.closed).toBe(true);
|
||||
expect(error).toEqual(new Error("error parsing"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { PromiseSource } from "./Utils";
|
||||
|
||||
export class TestWebSocket {
|
||||
public binaryType: "blob" | "arraybuffer" = "blob";
|
||||
public bufferedAmount: number = 0;
|
||||
public extensions: string = "";
|
||||
public onclose!: ((this: WebSocket, ev: CloseEvent) => any);
|
||||
public onerror!: ((this: WebSocket, ev: Event) => any);
|
||||
public onmessage!: ((this: WebSocket, ev: MessageEvent) => any);
|
||||
public protocol: string;
|
||||
public readyState: number = 1;
|
||||
public url: string;
|
||||
|
||||
public static webSocket: TestWebSocket;
|
||||
public receivedData: Array<(string | ArrayBuffer | Blob | ArrayBufferView)>;
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
private _onopen?: (this: WebSocket, evt: Event) => any;
|
||||
public openSet: PromiseSource = new PromiseSource();
|
||||
public set onopen(value: (this: WebSocket, evt: Event) => any) {
|
||||
this._onopen = value;
|
||||
this.openSet.resolve();
|
||||
}
|
||||
|
||||
public get onopen(): (this: WebSocket, evt: Event) => any {
|
||||
return this._onopen!;
|
||||
}
|
||||
|
||||
public close(code?: number | undefined, reason?: string | undefined): void {
|
||||
const closeEvent = new TestCloseEvent();
|
||||
closeEvent.code = code || 1000;
|
||||
closeEvent.reason = reason!;
|
||||
closeEvent.wasClean = closeEvent.code === 1000;
|
||||
this.onclose(closeEvent);
|
||||
}
|
||||
|
||||
public send(data: string | ArrayBuffer | Blob | ArrayBufferView): void {
|
||||
this.receivedData.push(data);
|
||||
}
|
||||
|
||||
public addEventListener<K extends "close" | "error" | "message" | "open">(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
public addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void;
|
||||
public addEventListener(type: any, listener: any, options?: any) {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public removeEventListener<K extends "close" | "error" | "message" | "open">(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void;
|
||||
public removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void;
|
||||
public removeEventListener(type: any, listener: any, options?: any) {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public dispatchEvent(evt: Event): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
constructor(url: string, protocols?: string | string[]) {
|
||||
this.url = url;
|
||||
this.protocol = protocols ? (typeof protocols === "string" ? protocols : protocols[0]) : "";
|
||||
TestWebSocket.webSocket = this;
|
||||
this.receivedData = [];
|
||||
}
|
||||
|
||||
public readonly CLOSED: number = 1;
|
||||
public static readonly CLOSED: number = 1;
|
||||
public readonly CLOSING: number = 2;
|
||||
public static readonly CLOSING: number = 2;
|
||||
public readonly CONNECTING: number = 3;
|
||||
public static readonly CONNECTING: number = 3;
|
||||
public readonly OPEN: number = 4;
|
||||
public static readonly OPEN: number = 4;
|
||||
}
|
||||
|
||||
export class TestEvent {
|
||||
public bubbles: boolean = false;
|
||||
public cancelBubble: boolean = false;
|
||||
public cancelable: boolean = false;
|
||||
public currentTarget!: EventTarget;
|
||||
public defaultPrevented: boolean = false;
|
||||
public eventPhase: number = 0;
|
||||
public isTrusted: boolean = false;
|
||||
public returnValue: boolean = false;
|
||||
public scoped: boolean = false;
|
||||
public srcElement!: Element | null;
|
||||
public target!: EventTarget;
|
||||
public timeStamp: number = 0;
|
||||
public type: string = "";
|
||||
public deepPath(): EventTarget[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public preventDefault(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public stopImmediatePropagation(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public stopPropagation(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public AT_TARGET: number = 0;
|
||||
public BUBBLING_PHASE: number = 0;
|
||||
public CAPTURING_PHASE: number = 0;
|
||||
public NONE: number = 0;
|
||||
}
|
||||
|
||||
export class TestErrorEvent {
|
||||
public colno: number = 0;
|
||||
public error: any;
|
||||
public filename: string = "";
|
||||
public lineno: number = 0;
|
||||
public message: string = "";
|
||||
public initErrorEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, messageArg: string, filenameArg: string, linenoArg: number): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public bubbles: boolean = false;
|
||||
public cancelBubble: boolean = false;
|
||||
public cancelable: boolean = false;
|
||||
public currentTarget!: EventTarget | null;
|
||||
public defaultPrevented: boolean = false;
|
||||
public eventPhase: number = 0;
|
||||
public isTrusted: boolean = false;
|
||||
public returnValue: boolean = false;
|
||||
public scoped: boolean = false;
|
||||
public srcElement!: Element | null;
|
||||
public target!: EventTarget | null;
|
||||
public timeStamp: number = 0;
|
||||
public type: string = "";
|
||||
public deepPath(): EventTarget[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public preventDefault(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public stopImmediatePropagation(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public stopPropagation(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public AT_TARGET: number = 0;
|
||||
public BUBBLING_PHASE: number = 0;
|
||||
public CAPTURING_PHASE: number = 0;
|
||||
public NONE: number = 0;
|
||||
}
|
||||
|
||||
export class TestCloseEvent {
|
||||
public code: number = 0;
|
||||
public reason: string = "";
|
||||
public wasClean: boolean = false;
|
||||
public initCloseEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, wasCleanArg: boolean, codeArg: number, reasonArg: string): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public bubbles: boolean = false;
|
||||
public cancelBubble: boolean = false;
|
||||
public cancelable: boolean = false;
|
||||
public currentTarget!: EventTarget;
|
||||
public defaultPrevented: boolean = false;
|
||||
public eventPhase: number = 0;
|
||||
public isTrusted: boolean = false;
|
||||
public returnValue: boolean = false;
|
||||
public scoped: boolean = false;
|
||||
public srcElement!: Element | null;
|
||||
public target!: EventTarget;
|
||||
public timeStamp: number = 0;
|
||||
public type: string = "";
|
||||
public deepPath(): EventTarget[] {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public initEvent(type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public preventDefault(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public stopImmediatePropagation(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public stopPropagation(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public AT_TARGET: number = 0;
|
||||
public BUBBLING_PHASE: number = 0;
|
||||
public CAPTURING_PHASE: number = 0;
|
||||
public NONE: number = 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { ILogger } from "../src/ILogger";
|
||||
import { TransferFormat } from "../src/ITransport";
|
||||
import { WebSocketTransport } from "../src/WebSocketTransport";
|
||||
import { VerifyLogger } from "./Common";
|
||||
import { TestMessageEvent } from "./TestEventSource";
|
||||
import { TestCloseEvent, TestEvent, TestWebSocket } from "./TestWebSocket";
|
||||
|
||||
describe("WebSocketTransport", () => {
|
||||
it("sets websocket binarytype to arraybuffer on Binary transferformat", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
await createAndStartWebSocket(logger, "http://example.com", undefined, TransferFormat.Binary);
|
||||
|
||||
expect(TestWebSocket.webSocket.binaryType).toBe("arraybuffer");
|
||||
});
|
||||
});
|
||||
|
||||
it("connect waits for WebSocket to be connected", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
const webSocket = new WebSocketTransport(undefined, logger, true, TestWebSocket);
|
||||
|
||||
let connectComplete: boolean = false;
|
||||
const connectPromise = (async () => {
|
||||
await webSocket.connect("http://example.com", TransferFormat.Text);
|
||||
connectComplete = true;
|
||||
})();
|
||||
|
||||
await TestWebSocket.webSocket.openSet;
|
||||
|
||||
expect(connectComplete).toBe(false);
|
||||
|
||||
TestWebSocket.webSocket.onopen(new TestEvent());
|
||||
|
||||
await connectPromise;
|
||||
expect(connectComplete).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("connect fails if there is error during connect", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
(global as any).ErrorEvent = TestEvent;
|
||||
const webSocket = new WebSocketTransport(undefined, logger, true, TestWebSocket);
|
||||
|
||||
let connectComplete: boolean = false;
|
||||
const connectPromise = (async () => {
|
||||
await webSocket.connect("http://example.com", TransferFormat.Text);
|
||||
connectComplete = true;
|
||||
})();
|
||||
|
||||
await TestWebSocket.webSocket.openSet;
|
||||
|
||||
expect(connectComplete).toBe(false);
|
||||
|
||||
TestWebSocket.webSocket.onerror(new TestEvent());
|
||||
|
||||
await expect(connectPromise)
|
||||
.rejects;
|
||||
expect(connectComplete).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
[["http://example.com", "ws://example.com?access_token=secretToken"],
|
||||
["http://example.com?value=null", "ws://example.com?value=null&access_token=secretToken"],
|
||||
["https://example.com?value=null", "wss://example.com?value=null&access_token=secretToken"]]
|
||||
.forEach(([input, expected]) => {
|
||||
it(`generates correct WebSocket URL for ${input} with access_token`, async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
await createAndStartWebSocket(logger, input, () => "secretToken");
|
||||
|
||||
expect(TestWebSocket.webSocket.url).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
[["http://example.com", "ws://example.com"],
|
||||
["http://example.com?value=null", "ws://example.com?value=null"],
|
||||
["https://example.com?value=null", "wss://example.com?value=null"]]
|
||||
.forEach(([input, expected]) => {
|
||||
it(`generates correct WebSocket URL for ${input}`, async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
await createAndStartWebSocket(logger, input, undefined);
|
||||
|
||||
expect(TestWebSocket.webSocket.url).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("can receive data", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
const webSocket = await createAndStartWebSocket(logger);
|
||||
|
||||
let received: string | ArrayBuffer;
|
||||
webSocket.onreceive = (data) => {
|
||||
received = data;
|
||||
};
|
||||
|
||||
const message = new TestMessageEvent();
|
||||
message.data = "receive data";
|
||||
TestWebSocket.webSocket.onmessage(message);
|
||||
|
||||
expect(typeof received!).toBe("string");
|
||||
expect(received!).toBe("receive data");
|
||||
});
|
||||
});
|
||||
|
||||
it("is closed from WebSocket onclose with error", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
(global as any).ErrorEvent = TestEvent;
|
||||
const webSocket = await createAndStartWebSocket(logger);
|
||||
|
||||
let closeCalled: boolean = false;
|
||||
let error: Error;
|
||||
webSocket.onclose = (e) => {
|
||||
closeCalled = true;
|
||||
error = e!;
|
||||
};
|
||||
|
||||
const message = new TestCloseEvent();
|
||||
message.wasClean = false;
|
||||
message.code = 1;
|
||||
message.reason = "just cause";
|
||||
TestWebSocket.webSocket.onclose(message);
|
||||
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(error!).toEqual(new Error("Websocket closed with status code: 1 (just cause)"));
|
||||
|
||||
await expect(webSocket.send(""))
|
||||
.rejects
|
||||
.toThrow("WebSocket is not in the OPEN state");
|
||||
});
|
||||
});
|
||||
|
||||
it("is closed from WebSocket onclose", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
(global as any).ErrorEvent = TestEvent;
|
||||
const webSocket = await createAndStartWebSocket(logger);
|
||||
|
||||
let closeCalled: boolean = false;
|
||||
let error: Error;
|
||||
webSocket.onclose = (e) => {
|
||||
closeCalled = true;
|
||||
error = e!;
|
||||
};
|
||||
|
||||
const message = new TestCloseEvent();
|
||||
message.wasClean = true;
|
||||
message.code = 1000;
|
||||
message.reason = "success";
|
||||
TestWebSocket.webSocket.onclose(message);
|
||||
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(error!).toBeUndefined();
|
||||
|
||||
await expect(webSocket.send(""))
|
||||
.rejects
|
||||
.toThrow("WebSocket is not in the OPEN state");
|
||||
});
|
||||
});
|
||||
|
||||
it("is closed from Transport stop", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
(global as any).ErrorEvent = TestEvent;
|
||||
const webSocket = await createAndStartWebSocket(logger);
|
||||
|
||||
let closeCalled: boolean = false;
|
||||
let error: Error;
|
||||
webSocket.onclose = (e) => {
|
||||
closeCalled = true;
|
||||
error = e!;
|
||||
};
|
||||
|
||||
await webSocket.stop();
|
||||
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(error!).toBeUndefined();
|
||||
|
||||
await expect(webSocket.send(""))
|
||||
.rejects
|
||||
.toThrow("WebSocket is not in the OPEN state");
|
||||
});
|
||||
});
|
||||
|
||||
[[TransferFormat.Text, "send data"],
|
||||
[TransferFormat.Binary, new Uint8Array([0, 1, 3])]]
|
||||
.forEach(([format, data]) => {
|
||||
it(`can send ${TransferFormat[format as TransferFormat]} data`, async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
const webSocket = await createAndStartWebSocket(logger, "http://example.com", undefined, format as TransferFormat);
|
||||
|
||||
TestWebSocket.webSocket.readyState = TestWebSocket.OPEN;
|
||||
await webSocket.send(data);
|
||||
|
||||
expect(TestWebSocket.webSocket.receivedData.length).toBe(1);
|
||||
expect(TestWebSocket.webSocket.receivedData[0]).toBe(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat): Promise<WebSocketTransport> {
|
||||
const webSocket = new WebSocketTransport(accessTokenFactory, logger, true, TestWebSocket);
|
||||
|
||||
const connectPromise = webSocket.connect(url || "http://example.com", format || TransferFormat.Text);
|
||||
|
||||
await TestWebSocket.webSocket.openSet;
|
||||
TestWebSocket.webSocket.onopen(new TestEvent());
|
||||
|
||||
await connectPromise;
|
||||
|
||||
return webSocket;
|
||||
}
|
||||
Loading…
Reference in New Issue