map: tickets 06+07 resolved; 05 partial (live spawn pending gitea token)
This commit is contained in:
+171
-126
@@ -2,167 +2,212 @@ import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getSettings, saveSettings } from "./settings";
|
||||
import { classNames, relativeTime, useSessions, useToasts } from "./store";
|
||||
import { FakeWebSocket, mockFetchJson, seedSettings, stubReload } from "./test/setup";
|
||||
import {
|
||||
FakeWebSocket,
|
||||
mockFetchJson,
|
||||
seedSettings,
|
||||
stubReload,
|
||||
} from "./test/setup";
|
||||
import type { EventFrame } from "./protocol";
|
||||
|
||||
const NOW = new Date("2024-05-01T12:00:00Z").getTime();
|
||||
|
||||
describe("relativeTime", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(NOW);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(NOW);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("formats all buckets", () => {
|
||||
expect(relativeTime(null)).toBe("never");
|
||||
expect(relativeTime(NOW - 10_000)).toBe("just now");
|
||||
expect(relativeTime(NOW + 30_000)).toBe("just now");
|
||||
expect(relativeTime(NOW - 5 * 60_000)).toBe("5m ago");
|
||||
expect(relativeTime(NOW - 3 * 3_600_000)).toBe("3h ago");
|
||||
expect(relativeTime(NOW - 2 * 86_400_000)).toBe("2d ago");
|
||||
});
|
||||
it("formats all buckets", () => {
|
||||
expect(relativeTime(null)).toBe("never");
|
||||
expect(relativeTime(NOW - 10_000)).toBe("just now");
|
||||
expect(relativeTime(NOW + 30_000)).toBe("just now");
|
||||
expect(relativeTime(NOW - 5 * 60_000)).toBe("5m ago");
|
||||
expect(relativeTime(NOW - 3 * 3_600_000)).toBe("3h ago");
|
||||
expect(relativeTime(NOW - 2 * 86_400_000)).toBe("2d ago");
|
||||
});
|
||||
});
|
||||
|
||||
describe("classNames", () => {
|
||||
it("joins truthy parts", () => {
|
||||
expect(classNames("a", false, "b", null, undefined, "c")).toBe("a b c");
|
||||
expect(classNames()).toBe("");
|
||||
});
|
||||
it("joins truthy parts", () => {
|
||||
expect(classNames("a", false, "b", null, undefined, "c")).toBe("a b c");
|
||||
expect(classNames()).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("useToasts", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("pushes a toast and removes it after TTL", () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => result.current.push("hello"));
|
||||
expect(result.current.toasts.map((t) => t.text)).toEqual(["hello"]);
|
||||
it("pushes a toast and removes it after TTL", () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => result.current.push("hello"));
|
||||
expect(result.current.toasts.map((t) => t.text)).toEqual(["hello"]);
|
||||
|
||||
act(() => result.current.push("second"));
|
||||
expect(result.current.toasts).toHaveLength(2);
|
||||
act(() => result.current.push("second"));
|
||||
expect(result.current.toasts).toHaveLength(2);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(4000);
|
||||
});
|
||||
expect(result.current.toasts).toHaveLength(0);
|
||||
});
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(4000);
|
||||
});
|
||||
expect(result.current.toasts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
function listEvent(seq: number): EventFrame {
|
||||
return { v: 1, sessionId: "s1", seq, ts: 0, type: "message_end" };
|
||||
return { v: 1, sessionId: "s1", seq, ts: 0, type: "message_end" };
|
||||
}
|
||||
|
||||
describe("useSessions", () => {
|
||||
it("returns null when unconfigured", () => {
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
it("returns null when unconfigured", () => {
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("seeds via REST, updates from session_list and spawn_status frames, refresh works", async () => {
|
||||
seedSettings();
|
||||
const sessions = [{ id: "s1", name: "one", online: true }];
|
||||
const fetchMock = mockFetchJson((url) => {
|
||||
if (url.includes("/api/sessions")) return url.includes("/events") ? [] : sessions;
|
||||
return [];
|
||||
});
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
it("seeds via REST, updates from session_list and spawn_status frames, refresh works", async () => {
|
||||
seedSettings();
|
||||
const sessions = [{ id: "s1", name: "one", online: true }];
|
||||
const fetchMock = mockFetchJson((url) => {
|
||||
if (url.includes("/api/sessions"))
|
||||
return url.includes("/events") ? [] : sessions;
|
||||
return [];
|
||||
});
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
|
||||
expect(result.current?.state).toBe("connecting");
|
||||
await waitFor(() => expect(result.current?.sessions).toEqual(sessions));
|
||||
expect(result.current?.state).toBe("connecting");
|
||||
await waitFor(() => expect(result.current?.sessions).toEqual(sessions));
|
||||
|
||||
const sock = FakeWebSocket.last();
|
||||
act(() => sock.serverOpen());
|
||||
expect(result.current?.state).toBe("open");
|
||||
const sock = FakeWebSocket.last();
|
||||
act(() => sock.serverOpen());
|
||||
expect(result.current?.state).toBe("open");
|
||||
|
||||
act(() => sock.serverMessage({ type: "session_list", sessions: [{ id: "s2", name: "two", online: false }] }));
|
||||
expect(result.current?.sessions.map((s) => s.id)).toEqual(["s2"]);
|
||||
act(() =>
|
||||
sock.serverMessage({
|
||||
type: "session_list",
|
||||
sessions: [{ id: "s2", name: "two", online: false }],
|
||||
}),
|
||||
);
|
||||
expect(result.current?.sessions.map((s) => s.id)).toEqual(["s2"]);
|
||||
|
||||
act(() => sock.serverMessage({ type: "spawn_status", jobs: [{ repo: "g/p", state: "cloning" }] }));
|
||||
expect(result.current?.spawnJobs).toEqual([{ repo: "g/p", state: "cloning" }]);
|
||||
act(() =>
|
||||
sock.serverMessage({
|
||||
type: "spawn_status",
|
||||
jobs: [{ repo: "g/p", state: "cloning" }],
|
||||
}),
|
||||
);
|
||||
expect(result.current?.spawnJobs).toEqual([
|
||||
{ repo: "g/p", state: "cloning" },
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
await result.current?.refresh();
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
});
|
||||
await act(async () => {
|
||||
await result.current?.refresh();
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refresh failure pushes a toast", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => {
|
||||
throw new Error("network down");
|
||||
});
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
await waitFor(() => expect(push).toHaveBeenCalledWith("sessions: network down"));
|
||||
expect(result.current).not.toBeNull();
|
||||
});
|
||||
it("refresh failure pushes a toast", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => {
|
||||
throw new Error("network down");
|
||||
});
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
await waitFor(() =>
|
||||
expect(push).toHaveBeenCalledWith("sessions: network down"),
|
||||
);
|
||||
expect(result.current).not.toBeNull();
|
||||
});
|
||||
|
||||
it("subscribe routes events frames for the session and detaches on unsubscribe", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => []);
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
const sock = FakeWebSocket.last();
|
||||
act(() => sock.serverOpen());
|
||||
it("subscribe routes events frames for the session and detaches on unsubscribe", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => []);
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
const sock = FakeWebSocket.last();
|
||||
act(() => sock.serverOpen());
|
||||
|
||||
const seen: EventFrame[][] = [];
|
||||
let off: (() => void) | undefined;
|
||||
act(() => {
|
||||
off = result.current?.subscribe("s1", (events) => seen.push(events));
|
||||
});
|
||||
expect(sock.sent).toContain(JSON.stringify({ type: "subscribe", sessionId: "s1" }));
|
||||
const seen: EventFrame[][] = [];
|
||||
let off: (() => void) | undefined;
|
||||
act(() => {
|
||||
off = result.current?.subscribe("s1", (events) => seen.push(events));
|
||||
});
|
||||
expect(sock.sent).toContain(
|
||||
JSON.stringify({ type: "subscribe", sessionId: "s1" }),
|
||||
);
|
||||
|
||||
act(() => sock.serverMessage({ type: "events", sessionId: "s1", after: 0, events: [listEvent(1)] }));
|
||||
act(() => sock.serverMessage({ type: "events", sessionId: "other", after: 0, events: [listEvent(2)] }));
|
||||
expect(seen).toEqual([[listEvent(1)]]);
|
||||
act(() =>
|
||||
sock.serverMessage({
|
||||
type: "events",
|
||||
sessionId: "s1",
|
||||
after: 0,
|
||||
events: [listEvent(1)],
|
||||
}),
|
||||
);
|
||||
act(() =>
|
||||
sock.serverMessage({
|
||||
type: "events",
|
||||
sessionId: "other",
|
||||
after: 0,
|
||||
events: [listEvent(2)],
|
||||
}),
|
||||
);
|
||||
expect(seen).toEqual([[listEvent(1)]]);
|
||||
|
||||
act(() => off?.());
|
||||
act(() => sock.serverMessage({ type: "events", sessionId: "s1", after: 1, events: [listEvent(3)] }));
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(sock.sent).toContain(JSON.stringify({ type: "unsubscribe", sessionId: "s1" }));
|
||||
});
|
||||
act(() => off?.());
|
||||
act(() =>
|
||||
sock.serverMessage({
|
||||
type: "events",
|
||||
sessionId: "s1",
|
||||
after: 1,
|
||||
events: [listEvent(3)],
|
||||
}),
|
||||
);
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(sock.sent).toContain(
|
||||
JSON.stringify({ type: "unsubscribe", sessionId: "s1" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("auth failure clears settings and reloads", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => []);
|
||||
const loc = stubReload();
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
const sock = FakeWebSocket.last();
|
||||
act(() => sock.serverOpen());
|
||||
act(() => sock.serverClose(1008));
|
||||
expect(loc.reload).toHaveBeenCalled();
|
||||
expect(getSettings()).toBeNull();
|
||||
await waitFor(() => expect(result.current).toBeNull());
|
||||
loc.restore();
|
||||
});
|
||||
it("auth failure clears settings and reloads", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => []);
|
||||
const loc = stubReload();
|
||||
const push = vi.fn();
|
||||
const { result } = renderHook(() => useSessions(push));
|
||||
const sock = FakeWebSocket.last();
|
||||
act(() => sock.serverOpen());
|
||||
act(() => sock.serverClose(1008));
|
||||
expect(loc.reload).toHaveBeenCalled();
|
||||
expect(getSettings()).toBeNull();
|
||||
await waitFor(() => expect(result.current).toBeNull());
|
||||
loc.restore();
|
||||
});
|
||||
|
||||
it("unmount closes the manager", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => []);
|
||||
const push = vi.fn();
|
||||
const { unmount } = renderHook(() => useSessions(push));
|
||||
await waitFor(() => expect(FakeWebSocket.instances.length).toBe(1));
|
||||
const sock = FakeWebSocket.last();
|
||||
unmount();
|
||||
expect(sock.closeCode).toBe(4900);
|
||||
expect(sock.onclose).toBeNull();
|
||||
});
|
||||
it("unmount closes the manager", async () => {
|
||||
seedSettings();
|
||||
mockFetchJson(() => []);
|
||||
const push = vi.fn();
|
||||
const { unmount } = renderHook(() => useSessions(push));
|
||||
await waitFor(() => expect(FakeWebSocket.instances.length).toBe(1));
|
||||
const sock = FakeWebSocket.last();
|
||||
unmount();
|
||||
expect(sock.closeCode).toBe(4900);
|
||||
expect(sock.onclose).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings persistence used by the store", () => {
|
||||
it("saved settings are visible", () => {
|
||||
saveSettings({ serverUrl: "http://a", token: "t" });
|
||||
expect(getSettings()?.serverUrl).toBe("http://a");
|
||||
});
|
||||
it("saved settings are visible", () => {
|
||||
saveSettings({ serverUrl: "http://a", token: "t" });
|
||||
expect(getSettings()?.serverUrl).toBe("http://a");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user