267 lines
9.4 KiB
TypeScript
267 lines
9.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import { useMemoryBackendStatus } from "../useMemoryBackendStatus";
|
|
import * as api from "../../api";
|
|
|
|
describe("useMemoryBackendStatus", () => {
|
|
const mockFetchMemoryBackendStatus = vi.spyOn(api, "fetchMemoryBackendStatus");
|
|
|
|
const mockFileBackendStatus: api.MemoryBackendStatus = {
|
|
currentBackend: "file",
|
|
capabilities: {
|
|
readable: true,
|
|
writable: true,
|
|
supportsAtomicWrite: true,
|
|
hasConflictResolution: false,
|
|
persistent: true,
|
|
},
|
|
availableBackends: ["file", "readonly", "qmd"],
|
|
};
|
|
|
|
const mockReadonlyBackendStatus: api.MemoryBackendStatus = {
|
|
currentBackend: "readonly",
|
|
capabilities: {
|
|
readable: true,
|
|
writable: false,
|
|
supportsAtomicWrite: false,
|
|
hasConflictResolution: false,
|
|
persistent: false,
|
|
},
|
|
availableBackends: ["file", "readonly", "qmd"],
|
|
};
|
|
|
|
const mockQmdBackendStatus: api.MemoryBackendStatus = {
|
|
currentBackend: "qmd",
|
|
capabilities: {
|
|
readable: true,
|
|
writable: true,
|
|
supportsAtomicWrite: false,
|
|
hasConflictResolution: false,
|
|
persistent: true,
|
|
},
|
|
availableBackends: ["file", "readonly", "qmd"],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockFetchMemoryBackendStatus.mockClear();
|
|
});
|
|
|
|
it("fetches status on initial mount", async () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
expect(result.current.loading).toBe(true);
|
|
expect(result.current.status).toBeNull();
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.status).toEqual(mockFileBackendStatus);
|
|
expect(result.current.currentBackend).toBe("file");
|
|
expect(result.current.capabilities).toEqual(mockFileBackendStatus.capabilities);
|
|
expect(result.current.availableBackends).toEqual(["file", "readonly", "qmd"]);
|
|
expect(result.current.error).toBeNull();
|
|
expect(result.current.lastUpdated).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("does not fetch when enabled is false", () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
|
|
|
|
const { result } = renderHook(() =>
|
|
useMemoryBackendStatus({ autoRefresh: false, enabled: false }),
|
|
);
|
|
|
|
expect(mockFetchMemoryBackendStatus).not.toHaveBeenCalled();
|
|
expect(result.current.loading).toBe(false);
|
|
expect(result.current.status).toBeNull();
|
|
});
|
|
|
|
it("fetches when enabled toggles from false to true", async () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ enabled }) => useMemoryBackendStatus({ autoRefresh: false, enabled }),
|
|
{ initialProps: { enabled: false } },
|
|
);
|
|
|
|
expect(mockFetchMemoryBackendStatus).not.toHaveBeenCalled();
|
|
expect(result.current.loading).toBe(false);
|
|
|
|
rerender({ enabled: true });
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(mockFetchMemoryBackendStatus).toHaveBeenCalledTimes(1);
|
|
expect(result.current.status).toEqual(mockFileBackendStatus);
|
|
});
|
|
|
|
it("handles fetch errors", async () => {
|
|
mockFetchMemoryBackendStatus.mockRejectedValue(new Error("Failed to connect"));
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.error).toBe("Failed to connect");
|
|
expect(result.current.status).toBeNull();
|
|
});
|
|
|
|
it("provides correct capability flags for file backend", async () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.isReadable).toBe(true);
|
|
expect(result.current.isWritable).toBe(true);
|
|
expect(result.current.supportsAtomicWrite).toBe(true);
|
|
});
|
|
|
|
it("provides correct capability flags for readonly backend", async () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockReadonlyBackendStatus);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.isReadable).toBe(true);
|
|
expect(result.current.isWritable).toBe(false);
|
|
expect(result.current.supportsAtomicWrite).toBe(false);
|
|
});
|
|
|
|
it("provides correct capability flags for qmd backend", async () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockQmdBackendStatus);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.isReadable).toBe(true);
|
|
expect(result.current.isWritable).toBe(true);
|
|
expect(result.current.supportsAtomicWrite).toBe(false);
|
|
});
|
|
|
|
it("refreshes data manually", async () => {
|
|
mockFetchMemoryBackendStatus
|
|
.mockResolvedValueOnce(mockFileBackendStatus)
|
|
.mockResolvedValueOnce(mockQmdBackendStatus);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(result.current.currentBackend).toBe("file");
|
|
|
|
// Manual refresh
|
|
await result.current.refresh();
|
|
|
|
await waitFor(() => expect(result.current.currentBackend).toBe("qmd"));
|
|
});
|
|
|
|
it("treats backend status as unknown while a refresh is in flight", async () => {
|
|
let resolveRefresh: ((value: api.MemoryBackendStatus) => void) | null = null;
|
|
|
|
mockFetchMemoryBackendStatus
|
|
.mockResolvedValueOnce(mockReadonlyBackendStatus)
|
|
.mockImplementationOnce(
|
|
() => new Promise<api.MemoryBackendStatus>((resolve) => {
|
|
resolveRefresh = resolve;
|
|
}),
|
|
);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(result.current.isWritable).toBe(false);
|
|
expect(result.current.status).toEqual(mockReadonlyBackendStatus);
|
|
|
|
act(() => {
|
|
void result.current.refresh();
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(true));
|
|
expect(result.current.status).toBeNull();
|
|
expect(result.current.capabilities).toBeNull();
|
|
expect(result.current.isWritable).toBe(false);
|
|
|
|
act(() => {
|
|
resolveRefresh?.(mockFileBackendStatus);
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(result.current.status).toEqual(mockFileBackendStatus);
|
|
expect(result.current.isWritable).toBe(true);
|
|
});
|
|
|
|
it("clears error on successful refresh after error", async () => {
|
|
mockFetchMemoryBackendStatus
|
|
.mockRejectedValueOnce(new Error("Network error"))
|
|
.mockResolvedValueOnce(mockFileBackendStatus);
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(result.current.error).toBe("Network error");
|
|
|
|
// Manual refresh
|
|
await result.current.refresh();
|
|
|
|
await waitFor(() => expect(result.current.error).toBeNull());
|
|
expect(result.current.status).toEqual(mockFileBackendStatus);
|
|
});
|
|
|
|
it("passes projectId to API", async () => {
|
|
mockFetchMemoryBackendStatus.mockResolvedValue(mockFileBackendStatus);
|
|
|
|
const { result } = renderHook(() =>
|
|
useMemoryBackendStatus({ projectId: "proj_abc", autoRefresh: false }),
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(mockFetchMemoryBackendStatus).toHaveBeenCalledWith("proj_abc");
|
|
});
|
|
|
|
it("returns expected default values before first fetch", () => {
|
|
mockFetchMemoryBackendStatus.mockImplementation(() => new Promise(() => {})); // Never resolves
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
expect(result.current.status).toBeNull();
|
|
expect(result.current.currentBackend).toBeNull();
|
|
expect(result.current.capabilities).toBeNull();
|
|
expect(result.current.availableBackends).toEqual([]);
|
|
expect(result.current.isReadable).toBe(false);
|
|
expect(result.current.isWritable).toBe(false);
|
|
expect(result.current.supportsAtomicWrite).toBe(false);
|
|
expect(result.current.loading).toBe(true);
|
|
expect(result.current.error).toBeNull();
|
|
expect(result.current.lastUpdated).toBeNull();
|
|
expect(typeof result.current.refresh).toBe("function");
|
|
});
|
|
|
|
it("exports the correct interface", () => {
|
|
expect(typeof useMemoryBackendStatus).toBe("function");
|
|
});
|
|
|
|
it("handles null error messages", async () => {
|
|
mockFetchMemoryBackendStatus.mockRejectedValue(new Error());
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
// Error with no message results in empty string
|
|
expect(result.current.error).toBe("");
|
|
});
|
|
|
|
it("handles non-Error rejections", async () => {
|
|
mockFetchMemoryBackendStatus.mockRejectedValue("string error");
|
|
|
|
const { result } = renderHook(() => useMemoryBackendStatus({ autoRefresh: false }));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.error).toBe("Failed to fetch memory backend status");
|
|
});
|
|
});
|