diff --git a/README.md b/README.md index e061cee..90decad 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,10 @@ A `WS` instance has the following attributes: `jest-websocket-mock` registers custom jest matchers to make assertions on received messages easier: -- `.toReceiveMessage`: async matcher that waits for the next message received +- `.toReceiveMessage`: async matcher that waits 1000ms for the the mock websocket + server to receive the expected message. It will time out with a helpful + message after 1000ms. +- `.toReceiveMessageNext`: async matcher that waits for the next message received by the the mock websocket server, and asserts its content. It will time out with a helpful message after 1000ms. - `.toHaveReceivedMessages`: synchronous matcher that checks that all the diff --git a/src/__tests__/matchers.test.ts b/src/__tests__/matchers.test.ts index 4740154..573cd99 100644 --- a/src/__tests__/matchers.test.ts +++ b/src/__tests__/matchers.test.ts @@ -52,8 +52,9 @@ describe(".toReceiveMessage", () => { .toThrowErrorMatchingInlineSnapshot(` "expect(WS).toReceiveMessage(expected) - Expected the websocket server to receive a message, - but it didn't receive anything in 1000ms." + Expected the following message within 1000ms: + "hello there" + but it didn't receive anything." `); }); @@ -64,8 +65,9 @@ describe(".toReceiveMessage", () => { ).rejects.toThrowErrorMatchingInlineSnapshot(` "expect(WS).toReceiveMessage(expected) - Expected the websocket server to receive a message, - but it didn't receive anything in 3000ms." + Expected the following message within 3000ms: + "hello there" + but it didn't receive anything." `); }); @@ -76,6 +78,179 @@ describe(".toReceiveMessage", () => { .toThrowErrorMatchingInlineSnapshot(` "expect(WS).toReceiveMessage(expected) + Expected the following message within 1000ms: + "HI!" + but instead received the following messages: + ["hello there"] + + Difference: + + - Expected + + Received + +  Array [ + - "HI!", + + "hello there", +  ]" + `); + }); + + it("displays all messages that were received instead of the expected message", async () => { + expect.hasAssertions(); + client.send("hello there"); + client.send("this is a test message"); + client.send("something else"); + await expect(expect(server).toReceiveMessage("HI!")).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).toReceiveMessage(expected) + + Expected the following message within 1000ms: + "HI!" + but instead received the following messages: + ["hello there", "this is a test message", "something else"] + + Difference: + + - Expected + + Received + +  Array [ + - "HI!", + + "hello there", + + "this is a test message", + + "something else", +  ]" + `); + }); + + it("fails when expecting a JSON message but the server is not configured for JSON protocols", async () => { + expect.hasAssertions(); + client.send(`{"answer":42}`); + await expect(expect(server).toReceiveMessage({ answer: 42 })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).toReceiveMessage(expected) + + Expected the following message within 1000ms: + {"answer": 42} + but instead received the following messages: + ["{\\"answer\\":42}"] + + Difference: + + - Expected + + Received + +  Array [ + - Object { + - "answer": 42, + - }, + + "{\\"answer\\":42}", +  ]" + `); + }); +}); + +describe(".not.toReceiveMessage", () => { + it("passes when the websocket server doesn't receive the expected message", async () => { + client.send("hello there"); + await expect(server).not.toReceiveMessage("What's up?"); + }); + + it("fails when called with an expected argument that is not a valid WS", async () => { + expect.hasAssertions(); + await expect(expect("boom").not.toReceiveMessage("hello there")).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).not.toReceiveMessage(expected) + + Expected the websocket object to be a valid WS mock. + Received: string + "boom"" + `); + }); + + it("passes when the WS server doesn't receive any messages", async () => { + expect.hasAssertions(); + await expect(server).not.toReceiveMessage("hello there"); + }); + + it("fails when the WS server receives the un-expected message", async () => { + expect.hasAssertions(); + client.send("hello there"); + await expect(expect(server).not.toReceiveMessage("hello there")).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).not.toReceiveMessage(expected) + + Did not expect to receive the following message: + "hello there" + but received it." + `); + }); +}); + +describe(".toReceiveMessageNext", () => { + it("passes when the websocket server receives the expected message", async () => { + client.send("hello there"); + await expect(server).toReceiveMessageNext("hello there"); + }); + + it("passes when the websocket server receives the expected message with custom timeout", async () => { + setTimeout(() => { + client.send("hello there"); + }, 2000); + + await expect(server).toReceiveMessageNext("hello there", { timeout: 3000 }); + }); + + it("passes when the websocket server receives the expected JSON message", async () => { + const jsonServer = new WS("ws://localhost:9876", { jsonProtocol: true }); + const jsonClient = new WebSocket("ws://localhost:9876"); + await jsonServer.connected; + jsonClient.send(`{"answer":42}`); + await expect(jsonServer).toReceiveMessageNext({ answer: 42 }); + }); + + it("fails when called with an expected argument that is not a valid WS", async () => { + expect.hasAssertions(); + await expect(expect("boom").toReceiveMessageNext("hello there")).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).toReceiveMessageNext(expected) + + Expected the websocket object to be a valid WS mock. + Received: string + "boom"" + `); + }); + + it("fails when the WS server does not receive the expected message", async () => { + expect.hasAssertions(); + await expect(expect(server).toReceiveMessageNext("hello there")).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).toReceiveMessageNext(expected) + + Expected the websocket server to receive a message, + but it didn't receive anything in 1000ms." + `); + }); + + it("fails when the WS server does not receive the expected message with custom timeout", async () => { + expect.hasAssertions(); + await expect( + expect(server).toReceiveMessageNext("hello there", { timeout: 3000 }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "expect(WS).toReceiveMessageNext(expected) + + Expected the websocket server to receive a message, + but it didn't receive anything in 3000ms." + `); + }); + + it("fails when the WS server receives a different message", async () => { + expect.hasAssertions(); + client.send("hello there"); + await expect(expect(server).toReceiveMessageNext("HI!")).rejects + .toThrowErrorMatchingInlineSnapshot(` + "expect(WS).toReceiveMessageNext(expected) + Expected the next received message to equal: "HI!" Received: @@ -94,9 +269,9 @@ describe(".toReceiveMessage", () => { it("fails when expecting a JSON message but the server is not configured for JSON protocols", async () => { expect.hasAssertions(); client.send(`{"answer":42}`); - await expect(expect(server).toReceiveMessage({ answer: 42 })).rejects + await expect(expect(server).toReceiveMessageNext({ answer: 42 })).rejects .toThrowErrorMatchingInlineSnapshot(` - "expect(WS).toReceiveMessage(expected) + "expect(WS).toReceiveMessageNext(expected) Expected the next received message to equal: {"answer": 42} @@ -110,17 +285,17 @@ describe(".toReceiveMessage", () => { }); }); -describe(".not.toReceiveMessage", () => { +describe(".not.toReceiveMessageNext", () => { it("passes when the websocket server doesn't receive the expected message", async () => { client.send("hello there"); - await expect(server).not.toReceiveMessage("What's up?"); + await expect(server).not.toReceiveMessageNext("What's up?"); }); it("fails when called with an expected argument that is not a valid WS", async () => { expect.hasAssertions(); - await expect(expect("boom").not.toReceiveMessage("hello there")).rejects + await expect(expect("boom").not.toReceiveMessageNext("hello there")).rejects .toThrowErrorMatchingInlineSnapshot(` - "expect(WS).not.toReceiveMessage(expected) + "expect(WS).not.toReceiveMessageNext(expected) Expected the websocket object to be a valid WS mock. Received: string @@ -130,9 +305,9 @@ describe(".not.toReceiveMessage", () => { it("fails when the WS server doesn't receive any messages", async () => { expect.hasAssertions(); - await expect(expect(server).not.toReceiveMessage("hello there")).rejects + await expect(expect(server).not.toReceiveMessageNext("hello there")).rejects .toThrowErrorMatchingInlineSnapshot(` - "expect(WS).not.toReceiveMessage(expected) + "expect(WS).not.toReceiveMessageNext(expected) Expected the websocket server to receive a message, but it didn't receive anything in 1000ms." @@ -142,9 +317,9 @@ describe(".not.toReceiveMessage", () => { it("fails when the WS server receives the un-expected message", async () => { expect.hasAssertions(); client.send("hello there"); - await expect(expect(server).not.toReceiveMessage("hello there")).rejects + await expect(expect(server).not.toReceiveMessageNext("hello there")).rejects .toThrowErrorMatchingInlineSnapshot(` - "expect(WS).not.toReceiveMessage(expected) + "expect(WS).not.toReceiveMessageNext(expected) Expected the next received message to not equal: "hello there" diff --git a/src/matchers.ts b/src/matchers.ts index 5728b73..8b3111e 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -13,6 +13,10 @@ declare global { message: DeserializedMessage, options?: ReceiveMessageOptions, ): Promise; + toReceiveMessageNext( + message: DeserializedMessage, + options?: ReceiveMessageOptions, + ): Promise; toHaveReceivedMessages( messages: Array>, ): R; @@ -55,6 +59,84 @@ expect.extend({ }; } + const waitDelay = options?.timeout ?? WAIT_DELAY; + let receivedMessages: DeserializedMessage[] = []; + + let timeoutId; + const messageOrTimeout = await Promise.race([ + new Promise(async (resolve) => { + let message; + + while (1) { + const received = await ws.nextMessage; + + receivedMessages.push(received); + + const pass = this.equals(received, expected); + + if (pass) { + message = () => + this.utils.matcherHint(".not.toReceiveMessage", "WS", "expected") + + "\n\n" + + `Did not expect to receive the following message:\n` + + ` ${this.utils.printExpected(expected)}\n` + + `but received it.`; + break; + } + } + + resolve(message); + }), + new Promise((resolve) => { + timeoutId = setTimeout(() => resolve(TIMEOUT), waitDelay); + }), + ]); + clearTimeout(timeoutId); + + if (messageOrTimeout === TIMEOUT) { + return { + actual: receivedMessages, + expected, + pass: false, // always fail + message: () => + this.utils.matcherHint( + this.isNot ? ".not.toReceiveMessage" : ".toReceiveMessage", + "WS", + "expected", + ) + + "\n\n" + + `${this.isNot ? "Did not expect" : "Expected"} the following message within ${waitDelay}ms:\n` + + ` ${this.utils.printExpected(expected)}\n` + + ((receivedMessages.length > 0) ? + "but instead received the following messages:\n" + + ` ${this.utils.printReceived(receivedMessages)}\n\n` + + `Difference:\n\n${diff([expected], receivedMessages, { expand: this.expand })}` : `but it didn't receive anything.`) + }; + } + const message = messageOrTimeout; + + return { + actual: receivedMessages, + expected, + message: message, + name: "toReceiveMessage", + pass: true, + }; + }, + + async toReceiveMessageNext( + ws: WS, + expected: DeserializedMessage, + options?: ReceiveMessageOptions, + ) { + const isWS = ws instanceof WS; + if (!isWS) { + return { + pass: !!this.isNot, // always fail + message: makeInvalidWsMessage.bind(this, ws, "toReceiveMessageNext"), + }; + } + const waitDelay = options?.timeout ?? WAIT_DELAY; let timeoutId; @@ -71,7 +153,7 @@ expect.extend({ pass: !!this.isNot, // always fail message: () => this.utils.matcherHint( - this.isNot ? ".not.toReceiveMessage" : ".toReceiveMessage", + this.isNot ? ".not.toReceiveMessageNext" : ".toReceiveMessageNext", "WS", "expected", ) + @@ -86,7 +168,7 @@ expect.extend({ const message = pass ? () => - this.utils.matcherHint(".not.toReceiveMessage", "WS", "expected") + + this.utils.matcherHint(".not.toReceiveMessageNext", "WS", "expected") + "\n\n" + `Expected the next received message to not equal:\n` + ` ${this.utils.printExpected(expected)}\n` + @@ -95,7 +177,7 @@ expect.extend({ : () => { const diffString = diff(expected, received, { expand: this.expand }); return ( - this.utils.matcherHint(".toReceiveMessage", "WS", "expected") + + this.utils.matcherHint(".toReceiveMessageNext", "WS", "expected") + "\n\n" + `Expected the next received message to equal:\n` + ` ${this.utils.printExpected(expected)}\n` + @@ -109,7 +191,7 @@ expect.extend({ actual: received, expected, message, - name: "toReceiveMessage", + name: "toReceiveMessageNext", pass, }; }, diff --git a/src/websocket.ts b/src/websocket.ts index 0c1769f..d902f94 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -24,7 +24,7 @@ export default class WS { static instances: Array = []; messages: Array = []; - messagesToConsume = new Queue(); + messagesToConsume = new Queue(); private _isConnected: Promise; private _isClosed: Promise<{}>;