test(web): add prompt-client, SmartActionsPanel, RunPromptModal, NoteEditor tests (G10-G13)
This commit is contained in:
parent
2a7cfbb73e
commit
4fd6994fb0
@ -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', () => {
|
||||
|
||||
101
web/src/components/NoteEditor.test.tsx
Normal file
101
web/src/components/NoteEditor.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
65
web/src/components/RunPromptModal.test.tsx
Normal file
65
web/src/components/RunPromptModal.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
52
web/src/components/SmartActionsPanel.test.tsx
Normal file
52
web/src/components/SmartActionsPanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
122
web/src/lib/prompt-client.test.ts
Normal file
122
web/src/lib/prompt-client.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user