Cause: update_project_settings remplaçait tout ProjectSettings avec la seule couleur fournie — champ absent et null indistinguables — et des réponses de chargement/sauvegarde obsolètes d'un projet précédent pouvaient réafficher/écraser la couleur lors d'un changement de projet rapide, d'où l'impression que la couleur ne prenait pas sur tous les projets. - DTO tri-état (absent = inchangé, null = défaut produit) avec Deserialize manuel et use case de mise à jour partielle du champ theme_color; commandes Tauri et web-server alignées sur le même contrat. - Garde-fous de course frontend: séquences load/save + révision thème par projet (pub/sub onProjectThemeChanged) dans ProjectSettingsPanel, garde de révision de focus dans ViewWindow. - Tests: infrastructure settings, contrats web-server (inchangé/reset), ProjectSettingsPanel, ViewWindow, projects. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
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<T>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectSettingsPanel projectId={project.id} />
|
|
</DIProvider>,
|
|
);
|
|
|
|
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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectSettingsPanel projectId={project.id} />
|
|
</DIProvider>,
|
|
);
|
|
|
|
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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectSettingsPanel projectId={project.id} />
|
|
<ProjectSettingsPanel projectId={project.id} />
|
|
</DIProvider>,
|
|
);
|
|
|
|
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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectSettingsPanel projectId={project.id} />
|
|
</DIProvider>,
|
|
);
|
|
|
|
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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectSettingsPanel projectId={alpha.id} />
|
|
</DIProvider>,
|
|
);
|
|
|
|
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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectSettingsPanel projectId={beta.id} />
|
|
</DIProvider>,
|
|
);
|
|
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");
|
|
});
|
|
});
|