import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { createMockGateways } from "@/adapters/mock"; import { DIProvider } from "@/app/di"; import { ProjectSettingsPanel, PROJECT_THEME_CHANGED_EVENT } from "./ProjectSettingsPanel"; import { accessibleProjectAccent, publishProjectThemeChanged, validThemeColor, } from "./projectTheme"; function deferred() { let resolve!: (value: T) => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } describe("ProjectSettingsPanel", () => { it("persists a project colour, emits the scoped refresh, and resets to fallback", async () => { const gateways = createMockGateways(); const project = await gateways.project.createProject("Alpha", "/alpha"); const update = vi.spyOn(gateways.project, "updateProjectSettings"); const changed = vi.fn(); window.addEventListener(PROJECT_THEME_CHANGED_EVENT, changed); render( , ); const picker = (await screen.findByLabelText("Couleur du projet")) as HTMLInputElement; await waitFor(() => expect(picker.disabled).toBe(false)); fireEvent.change(picker, { target: { value: "#aa3366" } }); await waitFor(() => expect(update).toHaveBeenCalledWith(project.id, { themeColor: "#AA3366" })); expect(changed).toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: "Rétablir la couleur par défaut" })); await waitFor(() => expect(update).toHaveBeenLastCalledWith(project.id, { themeColor: null })); expect(screen.getByText("Contexte du projet")).toBeTruthy(); window.removeEventListener(PROJECT_THEME_CHANGED_EVENT, changed); }); it("clears a transient load error and retries the current project", async () => { const gateways = createMockGateways(); const project = await gateways.project.createProject("Alpha", "/alpha"); const read = vi .spyOn(gateways.project, "readProjectSettings") .mockRejectedValueOnce(new Error("temporary")) .mockResolvedValue({ themeColor: "#22AA66" }); render( , ); const alert = await screen.findByRole("alert"); expect(alert.textContent).toContain("Impossible de charger"); fireEvent.click(screen.getByRole("button", { name: "Réessayer" })); await waitFor(() => expect(read).toHaveBeenCalledTimes(2)); await waitFor(() => expect(screen.queryByRole("alert")).toBeNull()); expect((screen.getByLabelText("Couleur du projet") as HTMLInputElement).value).toBe( "#22aa66", ); }); it("keeps sibling settings surfaces in sync without persisting the blue fallback", async () => { const gateways = createMockGateways(); const project = await gateways.project.createProject("Alpha", "/alpha"); const update = vi.spyOn(gateways.project, "updateProjectSettings"); render( , ); const pickers = (await screen.findAllByLabelText( "Couleur du projet", )) as HTMLInputElement[]; await waitFor(() => expect(pickers.every((picker) => !picker.disabled)).toBe(true), ); expect(pickers.map((picker) => picker.value)).toEqual(["#4c8dff", "#4c8dff"]); expect(update).not.toHaveBeenCalled(); fireEvent.change(pickers[0], { target: { value: "#aa3366" } }); await waitFor(() => expect(pickers[1].value).toBe("#aa3366")); }); it("does not let a delayed read replace a newer cross-window colour", async () => { const gateways = createMockGateways(); const project = await gateways.project.createProject("Alpha", "/alpha"); const pendingRead = deferred<{ themeColor: string | null }>(); vi.spyOn(gateways.project, "readProjectSettings").mockReturnValue( pendingRead.promise, ); render( , ); publishProjectThemeChanged({ projectId: project.id, themeColor: "#22AA66" }); pendingRead.resolve({ themeColor: "#AA3366" }); await waitFor(() => expect((screen.getByLabelText("Couleur du projet") as HTMLInputElement).value).toBe( "#22aa66", ), ); }); it("does not apply a delayed save response to the newly edited project", async () => { const gateways = createMockGateways(); const alpha = await gateways.project.createProject("Alpha", "/alpha"); const beta = await gateways.project.createProject("Beta", "/beta"); await gateways.project.updateProjectSettings(beta.id, { themeColor: "#22AA66" }); const pendingSave = deferred<{ themeColor: string | null }>(); vi.spyOn(gateways.project, "updateProjectSettings").mockImplementation( async (projectId, settings) => projectId === alpha.id ? pendingSave.promise : settings, ); const view = render( , ); const picker = (await screen.findByLabelText("Couleur du projet")) as HTMLInputElement; await waitFor(() => expect(picker.disabled).toBe(false)); fireEvent.change(picker, { target: { value: "#aa3366" } }); view.rerender( , ); await waitFor(() => expect(picker.value).toBe("#22aa66")); pendingSave.resolve({ themeColor: "#AA3366" }); await waitFor(() => expect(picker.value).toBe("#22aa66")); }); it("keeps strict colours and raises dark accents to readable contrast", () => { expect(validThemeColor("#12abEF")).toBe("#12ABEF"); expect(validThemeColor("red")).toBeNull(); expect(accessibleProjectAccent("#000000")).not.toBe("#000000"); }); });