aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/components/DeleteNoteTypeModal.test.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-02 11:58:13 +0900
committernsfisis <nsfisis@gmail.com>2026-05-02 11:58:13 +0900
commitd47d1a014a71ae65cbbf1b384eed87c6fe078b07 (patch)
tree76c3af22e10f963104f84f3d680c5dddfc8b2be6 /src/client/components/DeleteNoteTypeModal.test.tsx
parent023d0fcfce575030ee503c5f60df8c28dba7ab07 (diff)
downloadkioku-d47d1a014a71ae65cbbf1b384eed87c6fe078b07.tar.gz
kioku-d47d1a014a71ae65cbbf1b384eed87c6fe078b07.tar.zst
kioku-d47d1a014a71ae65cbbf1b384eed87c6fe078b07.zip
feat(note-types): make note type CRUD work fully offline-firstHEADmain
CreateNoteTypeModal, DeleteNoteTypeModal, and the NoteTypeEditor (which covers field add/edit/delete/reorder) now write through the local IndexedDB repositories and fire-and-forget syncActionAtom, mirroring the deck-CRUD pattern. The dead EditNoteTypeModal — never imported — is removed. The local hasNotes / hasNoteFieldValues guards mirror the server's delete-time checks so a note type with attached notes, or a field with saved values, can't be silently soft-deleted offline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/client/components/DeleteNoteTypeModal.test.tsx')
-rw-r--r--src/client/components/DeleteNoteTypeModal.test.tsx131
1 files changed, 41 insertions, 90 deletions
diff --git a/src/client/components/DeleteNoteTypeModal.test.tsx b/src/client/components/DeleteNoteTypeModal.test.tsx
index c73fbe0..ec67a82 100644
--- a/src/client/components/DeleteNoteTypeModal.test.tsx
+++ b/src/client/components/DeleteNoteTypeModal.test.tsx
@@ -3,38 +3,24 @@
*/
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import { atom } from "jotai";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mockDelete = vi.fn();
-const mockHandleResponse = vi.fn();
+const mockHasNotes = vi.fn();
+const mockTriggerSync = vi.fn(() => Promise.resolve(null));
-vi.mock("../api/client", () => ({
- apiClient: {
- rpc: {
- api: {
- "note-types": {
- ":id": {
- $delete: (args: unknown) => mockDelete(args),
- },
- },
- },
- },
- handleResponse: (res: unknown) => mockHandleResponse(res),
- },
- ApiClientError: class ApiClientError extends Error {
- constructor(
- message: string,
- public status: number,
- public code?: string,
- ) {
- super(message);
- this.name = "ApiClientError";
- }
+vi.mock("../db/repositories", () => ({
+ localNoteTypeRepository: {
+ delete: (...args: unknown[]) => mockDelete(...args),
+ hasNotes: (...args: unknown[]) => mockHasNotes(...args),
},
}));
-import { ApiClientError } from "../api/client";
-// Import after mock is set up
+vi.mock("../atoms", () => ({
+ syncActionAtom: atom(null, () => mockTriggerSync()),
+}));
+
import { DeleteNoteTypeModal } from "./DeleteNoteTypeModal";
describe("DeleteNoteTypeModal", () => {
@@ -52,8 +38,8 @@ describe("DeleteNoteTypeModal", () => {
beforeEach(() => {
vi.clearAllMocks();
- mockDelete.mockResolvedValue({ ok: true });
- mockHandleResponse.mockResolvedValue({});
+ mockDelete.mockResolvedValue(true);
+ mockHasNotes.mockResolvedValue(false);
});
afterEach(() => {
@@ -109,28 +95,7 @@ describe("DeleteNoteTypeModal", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
- it("calls onClose when clicking outside the modal", async () => {
- const user = userEvent.setup();
- const onClose = vi.fn();
- render(<DeleteNoteTypeModal {...defaultProps} onClose={onClose} />);
-
- const dialog = screen.getByRole("dialog");
- await user.click(dialog);
-
- expect(onClose).toHaveBeenCalledTimes(1);
- });
-
- it("does not call onClose when clicking inside the modal content", async () => {
- const user = userEvent.setup();
- const onClose = vi.fn();
- render(<DeleteNoteTypeModal {...defaultProps} onClose={onClose} />);
-
- await user.click(screen.getByText("Basic"));
-
- expect(onClose).not.toHaveBeenCalled();
- });
-
- it("deletes noteType when Delete is clicked", async () => {
+ it("deletes noteType via local repository when Delete is clicked", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
const onNoteTypeDeleted = vi.fn();
@@ -147,41 +112,29 @@ describe("DeleteNoteTypeModal", () => {
await user.click(screen.getByRole("button", { name: "Delete" }));
await waitFor(() => {
- expect(mockDelete).toHaveBeenCalledWith({
- param: { id: "note-type-123" },
- });
+ expect(mockDelete).toHaveBeenCalledWith("note-type-123");
});
expect(onNoteTypeDeleted).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledTimes(1);
});
- it("shows loading state during deletion", async () => {
+ it("triggers a background sync after a successful delete", async () => {
const user = userEvent.setup();
- mockDelete.mockImplementation(() => new Promise(() => {})); // Never resolves
-
render(<DeleteNoteTypeModal {...defaultProps} />);
await user.click(screen.getByRole("button", { name: "Delete" }));
- expect(screen.getByRole("button", { name: "Deleting..." })).toBeDefined();
- expect(screen.getByRole("button", { name: "Deleting..." })).toHaveProperty(
- "disabled",
- true,
- );
- expect(screen.getByRole("button", { name: "Cancel" })).toHaveProperty(
- "disabled",
- true,
- );
+ await waitFor(() => {
+ expect(mockTriggerSync).toHaveBeenCalled();
+ });
});
- it("displays API error message", async () => {
+ it("blocks deletion when notes still reference the type", async () => {
const user = userEvent.setup();
- mockHandleResponse.mockRejectedValue(
- new ApiClientError("Note type not found", 404),
- );
+ mockHasNotes.mockResolvedValue(true);
render(<DeleteNoteTypeModal {...defaultProps} />);
@@ -189,33 +142,36 @@ describe("DeleteNoteTypeModal", () => {
await waitFor(() => {
expect(screen.getByRole("alert").textContent).toContain(
- "Note type not found",
+ "Cannot delete note type with existing notes",
);
});
+ expect(mockDelete).not.toHaveBeenCalled();
});
- it("displays conflict error when notes exist", async () => {
+ it("shows loading state during deletion", async () => {
const user = userEvent.setup();
- mockHandleResponse.mockRejectedValue(
- new ApiClientError("Cannot delete note type with existing notes", 409),
- );
+ mockDelete.mockImplementation(() => new Promise(() => {}));
render(<DeleteNoteTypeModal {...defaultProps} />);
await user.click(screen.getByRole("button", { name: "Delete" }));
- await waitFor(() => {
- expect(screen.getByRole("alert").textContent).toContain(
- "Cannot delete note type with existing notes",
- );
- });
+ expect(screen.getByRole("button", { name: "Deleting..." })).toBeDefined();
+ expect(screen.getByRole("button", { name: "Deleting..." })).toHaveProperty(
+ "disabled",
+ true,
+ );
+ expect(screen.getByRole("button", { name: "Cancel" })).toHaveProperty(
+ "disabled",
+ true,
+ );
});
- it("displays generic error on unexpected failure", async () => {
+ it("shows an error when the note type no longer exists locally", async () => {
const user = userEvent.setup();
- mockDelete.mockRejectedValue(new Error("Network error"));
+ mockDelete.mockResolvedValue(false);
render(<DeleteNoteTypeModal {...defaultProps} />);
@@ -223,17 +179,15 @@ describe("DeleteNoteTypeModal", () => {
await waitFor(() => {
expect(screen.getByRole("alert").textContent).toContain(
- "Failed to delete note type. Please try again.",
+ "Note type not found.",
);
});
});
- it("displays error when handleResponse throws", async () => {
+ it("displays a generic error when the local write fails", async () => {
const user = userEvent.setup();
- mockHandleResponse.mockRejectedValue(
- new ApiClientError("Not authenticated", 401),
- );
+ mockDelete.mockRejectedValue(new Error("disk full"));
render(<DeleteNoteTypeModal {...defaultProps} />);
@@ -241,7 +195,7 @@ describe("DeleteNoteTypeModal", () => {
await waitFor(() => {
expect(screen.getByRole("alert").textContent).toContain(
- "Not authenticated",
+ "Failed to delete note type. Please try again.",
);
});
});
@@ -250,23 +204,20 @@ describe("DeleteNoteTypeModal", () => {
const user = userEvent.setup();
const onClose = vi.fn();
- mockHandleResponse.mockRejectedValue(new ApiClientError("Some error", 404));
+ mockDelete.mockRejectedValueOnce(new Error("Some error"));
const { rerender } = render(
<DeleteNoteTypeModal {...defaultProps} onClose={onClose} />,
);
- // Trigger error
await user.click(screen.getByRole("button", { name: "Delete" }));
await waitFor(() => {
expect(screen.getByRole("alert")).toBeDefined();
});
- // Close and reopen the modal
await user.click(screen.getByRole("button", { name: "Cancel" }));
rerender(<DeleteNoteTypeModal {...defaultProps} onClose={onClose} />);
- // Error should be cleared
expect(screen.queryByRole("alert")).toBeNull();
});