104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { apiFetch, fetchProposalDetail, fetchProposals, WorkbenchApiError } from "./client";
|
|
import { proposalDetail, proposalSummaries } from "./__fixtures__/proposals";
|
|
|
|
describe("apiFetch", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("parses ok:true envelope and returns data", async () => {
|
|
const mockData = { backend: "numpy", git_revision: "abc123" };
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
json: vi.fn().mockResolvedValue({ ok: true, generated_at: "2026-01-01T00:00:00Z", data: mockData }),
|
|
}),
|
|
);
|
|
|
|
const result = await apiFetch("/runtime/status");
|
|
expect(result).toEqual(mockData);
|
|
});
|
|
|
|
it("throws WorkbenchApiError on ok:false envelope", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
json: vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
generated_at: "2026-01-01T00:00:00Z",
|
|
error: { code: "runtime_unavailable", message: "Server is offline" },
|
|
}),
|
|
}),
|
|
);
|
|
|
|
await expect(apiFetch("/runtime/status")).rejects.toThrow(WorkbenchApiError);
|
|
await expect(apiFetch("/runtime/status")).rejects.toMatchObject({
|
|
code: "runtime_unavailable",
|
|
message: "Server is offline",
|
|
});
|
|
});
|
|
|
|
it("AbortController fires on timeout (never-resolving fetch)", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const abortMock = vi.fn();
|
|
const originalAbortController = global.AbortController;
|
|
|
|
// Replace AbortController to track abort calls
|
|
const mockController = {
|
|
signal: { aborted: false } as AbortSignal,
|
|
abort: abortMock,
|
|
};
|
|
vi.stubGlobal("AbortController", vi.fn(() => mockController));
|
|
|
|
// fetch never resolves
|
|
vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {})));
|
|
|
|
const fetchPromise = apiFetch("/runtime/status");
|
|
|
|
// Advance past the 5s timeout
|
|
await vi.advanceTimersByTimeAsync(6000);
|
|
|
|
expect(abortMock).toHaveBeenCalled();
|
|
|
|
// Clean up
|
|
global.AbortController = originalAbortController;
|
|
vi.useRealTimers();
|
|
// fetchPromise will never resolve but that's expected
|
|
fetchPromise.catch(() => {});
|
|
});
|
|
});
|
|
|
|
describe("proposal fetchers", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("unwraps the /proposals items envelope and applies a state filter locally", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
json: vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
generated_at: "now",
|
|
data: { items: proposalSummaries },
|
|
}),
|
|
}),
|
|
);
|
|
|
|
await expect(fetchProposals("accepted")).resolves.toEqual([proposalSummaries[1]]);
|
|
});
|
|
|
|
it("fetches proposal detail without mutating the proposal log", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
json: vi.fn().mockResolvedValue({ ok: true, generated_at: "now", data: proposalDetail }),
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await expect(fetchProposalDetail("proposal/detail id")).resolves.toEqual(proposalDetail);
|
|
expect(fetchMock.mock.calls[0][0]).toBe("http://127.0.0.1:8765/proposals/proposal%2Fdetail%20id");
|
|
const init = fetchMock.mock.calls[0][1] as RequestInit;
|
|
expect(init.method).toBeUndefined();
|
|
});
|
|
});
|