Skip to content

Commit

Permalink
Add tests for ChannelFetch
Browse files Browse the repository at this point in the history
  • Loading branch information
ghengeveld committed Jul 16, 2024
1 parent d5af903 commit b706651
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 22 deletions.
96 changes: 96 additions & 0 deletions src/utils/ChannelFetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { FETCH_ABORTED, FETCH_REQUEST, FETCH_RESPONSE } from "../constants";
import { ChannelFetch } from "./ChannelFetch";
import { MockChannel } from "./MockChannel";

const resolveAfter = (ms: number, value: any) =>
new Promise((resolve) => setTimeout(resolve, ms, value));

const rejectAfter = (ms: number, reason: any) =>
new Promise((_, reject) => setTimeout(reject, ms, reason));

describe("ChannelFetch", () => {
let channel: MockChannel;

beforeEach(() => {
channel = new MockChannel();
});

it("should handle fetch requests", async () => {
const fetch = vi.fn(() => resolveAfter(100, { headers: [], text: async () => "data" }));
ChannelFetch.subscribe("req", channel, fetch as any);

channel.emit(FETCH_REQUEST, {
requestId: "req",
input: "https://example.com",
init: { headers: { foo: "bar" } },
});

await vi.waitFor(() => {
expect(fetch).toHaveBeenCalledWith("https://example.com", {
headers: { foo: "bar" },
signal: expect.any(AbortSignal),
});
});
});

it("should send fetch responses", async () => {
const fetch = vi.fn(() => resolveAfter(100, { headers: [], text: async () => "data" }));
const instance = ChannelFetch.subscribe("res", channel, fetch as any);

const promise = new Promise<void>((resolve) => {
channel.on(FETCH_RESPONSE, ({ response, error }) => {
expect(response.body).toBe("data");
expect(error).toBeUndefined();
resolve();
});
});

channel.emit(FETCH_REQUEST, { requestId: "res", input: "https://example.com" });
await vi.waitFor(() => {
expect(instance.abortControllers.size).toBe(1);
});

await promise;

expect(instance.abortControllers.size).toBe(0);
});

it("should send fetch error responses", async () => {
const fetch = vi.fn(() => rejectAfter(100, new Error("oops")));
const instance = ChannelFetch.subscribe("err", channel, fetch as any);

const promise = new Promise<void>((resolve) => {
channel.on(FETCH_RESPONSE, ({ response, error }) => {
expect(response).toBeUndefined();
expect(error).toMatch(/oops/);
resolve();
});
});

channel.emit(FETCH_REQUEST, { requestId: "err", input: "https://example.com" });
await vi.waitFor(() => {
expect(instance.abortControllers.size).toBe(1);
});

await promise;
expect(instance.abortControllers.size).toBe(0);
});

it("should abort fetch requests", async () => {
const fetch = vi.fn((input, init) => new Promise<Response>(() => {}));
const instance = ChannelFetch.subscribe("abort", channel, fetch);

channel.emit(FETCH_REQUEST, { requestId: "abort", input: "https://example.com" });
await vi.waitFor(() => {
expect(instance.abortControllers.size).toBe(1);
});

channel.emit(FETCH_ABORTED, { requestId: "abort" });
await vi.waitFor(() => {
expect(fetch.mock.lastCall?.[1].signal.aborted).toBe(true);
expect(instance.abortControllers.size).toBe(0);
});
});
});
11 changes: 6 additions & 5 deletions src/utils/ChannelFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class ChannelFetch {

abortControllers: Map<string, AbortController>;

constructor(channel: ChannelLike) {
constructor(channel: ChannelLike, _fetch = fetch) {
this.channel = channel;
this.abortControllers = new Map<string, AbortController>();

Expand All @@ -25,21 +25,22 @@ export class ChannelFetch {
this.abortControllers.set(requestId, controller);

try {
const res = await fetch(input as RequestInfo, { ...init, signal: controller.signal });
const res = await _fetch(input as RequestInfo, { ...init, signal: controller.signal });
const body = await res.text();
const headers = Array.from(res.headers as any);
const response = { body, headers, status: res.status, statusText: res.statusText };
this.channel.emit(FETCH_RESPONSE, { requestId, response });
} catch (error) {
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
this.channel.emit(FETCH_RESPONSE, { requestId, error });
} finally {
this.abortControllers.delete(requestId);
}
});
}

static subscribe<T>(key: string, channel: ChannelLike) {
const instance = instances.get(key) || new ChannelFetch(channel);
static subscribe(key: string, channel: ChannelLike, _fetch = fetch) {
const instance = instances.get(key) || new ChannelFetch(channel, _fetch);
if (!instances.has(key)) instances.set(key, instance);
return instance;
}
Expand Down
16 changes: 16 additions & 0 deletions src/utils/MockChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export class MockChannel {
private listeners: Record<string, ((...args: any[]) => void)[]> = {};

on(event: string, listener: (...args: any[]) => void) {
this.listeners[event] = [...(this.listeners[event] ?? []), listener];
}

off(event: string, listener: (...args: any[]) => void) {
this.listeners[event] = (this.listeners[event] ?? []).filter((l) => l !== listener);
}

emit(event: string, ...args: any[]) {
// setTimeout is used to simulate the asynchronous nature of the real channel
(this.listeners[event] || []).forEach((listener) => setTimeout(() => listener(...args)));
}
}
18 changes: 1 addition & 17 deletions src/utils/SharedState.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import { beforeEach, describe, expect, it } from "vitest";

import { MockChannel } from "./MockChannel";
import { SharedState } from "./SharedState";

class MockChannel {
private listeners: Record<string, ((...args: any[]) => void)[]> = {};

on(event: string, listener: (...args: any[]) => void) {
this.listeners[event] = [...(this.listeners[event] ?? []), listener];
}

off(event: string, listener: (...args: any[]) => void) {
this.listeners[event] = (this.listeners[event] ?? []).filter((l) => l !== listener);
}

emit(event: string, ...args: any[]) {
// setTimeout is used to simulate the asynchronous nature of the real channel
(this.listeners[event] || []).forEach((listener) => setTimeout(() => listener(...args)));
}
}

const tick = () => new Promise((resolve) => setTimeout(resolve, 0));

describe("SharedState", () => {
Expand Down

0 comments on commit b706651

Please sign in to comment.