import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "@tutti-os/ui-system"; import type { AgentQuickPromptLabels } from "./agentQuickPromptLabels"; import { AgentQuickPromptPopover } from "./AgentQuickPromptPopover"; import type { AgentQuickPromptLibraryController } from "./useAgentQuickPromptLibrary"; const labels = new Proxy( { deleteDescription: (title: string) => `Delete ${title}`, dragCancel: (title: string) => `Canceled ${title}`, dragDrop: (title: string) => `Dropped ${title}`, dragHandle: (title: string) => `Reorder ${title}`, dragMove: (title: string) => `Moving ${title}`, dragStart: (title: string) => `Picked up ${title}`, title: "Quick prompts", trigger: "Prompts", triggerTooltip: "Choose a quick prompt", searchPlaceholder: "Search quick prompts", startSorting: "Reorder", finishSorting: "Done", insertionError: "Could not insert into the Composer. Try again from the quick prompt list.", reorderDisabledMinimum: "At least two quick prompts are required to adjust their order", reorderDisabledPending: "Wait for the current quick prompt change to finish", reorderDisabledSearch: "Clear the search to adjust prompt order", reorderDisabledUnsupported: "This host does not support prompt reordering", add: "New prompt", createFromTemplate: "Recommended templates", moreActions: "More prompt actions", edit: "Edit", delete: "Delete", empty: "No quick prompts yet", loadError: "Quick prompts could not be loaded", loading: "Loading quick prompts…", noResults: "No matching quick prompts", recommendedTemplatesTitle: "Recommended templates", recommendedTemplatesDescription: "Choose one to prefill the editor. Saving adds it as a quick prompt and inserts it into the Composer.", returnToPrompts: "My prompts", useTemplate: "Use template", recommendedTemplates: [ { id: "summary-common-prompts", title: "Summarize common prompts", description: "Find insights and repeated work patterns", content: "Summarize my common prompts" }, { id: "understand-context", title: "Understand the situation", description: "Summarize context, constraints, risks, and next steps", content: "Summarize the situation" }, { id: "create-action-plan", title: "Create an action plan", description: "Break a goal into prioritized, verifiable steps", content: "Create an action plan" }, { id: "review-and-improve", title: "Review and improve", description: "Find gaps, risks, and practical improvements", content: "Review and improve" }, { id: "draft-clear-update", title: "Draft a clear update", description: "Write a concise explanation for the intended audience", content: "Draft an update" } ] }, { get: (target, property) => Reflect.get(target, property) ?? String(property) } ) as unknown as AgentQuickPromptLabels; function controller( patch: Partial = {} ): AgentQuickPromptLibraryController { const prompt = { id: "prompt-1", title: "Review", content: "Review the current change", version: 1, createdAtUnixMs: 1, updatedAtUnixMs: 2 }; return { capabilityAvailable: true, canReorder: false, close: vi.fn(), closeDialog: vi.fn(), deletePrompt: vi.fn(), filteredPrompts: [prompt], isDeleting: false, isEditorOpen: false, isPopoverOpen: true, isSaving: false, isInteractionLocked: false, isReordering: false, initialDraft: null, insertionError: false, labels, mode: "popover", mutationError: null, openCreate: vi.fn(), openEdit: vi.fn(), openPopover: vi.fn(), promptToDelete: null, retry: vi.fn(), reorderCapabilityAvailable: true, reorderError: null, reorderPrompts: vi.fn(async () => true), saveDraft: vi.fn(async () => true), searchQuery: "", showReorderHandles: false, selectPrompt: vi.fn(), selectedPrompt: null, setPopoverOpen: vi.fn(), setSearchQuery: vi.fn(), snapshot: { enabled: true, status: "ready", prompts: [prompt], error: null, revision: 1, pendingMutationIds: [] }, submitDelete: vi.fn(async () => true), ...patch }; } describe("AgentQuickPromptPopover", () => { it("uses the shared composer trigger color and icon sizing", () => { render( ); const trigger = screen.getByRole("button", { name: "Choose a quick prompt" }); expect(trigger).toHaveClass("agent-gui-node__composer-menu-trigger"); expect(trigger).toHaveClass("focus-visible:!outline-2"); expect(trigger.querySelector("svg")).toHaveClass("size-4"); }); it("keeps selection and direct icon management controls as sibling buttons", () => { const subject = controller(); render( ); const selection = screen.getByRole("button", { name: /^Review/u }); expect(selection.querySelector("button")).toBeNull(); const edit = screen.getByRole("button", { name: "Edit" }); const remove = screen.getByRole("button", { name: "Delete" }); expect(edit).toBeInTheDocument(); expect(remove).toBeInTheDocument(); fireEvent.pointerDown(edit, { button: 0 }); expect(subject.openEdit).toHaveBeenCalledOnce(); fireEvent.pointerDown(remove, { button: 0 }); expect(subject.deletePrompt).toHaveBeenCalledOnce(); }); it("shows reorder handles only in explicit sorting mode", () => { const first = controller().filteredPrompts[0]!; const second = { ...first, id: "prompt-2", title: "Plan", content: "Plan the current change" }; const subject = controller({ canReorder: true, filteredPrompts: [first, second], showReorderHandles: true, snapshot: { enabled: true, status: "ready", prompts: [first, second], error: null, revision: 1, pendingMutationIds: [] } }); const rendered = render( ); expect(screen.queryByRole("button", { name: "Reorder Review" })).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Reorder" })); expect( screen.getByRole("button", { name: "Reorder Review" }) ).toBeEnabled(); expect(screen.queryByPlaceholderText("Search quick prompts")).toBeNull(); expect(screen.queryByRole("button", { name: "Edit" })).toBeNull(); expect(screen.queryByRole("button", { name: "Delete" })).toBeNull(); expect( screen.queryByRole("button", { name: /Summarize common prompts.*Use template/u }) ).toBeNull(); expect( screen.queryByRole("button", { name: "Recommended templates" }) ).toBeNull(); rendered.rerender( ); expect( screen.getByRole("button", { name: "Reorder Review" }) ).toBeDisabled(); expect(screen.queryByRole("button", { name: /^Review/u })).toBeNull(); expect(screen.getByText("Review")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Done" })).toBeDisabled(); }); it("enters and finishes sorting on primary pointer down and keyboard click", () => { const first = controller().filteredPrompts[0]!; const second = { ...first, id: "prompt-2", title: "Plan" }; render( ); const sort = screen.getByRole("button", { name: "Reorder" }); fireEvent.pointerDown(sort, { button: 0 }); expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); const done = screen.getByRole("button", { name: "Done" }); fireEvent.pointerDown(done, { button: 0 }); expect(screen.getByRole("button", { name: "Reorder" })).toBeInTheDocument(); expect( screen.getByPlaceholderText("Search quick prompts") ).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Reorder" })); expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); expect(screen.queryByPlaceholderText("Search quick prompts")).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Done" })); expect(screen.getByRole("button", { name: "Reorder" })).toBeInTheDocument(); expect( screen.getByPlaceholderText("Search quick prompts") ).toBeInTheDocument(); }); it("reports a Composer insertion failure without reporting a save failure", () => { render( ); expect(screen.getByRole("alert")).toHaveTextContent( "Could not insert into the Composer. Try again from the quick prompt list." ); }); it("returns to the prompt list when insertion fails from the template view", () => { const rendered = render( ); fireEvent.click( screen.getByRole("button", { name: "Recommended templates" }) ); expect( screen.getByRole("heading", { name: "Recommended templates", level: 2 }) ).toBeInTheDocument(); rendered.rerender( ); expect( screen.getByRole("heading", { name: "Quick prompts", level: 2 }) ).toBeInTheDocument(); expect(screen.getByRole("alert")).toHaveTextContent( "Could not insert into the Composer. Try again from the quick prompt list." ); expect( screen.getByRole("button", { name: /^Review/u }) ).toBeInTheDocument(); }); it("explains why reordering is unavailable with stable reason priority", () => { const first = controller().filteredPrompts[0]!; const second = { ...first, id: "prompt-2", title: "Plan" }; const rendered = render( ); expect( screen.getByLabelText( "At least two quick prompts are required to adjust their order" ) ).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Reorder" })).toBeDisabled(); rendered.rerender( ); expect( screen.getByLabelText("This host does not support prompt reordering") ).toBeInTheDocument(); rendered.rerender( ); expect(screen.getByLabelText(labels.loading)).toBeInTheDocument(); rendered.rerender( ); expect(screen.getByLabelText(labels.loadError)).toBeInTheDocument(); }); it("resets sorting mode when the controlled Popover closes externally", () => { const first = controller().filteredPrompts[0]!; const second = { ...first, id: "prompt-2", title: "Plan" }; const openController = controller({ canReorder: true, filteredPrompts: [first, second], showReorderHandles: true, snapshot: { enabled: true, status: "ready", prompts: [first, second], error: null, revision: 1, pendingMutationIds: [] } }); const rendered = render( ); fireEvent.click(screen.getByRole("button", { name: "Reorder" })); expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument(); rendered.rerender( ); rendered.rerender( ); expect(screen.getByRole("button", { name: "Reorder" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Done" })).toBeNull(); expect(screen.queryByRole("button", { name: "Reorder Review" })).toBeNull(); }); it("disables recommended-template actions while a shared mutation is pending", () => { const subject = controller({ filteredPrompts: [], isInteractionLocked: true, snapshot: { enabled: true, status: "ready", prompts: [], error: null, revision: 1, pendingMutationIds: ["prompt-1"] } }); render( ); const summaryTemplate = screen.getByRole("button", { name: /Summarize common prompts.*Use template/u }); const createTemplate = screen.getByRole("button", { name: /Understand the situation.*Use template/u }); expect(summaryTemplate).toBeDisabled(); expect(createTemplate).toBeDisabled(); fireEvent.pointerDown(summaryTemplate, { button: 0 }); fireEvent.click(summaryTemplate, { detail: 1 }); expect(subject.openCreate).not.toHaveBeenCalled(); }); it("selects on primary pointer down and still accepts a later keyboard click", () => { const subject = controller(); render( ); const prompt = screen.getByRole("button", { name: /^Review/u }); fireEvent.pointerDown(prompt, { button: 0 }); expect(subject.selectPrompt).toHaveBeenCalledOnce(); fireEvent.click(prompt); expect(subject.selectPrompt).toHaveBeenCalledTimes(2); }); it("keeps touch and pen actions cancellable until click", () => { const subject = controller(); render( ); const prompt = screen.getByRole("button", { name: /^Review/u }); fireEvent.pointerDown(prompt, { button: 0, pointerType: "touch" }); fireEvent.pointerCancel(prompt, { pointerType: "touch" }); expect(subject.selectPrompt).not.toHaveBeenCalled(); fireEvent.pointerDown(prompt, { button: 0, pointerType: "pen" }); expect(subject.selectPrompt).not.toHaveBeenCalled(); fireEvent.click(prompt, { detail: 1 }); expect(subject.selectPrompt).toHaveBeenCalledOnce(); }); it("keeps the common-prompts recommendation inside the template view", () => { const subject = controller(); render( ); expect( screen.queryByRole("button", { name: /Summarize common prompts.*Use template/u }) ).toBeNull(); fireEvent.pointerDown( screen.getByRole("button", { name: "Recommended templates" }), { button: 0 } ); const summaryTemplate = screen.getByRole("button", { name: /Summarize common prompts.*Use template/u }); fireEvent.pointerDown(summaryTemplate, { button: 0 }); fireEvent.click(summaryTemplate, { detail: 1 }); expect(subject.openCreate).toHaveBeenCalledWith( { title: "Summarize common prompts", content: "Summarize my common prompts" }, { insertIntoComposerAfterSave: true, usagePromptType: "recommended_template" } ); expect(subject.openCreate).toHaveBeenCalledOnce(); expect(subject.saveDraft).not.toHaveBeenCalled(); }); it("starts creation on primary pointer down so the Dialog survives Popover dismissal", () => { const subject = controller(); render( ); fireEvent.pointerDown(screen.getByRole("button", { name: "New prompt" }), { button: 0 }); expect(subject.openCreate).toHaveBeenCalledOnce(); }); it("prefills the create editor for every recommended template", () => { const subject = controller({ filteredPrompts: [], snapshot: { enabled: true, status: "ready", prompts: [], error: null, revision: 1, pendingMutationIds: [] } }); render( ); const summaryTemplate = screen.getByRole("button", { name: /Summarize common prompts.*Use template/u }); fireEvent.click(summaryTemplate); expect(subject.openCreate).toHaveBeenNthCalledWith( 1, { title: "Summarize common prompts", content: "Summarize my common prompts" }, { insertIntoComposerAfterSave: true, usagePromptType: "recommended_template" } ); const createTemplate = screen.getByRole("button", { name: /Understand the situation.*Use template/u }); fireEvent.click(createTemplate); expect(subject.openCreate).toHaveBeenNthCalledWith( 2, { title: "Understand the situation", content: "Summarize the situation" }, { insertIntoComposerAfterSave: true, usagePromptType: "recommended_template" } ); expect(subject.openCreate).toHaveBeenCalledTimes(2); }); it("lets a non-empty library reopen recommended templates in the same Popover", () => { render( ); fireEvent.pointerDown( screen.getByRole("button", { name: "Recommended templates" }), { button: 0 } ); expect( screen.getByRole("heading", { name: "Recommended templates", level: 2 }) ).toBeInTheDocument(); expect(screen.queryByPlaceholderText("Search quick prompts")).toBeNull(); expect( screen.getByRole("button", { name: "My prompts" }) ).toBeInTheDocument(); fireEvent.pointerDown(screen.getByRole("button", { name: "My prompts" }), { button: 0 }); expect( screen.getByRole("heading", { name: "Quick prompts", level: 2 }) ).toBeInTheDocument(); expect( screen.getByPlaceholderText("Search quick prompts") ).toBeInTheDocument(); }); it("runs dialog cancellation and deletion on primary pointer down", () => { const createSubject = controller({ isEditorOpen: true, isPopoverOpen: false, mode: "create" }); const createRender = render( ); fireEvent.pointerDown(screen.getByRole("button", { name: labels.cancel }), { button: 0 }); expect(createSubject.closeDialog).toHaveBeenCalledOnce(); createRender.unmount(); const deleteSubject = controller({ isPopoverOpen: false, mode: "delete", promptToDelete: controller().filteredPrompts[0]! }); render( ); fireEvent.pointerDown( screen.getByRole("button", { name: labels.deleteConfirm }), { button: 0 } ); expect(deleteSubject.submitDelete).toHaveBeenCalledOnce(); }); it("preserves an edited draft when a conflict refreshes the same prompt", () => { const prompt = controller().filteredPrompts[0]!; const rendered = render( ); const content = screen.getByLabelText(labels.contentLabel); fireEvent.change(content, { target: { value: "My unsaved draft" } }); rendered.rerender( ); expect(screen.getByLabelText(labels.contentLabel)).toHaveValue( "My unsaved draft" ); }); it("keeps Enter in the editor Dialog out of the Composer shortcut", () => { const onComposerKeyDown = vi.fn(); render(
); fireEvent.keyDown(screen.getByLabelText(labels.titleLabel), { key: "Enter" }); expect(onComposerKeyDown).not.toHaveBeenCalled(); }); });