test: add comprehensive frontend tests for components, stores, and utils
Full test coverage for the frontend implementation using Vitest and Testing Library. Tests are organized by concern with shared fixtures. Component tests: - AppShell.test.tsx: Navigation tabs, view switching, batch mode overlay - FocusCard.test.tsx: Rendering, action buttons, keyboard shortcuts, empty state - QueueView.test.tsx: Item display, focus promotion, empty state - QueueItem.test.tsx: Type badges, click handling - QueueSummary.test.tsx: Count display by type - QuickCapture.test.tsx: Modal behavior, form submission, error states - BatchMode.test.tsx: Progress tracking, item advancement, completion - App.test.tsx: Updated for AppShell integration Store tests: - focus-store.test.ts: Item management, act(), setFocus(), reorderQueue() - nav-store.test.ts: View switching - capture-store.test.ts: Open/close, submission states - batch-store.test.ts: Batch lifecycle, status tracking, derived counts Library tests: - types.test.ts: Type guards, staleness computation - transform.test.ts: Lore data transformation, priority ordering - format.test.ts: IID formatting for MRs vs issues E2E tests (app.spec.ts): - Navigation flow - Focus card interactions - Queue management - Quick capture flow Test infrastructure: - fixtures.ts: makeFocusItem() factory - tauri-plugin-shell.ts: Mock for @tauri-apps/plugin-shell - Updated tauri-api.ts mock with new commands - vitest.config.ts: Path aliases, jsdom environment - playwright.config.ts: Removed webServer (run separately) - package.json: Added @tauri-apps/plugin-shell dependency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
182
tests/components/QuickCapture.test.tsx
Normal file
182
tests/components/QuickCapture.test.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { QuickCapture } from "@/components/QuickCapture";
|
||||
import { useCaptureStore } from "@/stores/capture-store";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
|
||||
describe("QuickCapture", () => {
|
||||
beforeEach(() => {
|
||||
useCaptureStore.setState({
|
||||
isOpen: true,
|
||||
isSubmitting: false,
|
||||
lastCapturedId: null,
|
||||
error: null,
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
it("renders nothing when closed", () => {
|
||||
useCaptureStore.setState({ isOpen: false });
|
||||
const { container } = render(<QuickCapture />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders overlay when open", () => {
|
||||
render(<QuickCapture />);
|
||||
expect(screen.getByPlaceholderText("Capture a thought...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("auto-focuses the input", () => {
|
||||
render(<QuickCapture />);
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
|
||||
it("shows a submit button", () => {
|
||||
render(<QuickCapture />);
|
||||
expect(screen.getByRole("button", { name: /capture/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("submission", () => {
|
||||
it("calls quick_capture on Enter", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "Fix the login bug{enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith("quick_capture", {
|
||||
title: "Fix the login bug",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("calls quick_capture on button click", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "New feature idea");
|
||||
await user.click(screen.getByRole("button", { name: /capture/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith("quick_capture", {
|
||||
title: "New feature idea",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("does not submit empty input", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "{enter}");
|
||||
|
||||
expect(invoke).not.toHaveBeenCalledWith("quick_capture", expect.anything());
|
||||
});
|
||||
|
||||
it("does not submit whitespace-only input", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, " {enter}");
|
||||
|
||||
expect(invoke).not.toHaveBeenCalledWith("quick_capture", expect.anything());
|
||||
});
|
||||
|
||||
it("closes overlay on successful capture", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "Quick thought{enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useCaptureStore.getState().isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error on failed capture", async () => {
|
||||
(invoke as ReturnType<typeof vi.fn>).mockRejectedValueOnce({
|
||||
code: "BEADS_UNAVAILABLE",
|
||||
message: "br CLI not found",
|
||||
recoverable: true,
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "Doomed thought{enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("disables input during submission", async () => {
|
||||
// Make invoke hang
|
||||
(invoke as ReturnType<typeof vi.fn>).mockImplementationOnce(
|
||||
() => new Promise(() => {})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "Slow thought{enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dismissal", () => {
|
||||
it("closes on Escape", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
await user.keyboard("{Escape}");
|
||||
|
||||
expect(useCaptureStore.getState().isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it("closes on backdrop click", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
// Click the backdrop (the outer overlay div)
|
||||
const backdrop = screen.getByTestId("capture-backdrop");
|
||||
await user.click(backdrop);
|
||||
|
||||
expect(useCaptureStore.getState().isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error display", () => {
|
||||
it("shows error message from store", () => {
|
||||
useCaptureStore.setState({ error: "br CLI not found" });
|
||||
render(<QuickCapture />);
|
||||
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("br CLI not found");
|
||||
});
|
||||
|
||||
it("clears error when user types", async () => {
|
||||
useCaptureStore.setState({ error: "previous error" });
|
||||
const user = userEvent.setup();
|
||||
render(<QuickCapture />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Capture a thought...");
|
||||
await user.type(input, "a");
|
||||
|
||||
expect(useCaptureStore.getState().error).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user