On desktop, tauri-plugin-dialog 2.6.0 replaces window.confirm with an async function: confirm(...) returned a Promise, always truthy, so every guard `if (!confirm(...)) return;` let the action run before the user answered. Deleting a secondary profile removed its database even on "Cancel". - confirmAction() (src/services/confirmService.ts) calls the plugin's ask() with a warning icon and buttons labelled in the app language (common.confirm / common.cancel); a failed call resolves false and is logged (fail closed). @tauri-apps/plugin-dialog 2.6.0 added, the crate's version. - The six sites await it: profile, category, adjustment, budget template and Balance category deletions, and both confirmations of the license key removal (performRemoveLicense takes an async confirm). - native-dialog-guard.test.ts fails on any native confirm/alert in src/ and on any confirmation not awaited (TypeScript does not flag `!promise`). - Tests: confirmService, the deletion of a profile in ProfileFormModal, and the license removal tests with an async confirm (test: reproduces #421: six of them were red on the old code). Refs #421 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
// 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<boolean>>(),
|
|
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<string, unknown> & { children?: ReactNode; onClick?: () => Promise<void> };
|
|
|
|
/** The elements of the tree whose children hold an element of the given type. */
|
|
function holding(node: ReactNode, type: unknown): ReactElement<Props>[] {
|
|
if (Array.isArray(node)) return node.flatMap((n) => holding(n, type));
|
|
if (!isValidElement(node)) return [];
|
|
const el = node as ReactElement<Props>;
|
|
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");
|
|
});
|
|
});
|