From 3d2cfa952f678719c81642e1a2e7f871ac4fbfd8 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 28 Sep 2023 14:47:02 -0400 Subject: [PATCH] Add matchers for Observable emissions --- libs/common/spec/matchers/index.ts | 1 + libs/common/spec/matchers/to-emit.spec.ts | 54 ++++++++++++++++++++ libs/common/spec/matchers/to-emit.ts | 62 +++++++++++++++++++++++ libs/common/test.setup.ts | 10 +++- 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 libs/common/spec/matchers/to-emit.spec.ts create mode 100644 libs/common/spec/matchers/to-emit.ts diff --git a/libs/common/spec/matchers/index.ts b/libs/common/spec/matchers/index.ts index 59f6409fef..9d264f0633 100644 --- a/libs/common/spec/matchers/index.ts +++ b/libs/common/spec/matchers/index.ts @@ -1 +1,2 @@ export * from "./to-equal-buffer"; +export * from "./to-emit"; diff --git a/libs/common/spec/matchers/to-emit.spec.ts b/libs/common/spec/matchers/to-emit.spec.ts new file mode 100644 index 0000000000..0bee1bccc4 --- /dev/null +++ b/libs/common/spec/matchers/to-emit.spec.ts @@ -0,0 +1,54 @@ +import { Subject } from "rxjs"; + +describe("toEmit", () => { + let subject: Subject; + + beforeEach(() => { + subject = new Subject(); + }); + + afterEach(() => { + subject.complete(); + }); + + it("should pass if the observable emits", () => { + subject.next(true); + expect(subject).toEmit(); + }); + + it("should fail if the observable does not emit", () => { + expect(subject).not.toEmit(1); + }); + + it("should fail if the observable emits after the timeout", () => { + setTimeout(() => subject.next(true), 100); + expect(subject).not.toEmit(1); + }); +}); + +describe("toEmitValue", () => { + let subject: Subject; + + beforeEach(() => { + subject = new Subject(); + }); + + it("should pass if the observable emits the expected value", () => { + subject.next(true); + expect(subject).toEmitValue(true); + }); + + it("should fail if the observable does not emit the expected value", () => { + subject.next(true); + expect(subject).not.toEmitValue(false); + }); + + it("should fail if the observable does not emit anything", () => { + expect(subject).not.toEmitValue(true); + }); + + it("should fail if the observable emits the expected value after the timeout", () => { + setTimeout(() => subject.next(true), 100); + expect(subject).not.toEmitValue(true); + }); +}); diff --git a/libs/common/spec/matchers/to-emit.ts b/libs/common/spec/matchers/to-emit.ts new file mode 100644 index 0000000000..99747946cf --- /dev/null +++ b/libs/common/spec/matchers/to-emit.ts @@ -0,0 +1,62 @@ +import { Observable, Subscription } from "rxjs"; + +/** Asserts that an observable emitted any value */ +export const toEmit: jest.CustomMatcher = async function toEmit( + received: Observable, + timeoutMs = 100 +) { + return new Promise((resolve) => { + let subscription: Subscription = undefined; + const timeout = setTimeout(() => { + subscription?.unsubscribe(); + resolve({ + pass: false, + message: () => "expected observable to emit", + }); + }, timeoutMs); + + subscription = received.subscribe(() => { + clearTimeout(timeout); + resolve({ + pass: true, + message: () => "expected observable not to emit", + }); + }); + }); +}; + +/** Asserts that the first value emitted from an observable is equal to the expected value. + * Optionally, a comparer function can be provided to compare the values. By default, a strict equality check is used. + */ +export const toEmitValue: jest.CustomMatcher = async function toEmitValue( + received: Observable, + expected: T, + comparer?: (a: T, b: T) => boolean, + timeoutMs = 100 +) { + comparer = comparer ?? ((a, b) => a === b); + let emitted = false; + const value = await new Promise((resolve) => { + let subscription: Subscription = undefined; + const timeout = setTimeout(() => { + subscription?.unsubscribe(); + resolve(undefined); + }, timeoutMs); + + subscription = received.subscribe((value) => { + clearTimeout(timeout); + emitted = true; + resolve(value); + }); + }); + + return { + pass: emitted && comparer(value, expected), + message: () => + comparer(value, expected) + ? emitted + ? `expected observable not to emit ${expected}` + : `expected observable to emit ${expected}, but it did not emit` + : `expected observable to emit ${expected}, but it emitted `, + }; +}; diff --git a/libs/common/test.setup.ts b/libs/common/test.setup.ts index c50c7ca227..5bde602a8c 100644 --- a/libs/common/test.setup.ts +++ b/libs/common/test.setup.ts @@ -1,6 +1,6 @@ import { webcrypto } from "crypto"; -import { toEqualBuffer } from "./spec"; +import { toEmit, toEmitValue, toEqualBuffer } from "./spec"; Object.defineProperty(window, "crypto", { value: webcrypto, @@ -10,8 +10,16 @@ Object.defineProperty(window, "crypto", { expect.extend({ toEqualBuffer: toEqualBuffer, + toEmit: toEmit, + toEmitValue: toEmitValue, }); export interface CustomMatchers { toEqualBuffer(expected: Uint8Array | ArrayBuffer): R; + toEmit(timeoutMs?: number): R; + toEmitValue( + expected: unknown, + comparer?: (a: unknown, b: unknown) => boolean, + timeoutMs?: number + ): R; }