fix(frontend,application,backend,web-server,infrastructure): le set de couleurs projet s'applique à tous les projets — #316 (QA verte)

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>
This commit is contained in:
2026-09-11 13:01:05 +02:00
parent 89937c7cd9
commit bbe8c1d04b
10 changed files with 306 additions and 27 deletions

View File

@ -4,7 +4,19 @@ 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, validThemeColor } from "./projectTheme";
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 () => {
@ -55,6 +67,83 @@ describe("ProjectSettingsPanel", () => {
);
});
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();