import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import { InstancesProvider, useInstances } from "@/contexts/InstancesContext"; import { instancesApi } from "@/lib/api"; import type { Instance } from "@/types/instance"; import { AuthProvider } from "../AuthContext"; // Mock the API module vi.mock("@/lib/api", () => ({ instancesApi: { list: vi.fn(), create: vi.fn(), update: vi.fn(), start: vi.fn(), stop: vi.fn(), restart: vi.fn(), delete: vi.fn(), }, })); // Test component to access context function TestComponent() { const { instances, loading, error, createInstance, updateInstance, startInstance, stopInstance, restartInstance, deleteInstance, clearError, } = useInstances(); return (
{loading.toString()}
{error || "no-error"}
{instances.length}
{instances.map((instance) => (
{instance.name}:{instance.running.toString()}
))} {/* Action buttons for testing with specific instances */}
); } function renderWithProvider(children: ReactNode) { return render( {children} ); } describe("InstancesContext", () => { const mockInstances: Instance[] = [ { name: "instance1", running: true, options: { model: "model1.gguf" } }, { name: "instance2", running: false, options: { model: "model2.gguf" } }, ]; beforeEach(() => { vi.clearAllMocks(); window.sessionStorage.setItem('llamactl_management_key', 'test-api-key-123'); global.fetch = vi.fn(() => Promise.resolve(new Response(null, { status: 200 }))); // Default successful API responses vi.mocked(instancesApi.list).mockResolvedValue(mockInstances); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Initial Loading", () => { it("loads instances on mount", async () => { renderWithProvider(); // Should start loading expect(screen.getByTestId("loading")).toHaveTextContent("true"); // Should fetch instances await waitFor(() => { expect(instancesApi.list).toHaveBeenCalledOnce(); }); // Should display loaded instances await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); expect(screen.getByTestId("instance-instance1")).toHaveTextContent( "instance1:true" ); expect(screen.getByTestId("instance-instance2")).toHaveTextContent( "instance2:false" ); }); }); it("handles API error during initial load", async () => { const errorMessage = "Network error"; vi.mocked(instancesApi.list).mockRejectedValue(new Error(errorMessage)); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("error")).toHaveTextContent(errorMessage); expect(screen.getByTestId("instances-count")).toHaveTextContent("0"); }); }); }); describe("Create Instance", () => { it("creates instance and adds it to state", async () => { const newInstance: Instance = { name: "new-instance", running: false, options: { model: "test.gguf" }, }; vi.mocked(instancesApi.create).mockResolvedValue(newInstance); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); }); screen.getByTestId("create-instance").click(); await waitFor(() => { expect(instancesApi.create).toHaveBeenCalledWith("new-instance", { model: "test.gguf", }); }); await waitFor(() => { expect(screen.getByTestId("instances-count")).toHaveTextContent("3"); expect(screen.getByTestId("instance-new-instance")).toHaveTextContent( "new-instance:false" ); }); }); it("handles create instance error without changing state", async () => { const errorMessage = "Instance already exists"; vi.mocked(instancesApi.create).mockRejectedValue(new Error(errorMessage)); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); }); screen.getByTestId("create-instance").click(); await waitFor(() => { expect(screen.getByTestId("error")).toHaveTextContent(errorMessage); }); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); expect( screen.queryByTestId("instance-new-instance") ).not.toBeInTheDocument(); }); }); describe("Update Instance", () => { it("updates instance and maintains it in state", async () => { const updatedInstance: Instance = { name: "instance1", running: true, options: { model: "updated.gguf" }, }; vi.mocked(instancesApi.update).mockResolvedValue(updatedInstance); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); }); screen.getByTestId("update-instance").click(); await waitFor(() => { expect(instancesApi.update).toHaveBeenCalledWith("instance1", { model: "updated.gguf", }); }); await waitFor(() => { expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); expect(screen.getByTestId("instance-instance1")).toBeInTheDocument(); }); }); }); describe("Start/Stop Instance", () => { it("starts existing instance and updates its running state", async () => { vi.mocked(instancesApi.start).mockResolvedValue({} as Instance); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); // instance2 starts as not running expect(screen.getByTestId("instance-instance2")).toHaveTextContent( "instance2:false" ); }); // Start instance2 (button already configured to start instance2) screen.getByTestId("start-instance").click(); await waitFor(() => { expect(instancesApi.start).toHaveBeenCalledWith("instance2"); // The running state should be updated to true expect(screen.getByTestId("instance-instance2")).toHaveTextContent( "instance2:true" ); }); }); it("stops instance and updates running state to false", async () => { vi.mocked(instancesApi.stop).mockResolvedValue({} as Instance); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); // instance1 starts as running expect(screen.getByTestId("instance-instance1")).toHaveTextContent( "instance1:true" ); }); // Stop instance1 (button already configured to stop instance1) screen.getByTestId("stop-instance").click(); await waitFor(() => { expect(instancesApi.stop).toHaveBeenCalledWith("instance1"); // The running state should be updated to false expect(screen.getByTestId("instance-instance1")).toHaveTextContent( "instance1:false" ); }); }); it("handles start instance error", async () => { const errorMessage = "Failed to start instance"; vi.mocked(instancesApi.start).mockRejectedValue(new Error(errorMessage)); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); }); screen.getByTestId("start-instance").click(); await waitFor(() => { expect(screen.getByTestId("error")).toHaveTextContent(errorMessage); }); }); }); describe("Delete Instance", () => { it("deletes instance and removes it from state", async () => { vi.mocked(instancesApi.delete).mockResolvedValue(undefined); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); expect(screen.getByTestId("instance-instance2")).toBeInTheDocument(); }); screen.getByTestId("delete-instance").click(); await waitFor(() => { expect(instancesApi.delete).toHaveBeenCalledWith("instance2"); }); await waitFor(() => { expect(screen.getByTestId("instances-count")).toHaveTextContent("1"); expect( screen.queryByTestId("instance-instance2") ).not.toBeInTheDocument(); expect(screen.getByTestId("instance-instance1")).toBeInTheDocument(); // instance1 should still exist }); }); it("handles delete instance error without changing state", async () => { const errorMessage = "Instance is running"; vi.mocked(instancesApi.delete).mockRejectedValue(new Error(errorMessage)); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); }); screen.getByTestId("delete-instance").click(); await waitFor(() => { expect(screen.getByTestId("error")).toHaveTextContent(errorMessage); }); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); expect(screen.getByTestId("instance-instance2")).toBeInTheDocument(); }); }); describe("Error Management", () => { it("clears error when clearError is called", async () => { const errorMessage = "Test error"; vi.mocked(instancesApi.list).mockRejectedValue(new Error(errorMessage)); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("error")).toHaveTextContent(errorMessage); }); screen.getByTestId("clear-error").click(); await waitFor(() => { expect(screen.getByTestId("error")).toHaveTextContent("no-error"); }); }); }); describe("State Consistency", () => { it("maintains consistent state during multiple operations", async () => { // Test that operations don't interfere with each other const newInstance: Instance = { name: "new-instance", running: false, options: {}, }; vi.mocked(instancesApi.create).mockResolvedValue(newInstance); vi.mocked(instancesApi.start).mockResolvedValue({} as Instance); renderWithProvider(); await waitFor(() => { expect(screen.getByTestId("loading")).toHaveTextContent("false"); expect(screen.getByTestId("instances-count")).toHaveTextContent("2"); }); // Create new instance screen.getByTestId("create-instance").click(); await waitFor(() => { expect(screen.getByTestId("instances-count")).toHaveTextContent("3"); }); // Start an instance (this should not affect the count) screen.getByTestId("start-instance").click(); await waitFor(() => { expect(instancesApi.start).toHaveBeenCalled(); expect(screen.getByTestId("instances-count")).toHaveTextContent("3"); // Still 3 // But the running state should change expect(screen.getByTestId("instance-instance2")).toHaveTextContent( "instance2:true" ); }); }); }); });