test(web): add prompt-client, SmartActionsPanel, RunPromptModal, NoteEditor tests (G10-G13)

This commit is contained in:
saravanakumardb1 2026-04-06 13:34:04 -07:00
parent 2a7cfbb73e
commit 4fd6994fb0
5 changed files with 343 additions and 2 deletions

View File

@ -55,7 +55,7 @@ const mockLog = {
} as unknown as import('fastify').FastifyBaseLogger;
function makeNote(overrides: Partial<NoteDoc> = {}): NoteDoc {
return {
const base: NoteDoc = {
id: 'note-1',
productId: 'notelett',
userId: 'user-1',
@ -64,12 +64,13 @@ function makeNote(overrides: Partial<NoteDoc> = {}): 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', () => {

View File

@ -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: () => "<p>Test body</p>",
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 }) => <div data-testid="editor-content">{editor ? "editor" : "no-editor"}</div>,
}));
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: "<p>Hello world</p>",
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(<NoteEditor note={mockNote} onSave={onSave} />);
expect(screen.getByTestId("editor-content")).toBeInTheDocument();
});
it("renders Fix & Rewrite button when copilot props are provided", () => {
render(<NoteEditor note={mockNote} onSave={onSave} copilotNoteId="n1" copilotWorkspaceId="ws1" />);
expect(screen.getByText("Fix & Rewrite")).toBeInTheDocument();
});
it("renders Continue button when copilot props are provided", () => {
render(<NoteEditor note={mockNote} onSave={onSave} copilotNoteId="n1" copilotWorkspaceId="ws1" />);
const btn = screen.getByText(/Continue/);
expect(btn).toBeInTheDocument();
});
it("renders Explain button when copilot props are provided", () => {
render(<NoteEditor note={mockNote} onSave={onSave} copilotNoteId="n1" copilotWorkspaceId="ws1" />);
expect(screen.getByText("Explain")).toBeInTheDocument();
});
it("renders Tone dropdown button when copilot props are provided", () => {
render(<NoteEditor note={mockNote} onSave={onSave} copilotNoteId="n1" copilotWorkspaceId="ws1" />);
const toneBtn = screen.getByText(/Tone/);
expect(toneBtn).toBeInTheDocument();
});
it("renders standard formatting toolbar buttons (B, I, S)", () => {
render(<NoteEditor note={mockNote} onSave={onSave} />);
expect(screen.getByTitle("B")).toBeInTheDocument();
expect(screen.getByTitle("I")).toBeInTheDocument();
});
});

View File

@ -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(<RunPromptModal template={baseTemplate} noteId="n1" workspaceId="ws1" onClose={onClose} onResult={onResult} />);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Summarize")).toBeInTheDocument();
});
it("renders close button with aria-label", () => {
render(<RunPromptModal template={baseTemplate} noteId="n1" workspaceId="ws1" onClose={onClose} onResult={onResult} />);
expect(screen.getByLabelText("Close modal")).toBeInTheDocument();
});
it("renders run button with aria-label", () => {
render(<RunPromptModal template={baseTemplate} noteId="n1" workspaceId="ws1" onClose={onClose} onResult={onResult} />);
expect(screen.getByLabelText("Run prompt")).toBeInTheDocument();
});
it("renders custom instructions textarea", () => {
render(<RunPromptModal template={baseTemplate} noteId="n1" workspaceId="ws1" onClose={onClose} onResult={onResult} />);
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(<RunPromptModal template={multiTemplate} noteId="n1" workspaceId="ws1" onClose={onClose} onResult={onResult} />);
expect(screen.getByLabelText("Additional note IDs")).toBeInTheDocument();
});
it("does not show additional note IDs input for text templates", () => {
render(<RunPromptModal template={baseTemplate} noteId="n1" workspaceId="ws1" onClose={onClose} onResult={onResult} />);
expect(screen.queryByLabelText("Additional note IDs")).not.toBeInTheDocument();
});
});

View File

@ -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(<SmartActionsPanel noteId="n1" workspaceId="ws1" noteTags={[]} />);
expect(screen.getByText("Smart Actions")).toBeInTheDocument();
});
it("renders suggest tags button with aria-label", () => {
render(<SmartActionsPanel noteId="n1" workspaceId="ws1" noteTags={[]} />);
expect(screen.getByLabelText("Suggest tags")).toBeInTheDocument();
});
it("renders all category filter button", () => {
render(<SmartActionsPanel noteId="n1" workspaceId="ws1" noteTags={[]} />);
expect(screen.getByLabelText("All categories")).toBeInTheDocument();
});
it("displays reading time after load", async () => {
render(<SmartActionsPanel noteId="n1" workspaceId="ws1" noteTags={[]} />);
expect(await screen.findByText(/3 min read/)).toBeInTheDocument();
expect(await screen.findByText(/500 words/)).toBeInTheDocument();
});
it("displays template buttons after load", async () => {
render(<SmartActionsPanel noteId="n1" workspaceId="ws1" noteTags={[]} />);
expect(await screen.findByLabelText("Run: Summarize")).toBeInTheDocument();
expect(await screen.findByLabelText("Run: Bullet Points")).toBeInTheDocument();
});
});

View File

@ -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);
});
});