aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/components/EditNoteModal.test.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/components/EditNoteModal.test.tsx')
-rw-r--r--src/client/components/EditNoteModal.test.tsx329
1 files changed, 106 insertions, 223 deletions
diff --git a/src/client/components/EditNoteModal.test.tsx b/src/client/components/EditNoteModal.test.tsx
index 61f94bd..20c437f 100644
--- a/src/client/components/EditNoteModal.test.tsx
+++ b/src/client/components/EditNoteModal.test.tsx
@@ -4,11 +4,34 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { apiClient } from "../api/client";
+
+const mockNoteGet = vi.fn();
+const mockNotePut = vi.fn();
+const mockNoteTypeGet = vi.fn();
+const mockHandleResponse = vi.fn();
vi.mock("../api/client", () => ({
apiClient: {
- getAuthHeader: vi.fn(),
+ rpc: {
+ api: {
+ decks: {
+ ":deckId": {
+ notes: {
+ ":noteId": {
+ $get: (args: unknown) => mockNoteGet(args),
+ $put: (args: unknown) => mockNotePut(args),
+ },
+ },
+ },
+ },
+ "note-types": {
+ ":id": {
+ $get: (args: unknown) => mockNoteTypeGet(args),
+ },
+ },
+ },
+ },
+ handleResponse: (res: unknown) => mockHandleResponse(res),
},
ApiClientError: class ApiClientError extends Error {
constructor(
@@ -22,13 +45,10 @@ vi.mock("../api/client", () => ({
},
}));
+import { ApiClientError } from "../api/client";
// Import after mock is set up
import { EditNoteModal } from "./EditNoteModal";
-// Mock fetch globally
-const mockFetch = vi.fn();
-global.fetch = mockFetch;
-
describe("EditNoteModal", () => {
const defaultProps = {
isOpen: true,
@@ -72,9 +92,9 @@ describe("EditNoteModal", () => {
beforeEach(() => {
vi.clearAllMocks();
- vi.mocked(apiClient.getAuthHeader).mockReturnValue({
- Authorization: "Bearer access-token",
- });
+ mockNoteGet.mockResolvedValue({ ok: true });
+ mockNotePut.mockResolvedValue({ ok: true });
+ mockNoteTypeGet.mockResolvedValue({ ok: true });
});
afterEach(() => {
@@ -95,15 +115,9 @@ describe("EditNoteModal", () => {
});
it("renders modal when open with noteId", async () => {
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} />);
@@ -117,44 +131,29 @@ describe("EditNoteModal", () => {
});
it("fetches note and note type on open", async () => {
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} />);
await waitFor(() => {
- expect(mockFetch).toHaveBeenCalledWith(
- "/api/decks/deck-123/notes/note-456",
- {
- headers: { Authorization: "Bearer access-token" },
- },
- );
+ expect(mockNoteGet).toHaveBeenCalledWith({
+ param: { deckId: "deck-123", noteId: "note-456" },
+ });
});
await waitFor(() => {
- expect(mockFetch).toHaveBeenCalledWith("/api/note-types/note-type-1", {
- headers: { Authorization: "Bearer access-token" },
+ expect(mockNoteTypeGet).toHaveBeenCalledWith({
+ param: { id: "note-type-1" },
});
});
});
it("populates form with note field values", async () => {
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} />);
@@ -171,15 +170,9 @@ describe("EditNoteModal", () => {
});
it("displays note type name (read-only)", async () => {
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} />);
@@ -191,15 +184,9 @@ describe("EditNoteModal", () => {
it("disables save button when fields are empty", async () => {
const user = userEvent.setup();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} />);
@@ -216,15 +203,9 @@ describe("EditNoteModal", () => {
});
it("enables save button when all fields have values", async () => {
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} />);
@@ -240,15 +221,9 @@ describe("EditNoteModal", () => {
const user = userEvent.setup();
const onClose = vi.fn();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} onClose={onClose} />);
@@ -265,15 +240,9 @@ describe("EditNoteModal", () => {
const user = userEvent.setup();
const onClose = vi.fn();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
render(<EditNoteModal {...defaultProps} onClose={onClose} />);
@@ -293,19 +262,10 @@ describe("EditNoteModal", () => {
const onClose = vi.fn();
const onNoteUpdated = vi.fn();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields })
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues });
render(
<EditNoteModal
@@ -327,22 +287,15 @@ describe("EditNoteModal", () => {
await user.click(screen.getByRole("button", { name: "Save Changes" }));
await waitFor(() => {
- expect(mockFetch).toHaveBeenCalledWith(
- "/api/decks/deck-123/notes/note-456",
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- Authorization: "Bearer access-token",
+ expect(mockNotePut).toHaveBeenCalledWith({
+ param: { deckId: "deck-123", noteId: "note-456" },
+ json: {
+ fields: {
+ "field-1": "Updated front",
+ "field-2": "Existing back",
},
- body: JSON.stringify({
- fields: {
- "field-1": "Updated front",
- "field-2": "Existing back",
- },
- }),
},
- );
+ });
});
expect(onNoteUpdated).toHaveBeenCalledTimes(1);
@@ -370,19 +323,10 @@ describe("EditNoteModal", () => {
],
};
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: noteWithWhitespace }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: noteWithWhitespace }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: noteWithWhitespace })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields })
+ .mockResolvedValueOnce({ note: noteWithWhitespace });
render(<EditNoteModal {...defaultProps} />);
@@ -393,27 +337,20 @@ describe("EditNoteModal", () => {
await user.click(screen.getByRole("button", { name: "Save Changes" }));
await waitFor(() => {
- expect(mockFetch).toHaveBeenCalledWith(
- "/api/decks/deck-123/notes/note-456",
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- Authorization: "Bearer access-token",
+ expect(mockNotePut).toHaveBeenCalledWith({
+ param: { deckId: "deck-123", noteId: "note-456" },
+ json: {
+ fields: {
+ "field-1": "Trimmed",
+ "field-2": "Value",
},
- body: JSON.stringify({
- fields: {
- "field-1": "Trimmed",
- "field-2": "Value",
- },
- }),
},
- );
+ });
});
});
it("shows loading state during fetch", async () => {
- mockFetch.mockImplementationOnce(() => new Promise(() => {})); // Never resolves
+ mockNoteGet.mockImplementationOnce(() => new Promise(() => {})); // Never resolves
render(<EditNoteModal {...defaultProps} />);
@@ -423,16 +360,11 @@ describe("EditNoteModal", () => {
it("shows loading state during submission", async () => {
const user = userEvent.setup();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- })
- .mockImplementationOnce(() => new Promise(() => {})); // Never resolves
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
+
+ mockNotePut.mockImplementationOnce(() => new Promise(() => {})); // Never resolves
render(<EditNoteModal {...defaultProps} />);
@@ -450,11 +382,9 @@ describe("EditNoteModal", () => {
});
it("displays API error message when note fetch fails", async () => {
- mockFetch.mockResolvedValueOnce({
- ok: false,
- status: 404,
- json: async () => ({ error: "Note not found" }),
- });
+ mockHandleResponse.mockRejectedValueOnce(
+ new ApiClientError("Note not found", 404),
+ );
render(<EditNoteModal {...defaultProps} />);
@@ -464,16 +394,9 @@ describe("EditNoteModal", () => {
});
it("displays API error message when note type fetch fails", async () => {
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: false,
- status: 404,
- json: async () => ({ error: "Note type not found" }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockRejectedValueOnce(new ApiClientError("Note type not found", 404));
render(<EditNoteModal {...defaultProps} />);
@@ -487,20 +410,10 @@ describe("EditNoteModal", () => {
it("displays API error message when update fails", async () => {
const user = userEvent.setup();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- })
- .mockResolvedValueOnce({
- ok: false,
- status: 400,
- json: async () => ({ error: "Failed to update note" }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields })
+ .mockRejectedValueOnce(new ApiClientError("Failed to update note", 400));
render(<EditNoteModal {...defaultProps} />);
@@ -518,7 +431,7 @@ describe("EditNoteModal", () => {
});
it("displays generic error on unexpected failure", async () => {
- mockFetch.mockRejectedValueOnce(new Error("Network error"));
+ mockNoteGet.mockRejectedValueOnce(new Error("Network error"));
render(<EditNoteModal {...defaultProps} />);
@@ -529,30 +442,12 @@ describe("EditNoteModal", () => {
});
});
- it("displays error when not authenticated", async () => {
- vi.mocked(apiClient.getAuthHeader).mockReturnValue(undefined);
-
- render(<EditNoteModal {...defaultProps} />);
-
- await waitFor(() => {
- expect(screen.getByRole("alert").textContent).toContain(
- "Not authenticated",
- );
- });
- });
-
it("resets form when modal is closed and reopened with different noteId", async () => {
const onClose = vi.fn();
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
const { rerender } = render(
<EditNoteModal
@@ -603,15 +498,9 @@ describe("EditNoteModal", () => {
],
};
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: newNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: mockNoteTypeWithFields }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: newNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: mockNoteTypeWithFields });
// Reopen with different noteId
rerender(
@@ -639,15 +528,9 @@ describe("EditNoteModal", () => {
isReversible: true,
};
- mockFetch
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ note: mockNoteWithFieldValues }),
- })
- .mockResolvedValueOnce({
- ok: true,
- json: async () => ({ noteType: reversibleNoteType }),
- });
+ mockHandleResponse
+ .mockResolvedValueOnce({ note: mockNoteWithFieldValues })
+ .mockResolvedValueOnce({ noteType: reversibleNoteType });
render(<EditNoteModal {...defaultProps} />);