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:
189
tests/components/BatchMode.test.tsx
Normal file
189
tests/components/BatchMode.test.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { BatchMode } from "@/components/BatchMode";
|
||||
import { useBatchStore } from "@/stores/batch-store";
|
||||
import { makeFocusItem } from "../helpers/fixtures";
|
||||
|
||||
describe("BatchMode", () => {
|
||||
const onOpenUrl = vi.fn();
|
||||
const onExit = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useBatchStore.getState().exitBatch();
|
||||
});
|
||||
|
||||
function startBatchWith(count: number) {
|
||||
const items = Array.from({ length: count }, (_, i) =>
|
||||
makeFocusItem({
|
||||
id: `r${i + 1}`,
|
||||
type: "mr_review",
|
||||
title: `Review MR !${900 + i}`,
|
||||
iid: 900 + i,
|
||||
})
|
||||
);
|
||||
useBatchStore.getState().startBatch(items, "CODE REVIEWS");
|
||||
}
|
||||
|
||||
describe("rendering", () => {
|
||||
it("shows the batch label", () => {
|
||||
startBatchWith(3);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
expect(screen.getByText(/CODE REVIEWS/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows progress (1 of N)", () => {
|
||||
startBatchWith(4);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
expect(screen.getByText(/1 of 4/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the current item title", () => {
|
||||
startBatchWith(3);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
expect(screen.getByText("Review MR !900")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows item metadata", () => {
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
// Metadata line contains IID and project
|
||||
const metaLine = screen.getByText((_content, el) => {
|
||||
return (
|
||||
el?.tagName === "P" &&
|
||||
Boolean(el.textContent?.includes("!900")) &&
|
||||
Boolean(el.textContent?.includes("platform/core"))
|
||||
);
|
||||
});
|
||||
expect(metaLine).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows action buttons", () => {
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
expect(
|
||||
screen.getByRole("button", { name: /open in gl/i })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /done/i })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /skip/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows ESC to exit hint", () => {
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
expect(screen.getByText(/ESC to exit/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("actions", () => {
|
||||
it("Open in GL calls onOpenUrl with current item URL", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: /open in gl/i })
|
||||
);
|
||||
expect(onOpenUrl).toHaveBeenCalledWith(
|
||||
"https://gitlab.com/platform/core/-/merge_requests/847"
|
||||
);
|
||||
});
|
||||
|
||||
it("Done marks item and advances to next", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(3);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /done/i }));
|
||||
|
||||
expect(screen.getByText(/2 of 3/)).toBeInTheDocument();
|
||||
// Wait for AnimatePresence to swap items
|
||||
expect(await screen.findByText("Review MR !901")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Skip marks item and advances to next", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(3);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /skip/i }));
|
||||
|
||||
expect(screen.getByText(/2 of 3/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyboard shortcuts", () => {
|
||||
it("Cmd+D triggers Done", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.keyboard("{Meta>}d{/Meta}");
|
||||
|
||||
expect(screen.getByText(/2 of 2/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Cmd+S triggers Skip", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.keyboard("{Meta>}s{/Meta}");
|
||||
|
||||
expect(screen.getByText(/2 of 2/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Escape exits batch mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.keyboard("{Escape}");
|
||||
expect(onExit).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("completion", () => {
|
||||
it("shows celebration when all items are processed", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(2);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /done/i }));
|
||||
await user.click(screen.getByRole("button", { name: /done/i }));
|
||||
|
||||
expect(screen.getByText(/all done/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/2.*completed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows completed and skipped counts in celebration", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(3);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /done/i }));
|
||||
await user.click(screen.getByRole("button", { name: /skip/i }));
|
||||
await user.click(screen.getByRole("button", { name: /done/i }));
|
||||
|
||||
expect(screen.getByText(/2.*completed/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1.*skipped/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("celebration has a button to exit", async () => {
|
||||
const user = userEvent.setup();
|
||||
startBatchWith(1);
|
||||
render(<BatchMode onOpenUrl={onOpenUrl} onExit={onExit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /done/i }));
|
||||
|
||||
const exitBtn = screen.getByRole("button", { name: /back to focus/i });
|
||||
await user.click(exitBtn);
|
||||
expect(onExit).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user