From 4fd6994fb0dd9175f1eeaa8219550c21a1edcae9 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 6 Apr 2026 13:34:04 -0700 Subject: [PATCH] test(web): add prompt-client, SmartActionsPanel, RunPromptModal, NoteEditor tests (G10-G13) --- backend/src/lib/note-hooks.test.ts | 5 +- web/src/components/NoteEditor.test.tsx | 101 +++++++++++++++ web/src/components/RunPromptModal.test.tsx | 65 ++++++++++ web/src/components/SmartActionsPanel.test.tsx | 52 ++++++++ web/src/lib/prompt-client.test.ts | 122 ++++++++++++++++++ 5 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 web/src/components/NoteEditor.test.tsx create mode 100644 web/src/components/RunPromptModal.test.tsx create mode 100644 web/src/components/SmartActionsPanel.test.tsx create mode 100644 web/src/lib/prompt-client.test.ts diff --git a/backend/src/lib/note-hooks.test.ts b/backend/src/lib/note-hooks.test.ts index e7a6e86..9af6013 100644 --- a/backend/src/lib/note-hooks.test.ts +++ b/backend/src/lib/note-hooks.test.ts @@ -55,7 +55,7 @@ const mockLog = { } as unknown as import('fastify').FastifyBaseLogger; function makeNote(overrides: Partial = {}): NoteDoc { - return { + const base: NoteDoc = { id: 'note-1', productId: 'notelett', userId: 'user-1', @@ -64,12 +64,13 @@ function makeNote(overrides: Partial = {}): NoteDoc { body: 'Some body text for testing purposes.', status: 'active', tags: [], + links: [], createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', createdBy: 'user-1', updatedBy: 'user-1', - ...overrides, }; + return Object.assign(base, overrides); } describe('runPostSaveHooks', () => { diff --git a/web/src/components/NoteEditor.test.tsx b/web/src/components/NoteEditor.test.tsx new file mode 100644 index 0000000..663e86c --- /dev/null +++ b/web/src/components/NoteEditor.test.tsx @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; + +// Mock tiptap +vi.mock("@tiptap/react", () => ({ + useEditor: () => ({ + getHTML: () => "

Test body

", + commands: { setContent: vi.fn(), focus: vi.fn() }, + chain: () => ({ focus: () => ({ toggleBold: () => ({ run: vi.fn() }), toggleItalic: () => ({ run: vi.fn() }), toggleStrike: () => ({ run: vi.fn() }), toggleBulletList: () => ({ run: vi.fn() }), toggleOrderedList: () => ({ run: vi.fn() }), toggleHeading: () => ({ run: vi.fn() }), toggleBlockquote: () => ({ run: vi.fn() }), toggleCodeBlock: () => ({ run: vi.fn() }) }) }), + on: vi.fn(), + off: vi.fn(), + destroy: vi.fn(), + isDestroyed: false, + isActive: () => false, + state: { selection: { from: 0, to: 10, empty: true } }, + view: { state: { doc: { textBetween: () => "selected text" } } }, + }), + EditorContent: ({ editor }: { editor: unknown }) =>
{editor ? "editor" : "no-editor"}
, +})); + +vi.mock("@tiptap/starter-kit", () => ({ default: { configure: () => ({}) } })); +vi.mock("@tiptap/extension-placeholder", () => ({ default: { configure: () => ({}) } })); + +vi.mock("@/lib/copilot-client", () => ({ + copilotTransform: vi.fn().mockResolvedValue("Transformed text"), +})); + +vi.mock("@/lib/toast", () => ({ + toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() }, +})); + +vi.mock("@/lib/use-debounce", () => ({ + useDebounce: (val: unknown) => val, +})); + +import { NoteEditor } from "./NoteEditor"; +import type { NoteDetail } from "@/lib/types"; + +const mockNote: NoteDetail = { + id: "note-1", + workspaceId: "ws-1", + title: "Test Note", + body: "

Hello world

", + excerpt: "Hello world", + status: "active", + tags: [], + updatedAt: "2026-01-01T00:00:00Z", + updatedBy: "user-1", + metadata: { + owner: "user-1", + source: "manual", + reviewState: "none", + taskCount: 0, + artifactCount: 0, + }, + linkedNotes: [], + tasks: [], + artifacts: [], + timeline: [], +}; + +describe("NoteEditor", () => { + const onSave = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the editor content area", () => { + render(); + expect(screen.getByTestId("editor-content")).toBeInTheDocument(); + }); + + it("renders Fix & Rewrite button when copilot props are provided", () => { + render(); + expect(screen.getByText("Fix & Rewrite")).toBeInTheDocument(); + }); + + it("renders Continue button when copilot props are provided", () => { + render(); + const btn = screen.getByText(/Continue/); + expect(btn).toBeInTheDocument(); + }); + + it("renders Explain button when copilot props are provided", () => { + render(); + expect(screen.getByText("Explain")).toBeInTheDocument(); + }); + + it("renders Tone dropdown button when copilot props are provided", () => { + render(); + const toneBtn = screen.getByText(/Tone/); + expect(toneBtn).toBeInTheDocument(); + }); + + it("renders standard formatting toolbar buttons (B, I, S)", () => { + render(); + expect(screen.getByTitle("B")).toBeInTheDocument(); + expect(screen.getByTitle("I")).toBeInTheDocument(); + }); +}); diff --git a/web/src/components/RunPromptModal.test.tsx b/web/src/components/RunPromptModal.test.tsx new file mode 100644 index 0000000..42ba000 --- /dev/null +++ b/web/src/components/RunPromptModal.test.tsx @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; + +vi.mock("@/lib/prompt-client", () => ({ + runPrompt: vi.fn().mockResolvedValue({ content: "Result", templateSlug: "summarize", outputType: "new_note" }), +})); + +vi.mock("@/lib/toast", () => ({ + toast: { success: vi.fn(), error: vi.fn() }, +})); + +import { RunPromptModal } from "./RunPromptModal"; +import type { PromptTemplate } from "@/lib/types"; + +const baseTemplate: PromptTemplate = { + id: "t1", + slug: "summarize", + name: "Summarize", + description: "Summarize the note", + category: "transform", + inputType: "text", + outputType: "new_note", + builtIn: true, +}; + +describe("RunPromptModal", () => { + const onClose = vi.fn(); + const onResult = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders dialog with template name", () => { + render(); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText("Summarize")).toBeInTheDocument(); + }); + + it("renders close button with aria-label", () => { + render(); + expect(screen.getByLabelText("Close modal")).toBeInTheDocument(); + }); + + it("renders run button with aria-label", () => { + render(); + expect(screen.getByLabelText("Run prompt")).toBeInTheDocument(); + }); + + it("renders custom instructions textarea", () => { + render(); + expect(screen.getByLabelText("Custom instructions")).toBeInTheDocument(); + }); + + it("shows additional note IDs input for multi-note templates", () => { + const multiTemplate = { ...baseTemplate, inputType: "multi-note" as const }; + render(); + expect(screen.getByLabelText("Additional note IDs")).toBeInTheDocument(); + }); + + it("does not show additional note IDs input for text templates", () => { + render(); + expect(screen.queryByLabelText("Additional note IDs")).not.toBeInTheDocument(); + }); +}); diff --git a/web/src/components/SmartActionsPanel.test.tsx b/web/src/components/SmartActionsPanel.test.tsx new file mode 100644 index 0000000..2936d20 --- /dev/null +++ b/web/src/components/SmartActionsPanel.test.tsx @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; + +// Mock prompt-client +vi.mock("@/lib/prompt-client", () => ({ + listPromptTemplates: vi.fn().mockResolvedValue([ + { id: "t1", slug: "summarize", name: "Summarize", description: "Summarize note", category: "transform", inputType: "text", outputType: "new_note", builtIn: true }, + { id: "t2", slug: "bulletize", name: "Bullet Points", description: "Bullets", category: "transform", inputType: "text", outputType: "replace", builtIn: true }, + ]), + runPrompt: vi.fn().mockResolvedValue({ content: "Result", templateSlug: "summarize", outputType: "new_note" }), + suggestTags: vi.fn().mockResolvedValue(["tag1", "tag2"]), + getReadingTime: vi.fn().mockResolvedValue({ wordCount: 500, readingTimeMinutes: 3 }), +})); + +vi.mock("@/lib/toast", () => ({ + toast: { success: vi.fn(), error: vi.fn() }, +})); + +import { SmartActionsPanel } from "./SmartActionsPanel"; + +describe("SmartActionsPanel", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the Smart Actions heading", () => { + render(); + expect(screen.getByText("Smart Actions")).toBeInTheDocument(); + }); + + it("renders suggest tags button with aria-label", () => { + render(); + expect(screen.getByLabelText("Suggest tags")).toBeInTheDocument(); + }); + + it("renders all category filter button", () => { + render(); + expect(screen.getByLabelText("All categories")).toBeInTheDocument(); + }); + + it("displays reading time after load", async () => { + render(); + expect(await screen.findByText(/3 min read/)).toBeInTheDocument(); + expect(await screen.findByText(/500 words/)).toBeInTheDocument(); + }); + + it("displays template buttons after load", async () => { + render(); + expect(await screen.findByLabelText("Run: Summarize")).toBeInTheDocument(); + expect(await screen.findByLabelText("Run: Bullet Points")).toBeInTheDocument(); + }); +}); diff --git a/web/src/lib/prompt-client.test.ts b/web/src/lib/prompt-client.test.ts new file mode 100644 index 0000000..1e19979 --- /dev/null +++ b/web/src/lib/prompt-client.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const fetchMock = vi.fn(); + +vi.mock("@bytelyst/api-client", () => ({ + createApiClient: () => ({ + fetch: fetchMock, + }), +})); + +import { + listPromptTemplates, + getPromptTemplate, + createPromptTemplate, + deletePromptTemplate, + runPrompt, + suggestTags, + checkDuplicates, + suggestLinks, + getReadingTime, + compareNotes, + mergeNotes, + getKnowledgeGaps, + listPromptHistory, +} from "@/lib/prompt-client"; + +describe("prompt-client", () => { + beforeEach(() => { + fetchMock.mockReset(); + }); + + it("listPromptTemplates returns items array", async () => { + fetchMock.mockResolvedValue({ items: [{ id: "t1", slug: "summarize" }] }); + const result = await listPromptTemplates(); + expect(result).toEqual([{ id: "t1", slug: "summarize" }]); + expect(fetchMock).toHaveBeenCalledWith("/note-prompts"); + }); + + it("getPromptTemplate fetches by id", async () => { + fetchMock.mockResolvedValue({ id: "t1", slug: "summarize" }); + const result = await getPromptTemplate("t1"); + expect(result.id).toBe("t1"); + }); + + it("createPromptTemplate posts body", async () => { + fetchMock.mockResolvedValue({ id: "t-new", slug: "custom" }); + const result = await createPromptTemplate({ + slug: "custom", + name: "Custom", + description: "", + category: "transform", + inputType: "text", + outputType: "new_note", + } as never); + expect(result.slug).toBe("custom"); + expect(fetchMock).toHaveBeenCalledWith("/note-prompts", expect.objectContaining({ method: "POST" })); + }); + + it("deletePromptTemplate sends DELETE", async () => { + fetchMock.mockResolvedValue(undefined); + await deletePromptTemplate("t1"); + expect(fetchMock).toHaveBeenCalledWith("/note-prompts/t1", expect.objectContaining({ method: "DELETE" })); + }); + + it("runPrompt posts run request", async () => { + fetchMock.mockResolvedValue({ content: "Summary", templateSlug: "summarize" }); + const result = await runPrompt({ templateId: "summarize", noteId: "n1", workspaceId: "ws1" }); + expect(result.content).toBe("Summary"); + }); + + it("suggestTags returns tag array", async () => { + fetchMock.mockResolvedValue({ tags: ["tag1", "tag2"] }); + const tags = await suggestTags("n1", "ws1"); + expect(tags).toEqual(["tag1", "tag2"]); + }); + + it("checkDuplicates returns duplicates array", async () => { + fetchMock.mockResolvedValue({ duplicates: [{ id: "n2", title: "Dup", similarity: 0.9 }] }); + const dups = await checkDuplicates("n1", "ws1"); + expect(dups).toHaveLength(1); + expect(dups[0].similarity).toBe(0.9); + }); + + it("suggestLinks returns suggestions array", async () => { + fetchMock.mockResolvedValue({ suggestions: [{ id: "n3", title: "Related", similarity: 0.7 }] }); + const links = await suggestLinks("n1", "ws1"); + expect(links).toHaveLength(1); + }); + + it("getReadingTime returns word count and minutes", async () => { + fetchMock.mockResolvedValue({ wordCount: 500, readingTimeMinutes: 3 }); + const result = await getReadingTime("n1", "ws1"); + expect(result.wordCount).toBe(500); + expect(result.readingTimeMinutes).toBe(3); + }); + + it("compareNotes sends noteIds", async () => { + fetchMock.mockResolvedValue({ content: "Comparison", templateSlug: "compare" }); + const result = await compareNotes(["n1", "n2"], "ws1"); + expect(result.content).toBe("Comparison"); + }); + + it("mergeNotes sends noteIds", async () => { + fetchMock.mockResolvedValue({ content: "Merged", templateSlug: "merge" }); + const result = await mergeNotes(["n1", "n2"], "ws1"); + expect(result.content).toBe("Merged"); + }); + + it("getKnowledgeGaps returns gaps and topicMap", async () => { + fetchMock.mockResolvedValue({ gaps: [{ topic: "AI", description: "Missing", suggestedTitle: "AI Intro" }], topicMap: { AI: 1 } }); + const result = await getKnowledgeGaps("ws1"); + expect(result.gaps).toHaveLength(1); + expect(result.topicMap.AI).toBe(1); + }); + + it("listPromptHistory returns items", async () => { + fetchMock.mockResolvedValue({ items: [{ id: "h1", noteId: "n1" }], total: 1 }); + const result = await listPromptHistory("ws1", 10); + expect(result.items).toHaveLength(1); + expect(result.total).toBe(1); + }); +});