Files
2026-07-13 13:39:47 +08:00

131 lines
3.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { MediaGoClient } from "../src";
import * as EventSourceModule from "eventsource";
vi.mock("eventsource", () => {
const instances: any[] = [];
class BaseEvent {
type: string;
constructor(type: string) {
this.type = type;
}
}
class MockErrorEvent extends BaseEvent {
code?: number;
message?: string;
constructor(type: string, init: { code?: number; message?: string } = {}) {
super(type);
this.code = init.code;
this.message = init.message;
}
}
class MockMessageEvent<T = unknown> extends BaseEvent {
data: T | null;
constructor(type: string, init: { data?: T | null } = {}) {
super(type);
this.data = init.data ?? null;
}
}
type Listener = (event: any) => void;
class MockEventSource {
readonly url: string;
closed = false;
private readonly listeners = new Map<string, Set<Listener>>();
constructor(url: string) {
this.url = url;
instances.push(this);
}
addEventListener(type: string, listener: Listener): void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(listener);
}
removeEventListener(type: string, listener: Listener): void {
this.listeners.get(type)?.delete(listener);
}
close(): void {
this.closed = true;
this.listeners.clear();
}
dispatch(
type: string,
init: { data?: unknown; code?: number; message?: string } = {},
): void {
const listeners = this.listeners.get(type);
if (!listeners || listeners.size === 0) {
return;
}
const event =
type === "error"
? new MockErrorEvent(type, { code: init.code, message: init.message })
: new MockMessageEvent(type, { data: init.data });
for (const listener of listeners) {
listener(event);
}
}
}
return {
default: MockEventSource,
EventSource: MockEventSource,
ErrorEvent: MockErrorEvent,
__instances: instances,
};
});
const getInstances = () =>
(EventSourceModule as unknown as { __instances: any[] }).__instances ?? [];
describe("MediaGoClient.streamEvents", () => {
beforeEach(() => {
getInstances().length = 0;
});
it("connects to /api/events and re-emits task events", () => {
const client = new MediaGoClient({ baseURL: "http://example.com" });
const emitter = client.streamEvents();
const instances = getInstances();
expect(instances).toHaveLength(1);
expect(instances[0]?.url).toBe("http://example.com/api/events");
const onStart = vi.fn();
emitter.on("download-start", onStart);
instances[0]?.dispatch("download-start", {
data: JSON.stringify({ id: "task-1" }),
});
expect(onStart).toHaveBeenCalledWith({ id: "task-1" });
});
it("emits error events for invalid payloads and closes the source", () => {
const client = new MediaGoClient();
const emitter = client.streamEvents();
const instances = getInstances();
const source = instances.at(-1)!;
const onError = vi.fn();
emitter.on("error", onError);
source.dispatch("download-start", { data: "not-json" });
expect(onError).toHaveBeenCalledTimes(1);
emitter.close();
expect(source.closed).toBe(true);
});
});