Files
Felis/panel/src/pages/admin/ImageBuildPage.test.tsx
T

101 lines
3.8 KiB
TypeScript

// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import i18next from "i18next";
import { ImageBuildPage } from "./ImageBuildPage";
import type { Build } from "@/lib/types";
const calls = vi.hoisted(() => ({ getBuild: vi.fn(), cancelBuild: vi.fn(), listSubmissions: vi.fn() }));
vi.mock("@/lib/tier", () => ({
useTier: () => ({ loading: false, identity: { user_id: "owner-1", role: "owner" }, isAdmin: true, isOwner: true }),
}));
vi.mock("@/lib/config", () => ({ loadConfig: () => Promise.resolve({}) }));
vi.mock("@/lib/api", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/api")>();
return { ...actual, api: { ...actual.api, ...calls } };
});
const t = (key: string) => i18next.t(key);
const BUILD: Build = {
id: "b-1",
image_ref: "registry.felis.svc:5000/modpack:7",
status: "building",
requested_by: "owner-1",
created_at: new Date().toISOString(),
};
// Node's own (unconfigured) localStorage shadows jsdom's, so give the page one.
function memoryStorage(): Storage {
const m = new Map<string, string>();
return {
get length() {
return m.size;
},
key: (i) => [...m.keys()][i] ?? null,
getItem: (k) => m.get(k) ?? null,
setItem: (k, v) => void m.set(k, String(v)),
removeItem: (k) => void m.delete(k),
clear: () => m.clear(),
};
}
beforeEach(() => {
for (const fn of Object.values(calls)) fn.mockReset();
vi.stubGlobal("localStorage", memoryStorage());
localStorage.setItem("felis_triggered_builds", JSON.stringify([BUILD.id]));
calls.getBuild.mockResolvedValue(BUILD);
vi.spyOn(window, "confirm").mockImplementation(() => {
throw new Error("window.confirm used");
});
vi.spyOn(window, "alert").mockImplementation(() => {
throw new Error("window.alert used");
});
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
async function openCancel() {
render(
<MemoryRouter>
<ImageBuildPage />
</MemoryRouter>,
);
await userEvent.click(await screen.findByRole("button", { name: t("admin:cancel_build_btn") }));
return screen.getByRole("dialog", { name: t("admin:cancel_build_title") });
}
describe("ImageBuildPage cancel", () => {
it("asks in a dialog that names the image, then stops the build", async () => {
calls.cancelBuild.mockResolvedValue(undefined);
const dialog = await openCancel();
expect(within(dialog).getByText(BUILD.image_ref)).toBeTruthy();
expect(calls.cancelBuild).not.toHaveBeenCalled();
calls.getBuild.mockResolvedValue({ ...BUILD, status: "cancelled" });
await userEvent.click(within(dialog).getByRole("button", { name: t("admin:cancel_build_confirm") }));
expect(calls.cancelBuild).toHaveBeenCalledWith(BUILD.id);
await vi.waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
await vi.waitFor(() =>
expect(screen.queryByRole("button", { name: t("admin:cancel_build_btn") })).toBeNull(),
);
});
it("keeps building when the dialog is dismissed", async () => {
const dialog = await openCancel();
await userEvent.click(within(dialog).getByRole("button", { name: t("admin:cancel_build_keep") }));
expect(screen.queryByRole("dialog")).toBeNull();
expect(calls.cancelBuild).not.toHaveBeenCalled();
});
it("shows a refused cancel in the dialog", async () => {
calls.cancelBuild.mockRejectedValue({ status: 409, code: "test", message: "build already finished" });
const dialog = await openCancel();
await userEvent.click(within(dialog).getByRole("button", { name: t("admin:cancel_build_confirm") }));
expect((await within(dialog).findByRole("alert")).textContent).toBe("build already finished");
});
});