// ProfileFormModal — deleting a secondary profile waits for the confirmation (#421). // // Before #421 the dialog plugin's async `window.confirm` returned a Promise, always truthy: // the profile and its database were deleted before the user answered, and "Cancel" stopped // nothing. The modal runs as a plain function under the shared hook harness (#409); the test // clicks the delete button of the secondary profile through its props, the confirmation // answered by a mocked `confirmAction`. import { describe, it, expect, vi, beforeEach } from "vitest"; import { isValidElement, type ReactElement, type ReactNode } from "react"; vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig())); vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock()); const mocks = vi.hoisted(() => ({ confirmAction: vi.fn<(message: string) => Promise>(), deleteProfile: vi.fn(async () => undefined), })); vi.mock("../../services/confirmService", () => ({ confirmAction: mocks.confirmAction })); vi.mock("../../hooks/useEntitlement", () => ({ useEntitlement: () => ({ allowed: true, ready: true }), })); vi.mock("../../contexts/ProfileContext", () => ({ useProfile: () => ({ profiles: [ { id: "default", name: "Principal", color: "#4A90A4", pin_hash: null, db_filename: "simpl_resultat.db", created_at: "" }, { id: "second", name: "Second", color: "#C17767", pin_hash: null, db_filename: "profile_second.db", created_at: "" }, ], createProfile: vi.fn(), updateProfile: vi.fn(), deleteProfile: mocks.deleteProfile, setPin: vi.fn(), }), })); import { Trash2 } from "lucide-react"; import { hookHarness } from "../../test/hookHarness"; import ProfileFormModal from "./ProfileFormModal"; const harness = hookHarness(); type Props = Record & { children?: ReactNode; onClick?: () => Promise }; /** The elements of the tree whose children hold an element of the given type. */ function holding(node: ReactNode, type: unknown): ReactElement[] { if (Array.isArray(node)) return node.flatMap((n) => holding(n, type)); if (!isValidElement(node)) return []; const el = node as ReactElement; const children = ([] as ReactNode[]).concat(el.props.children ?? []); const holds = children.some((c) => isValidElement(c) && c.type === type); return [...(holds ? [el] : []), ...holding(el.props.children, type)]; } /** Clicks the only delete button, the secondary profile's, and waits for its handler. */ async function clickDelete() { const tree = harness.render(() => ProfileFormModal({ onClose: vi.fn() })); const buttons = holding(tree, Trash2); expect(buttons).toHaveLength(1); await buttons[0].props.onClick?.(); } beforeEach(() => { harness.clear(); mocks.confirmAction.mockReset(); mocks.deleteProfile.mockClear(); }); describe("ProfileFormModal deletion (#421)", () => { it("keeps the profile when the confirmation is refused", async () => { mocks.confirmAction.mockResolvedValue(false); await clickDelete(); expect(mocks.confirmAction).toHaveBeenCalledWith("t(profile.deleteConfirm)"); expect(mocks.deleteProfile).not.toHaveBeenCalled(); }); it("deletes the profile once confirmed", async () => { mocks.confirmAction.mockResolvedValue(true); await clickDelete(); expect(mocks.deleteProfile).toHaveBeenCalledWith("second"); }); });