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:
159
tests/stores/batch-store.test.ts
Normal file
159
tests/stores/batch-store.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useBatchStore } from "@/stores/batch-store";
|
||||
import { makeFocusItem } from "../helpers/fixtures";
|
||||
|
||||
describe("useBatchStore", () => {
|
||||
beforeEach(() => {
|
||||
useBatchStore.getState().exitBatch();
|
||||
});
|
||||
|
||||
describe("startBatch", () => {
|
||||
it("activates batch mode with items", () => {
|
||||
const items = [
|
||||
makeFocusItem({ id: "r1", title: "Review A" }),
|
||||
makeFocusItem({ id: "r2", title: "Review B" }),
|
||||
makeFocusItem({ id: "r3", title: "Review C" }),
|
||||
];
|
||||
|
||||
useBatchStore.getState().startBatch(items, "CODE REVIEWS");
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.isActive).toBe(true);
|
||||
expect(state.batchLabel).toBe("CODE REVIEWS");
|
||||
expect(state.items).toHaveLength(3);
|
||||
expect(state.statuses).toEqual(["pending", "pending", "pending"]);
|
||||
expect(state.currentIndex).toBe(0);
|
||||
expect(state.startedAt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("initializes with zero completed and skipped", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[makeFocusItem({ id: "r1" })],
|
||||
"TEST"
|
||||
);
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.completedCount()).toBe(0);
|
||||
expect(state.skippedCount()).toBe(0);
|
||||
expect(state.isFinished()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("markDone", () => {
|
||||
it("marks current item as done and advances", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[
|
||||
makeFocusItem({ id: "r1" }),
|
||||
makeFocusItem({ id: "r2" }),
|
||||
makeFocusItem({ id: "r3" }),
|
||||
],
|
||||
"TEST"
|
||||
);
|
||||
|
||||
useBatchStore.getState().markDone();
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.statuses[0]).toBe("done");
|
||||
expect(state.currentIndex).toBe(1);
|
||||
expect(state.completedCount()).toBe(1);
|
||||
});
|
||||
|
||||
it("finishes when last item is marked done", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[makeFocusItem({ id: "r1" }), makeFocusItem({ id: "r2" })],
|
||||
"TEST"
|
||||
);
|
||||
|
||||
useBatchStore.getState().markDone();
|
||||
useBatchStore.getState().markDone();
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.completedCount()).toBe(2);
|
||||
expect(state.isFinished()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("markSkipped", () => {
|
||||
it("marks current item as skipped and advances", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[makeFocusItem({ id: "r1" }), makeFocusItem({ id: "r2" })],
|
||||
"TEST"
|
||||
);
|
||||
|
||||
useBatchStore.getState().markSkipped();
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.statuses[0]).toBe("skipped");
|
||||
expect(state.currentIndex).toBe(1);
|
||||
expect(state.skippedCount()).toBe(1);
|
||||
});
|
||||
|
||||
it("mixed done and skipped tracks correctly", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[
|
||||
makeFocusItem({ id: "r1" }),
|
||||
makeFocusItem({ id: "r2" }),
|
||||
makeFocusItem({ id: "r3" }),
|
||||
],
|
||||
"TEST"
|
||||
);
|
||||
|
||||
useBatchStore.getState().markDone();
|
||||
useBatchStore.getState().markSkipped();
|
||||
useBatchStore.getState().markDone();
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.completedCount()).toBe(2);
|
||||
expect(state.skippedCount()).toBe(1);
|
||||
expect(state.isFinished()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exitBatch", () => {
|
||||
it("clears all batch state", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[makeFocusItem({ id: "r1" })],
|
||||
"TEST"
|
||||
);
|
||||
useBatchStore.getState().markDone();
|
||||
|
||||
useBatchStore.getState().exitBatch();
|
||||
|
||||
const state = useBatchStore.getState();
|
||||
expect(state.isActive).toBe(false);
|
||||
expect(state.items).toHaveLength(0);
|
||||
expect(state.statuses).toHaveLength(0);
|
||||
expect(state.currentIndex).toBe(0);
|
||||
expect(state.startedAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("progress tracking", () => {
|
||||
it("reports correct progress through batch", () => {
|
||||
useBatchStore.getState().startBatch(
|
||||
[
|
||||
makeFocusItem({ id: "r1" }),
|
||||
makeFocusItem({ id: "r2" }),
|
||||
makeFocusItem({ id: "r3" }),
|
||||
makeFocusItem({ id: "r4" }),
|
||||
],
|
||||
"REVIEWS"
|
||||
);
|
||||
|
||||
expect(useBatchStore.getState().isFinished()).toBe(false);
|
||||
|
||||
useBatchStore.getState().markDone();
|
||||
useBatchStore.getState().markDone();
|
||||
|
||||
expect(useBatchStore.getState().completedCount()).toBe(2);
|
||||
expect(useBatchStore.getState().isFinished()).toBe(false);
|
||||
|
||||
useBatchStore.getState().markSkipped();
|
||||
useBatchStore.getState().markDone();
|
||||
|
||||
expect(useBatchStore.getState().completedCount()).toBe(3);
|
||||
expect(useBatchStore.getState().skippedCount()).toBe(1);
|
||||
expect(useBatchStore.getState().isFinished()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
66
tests/stores/capture-store.test.ts
Normal file
66
tests/stores/capture-store.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useCaptureStore } from "@/stores/capture-store";
|
||||
|
||||
describe("useCaptureStore", () => {
|
||||
beforeEach(() => {
|
||||
useCaptureStore.setState({
|
||||
isOpen: false,
|
||||
isSubmitting: false,
|
||||
lastCapturedId: null,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("starts closed", () => {
|
||||
const state = useCaptureStore.getState();
|
||||
expect(state.isOpen).toBe(false);
|
||||
expect(state.isSubmitting).toBe(false);
|
||||
});
|
||||
|
||||
it("opens the overlay", () => {
|
||||
useCaptureStore.getState().open();
|
||||
expect(useCaptureStore.getState().isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("closes the overlay and resets state", () => {
|
||||
useCaptureStore.setState({ isOpen: true, isSubmitting: true, error: "test" });
|
||||
useCaptureStore.getState().close();
|
||||
|
||||
const state = useCaptureStore.getState();
|
||||
expect(state.isOpen).toBe(false);
|
||||
expect(state.isSubmitting).toBe(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
||||
it("sets submitting state", () => {
|
||||
useCaptureStore.getState().setSubmitting(true);
|
||||
expect(useCaptureStore.getState().isSubmitting).toBe(true);
|
||||
});
|
||||
|
||||
it("records a successful capture", () => {
|
||||
useCaptureStore.getState().captureSuccess("bd-123");
|
||||
|
||||
const state = useCaptureStore.getState();
|
||||
expect(state.lastCapturedId).toBe("bd-123");
|
||||
expect(state.isSubmitting).toBe(false);
|
||||
expect(state.isOpen).toBe(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
||||
it("records a failed capture", () => {
|
||||
useCaptureStore.setState({ isSubmitting: true });
|
||||
useCaptureStore.getState().captureError("br command failed");
|
||||
|
||||
const state = useCaptureStore.getState();
|
||||
expect(state.error).toBe("br command failed");
|
||||
expect(state.isSubmitting).toBe(false);
|
||||
expect(state.isOpen).toBe(true); // stays open so user can retry
|
||||
});
|
||||
|
||||
it("clears error when opening", () => {
|
||||
useCaptureStore.setState({ error: "old error" });
|
||||
useCaptureStore.getState().open();
|
||||
|
||||
expect(useCaptureStore.getState().error).toBeNull();
|
||||
});
|
||||
});
|
||||
192
tests/stores/focus-store.test.ts
Normal file
192
tests/stores/focus-store.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useFocusStore } from "@/stores/focus-store";
|
||||
import { makeFocusItem } from "../helpers/fixtures";
|
||||
|
||||
describe("useFocusStore", () => {
|
||||
beforeEach(() => {
|
||||
// Reset store between tests
|
||||
useFocusStore.setState({
|
||||
current: null,
|
||||
queue: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("setItems", () => {
|
||||
it("sets first item as current and rest as queue", () => {
|
||||
const items = [
|
||||
makeFocusItem({ id: "a", title: "First" }),
|
||||
makeFocusItem({ id: "b", title: "Second" }),
|
||||
makeFocusItem({ id: "c", title: "Third" }),
|
||||
];
|
||||
|
||||
useFocusStore.getState().setItems(items);
|
||||
|
||||
const state = useFocusStore.getState();
|
||||
expect(state.current?.id).toBe("a");
|
||||
expect(state.queue).toHaveLength(2);
|
||||
expect(state.queue[0].id).toBe("b");
|
||||
expect(state.queue[1].id).toBe("c");
|
||||
});
|
||||
|
||||
it("sets current to null when empty", () => {
|
||||
useFocusStore.getState().setItems([]);
|
||||
|
||||
const state = useFocusStore.getState();
|
||||
expect(state.current).toBeNull();
|
||||
expect(state.queue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("clears loading and error on setItems", () => {
|
||||
useFocusStore.setState({ isLoading: true, error: "old error" });
|
||||
|
||||
useFocusStore.getState().setItems([makeFocusItem()]);
|
||||
|
||||
const state = useFocusStore.getState();
|
||||
expect(state.isLoading).toBe(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("act", () => {
|
||||
it("advances to next item in queue", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "a" }),
|
||||
makeFocusItem({ id: "b" }),
|
||||
makeFocusItem({ id: "c" }),
|
||||
]);
|
||||
|
||||
const next = useFocusStore.getState().act("start");
|
||||
|
||||
expect(next?.id).toBe("b");
|
||||
expect(useFocusStore.getState().current?.id).toBe("b");
|
||||
expect(useFocusStore.getState().queue).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("returns null when queue is empty", () => {
|
||||
useFocusStore.getState().setItems([makeFocusItem({ id: "only" })]);
|
||||
|
||||
const next = useFocusStore.getState().act("skip");
|
||||
|
||||
expect(next).toBeNull();
|
||||
expect(useFocusStore.getState().current).toBeNull();
|
||||
expect(useFocusStore.getState().queue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("works with defer_1h action", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "a" }),
|
||||
makeFocusItem({ id: "b" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().act("defer_1h", "in a meeting");
|
||||
|
||||
expect(useFocusStore.getState().current?.id).toBe("b");
|
||||
});
|
||||
|
||||
it("works with defer_tomorrow action", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "a" }),
|
||||
makeFocusItem({ id: "b" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().act("defer_tomorrow");
|
||||
|
||||
expect(useFocusStore.getState().current?.id).toBe("b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setFocus", () => {
|
||||
it("promotes a queue item to current", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "a", title: "First" }),
|
||||
makeFocusItem({ id: "b", title: "Second" }),
|
||||
makeFocusItem({ id: "c", title: "Third" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().setFocus("c");
|
||||
|
||||
const state = useFocusStore.getState();
|
||||
expect(state.current?.id).toBe("c");
|
||||
// Previous current and other queue items are in queue
|
||||
expect(state.queue.map((i) => i.id)).toEqual(
|
||||
expect.arrayContaining(["a", "b"])
|
||||
);
|
||||
expect(state.queue).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("does nothing for unknown item ID", () => {
|
||||
useFocusStore.getState().setItems([makeFocusItem({ id: "a" })]);
|
||||
|
||||
useFocusStore.getState().setFocus("nonexistent");
|
||||
|
||||
expect(useFocusStore.getState().current?.id).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("reorderQueue", () => {
|
||||
it("moves an item from one position to another", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "focus" }),
|
||||
makeFocusItem({ id: "a" }),
|
||||
makeFocusItem({ id: "b" }),
|
||||
makeFocusItem({ id: "c" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().reorderQueue(2, 0);
|
||||
|
||||
const ids = useFocusStore.getState().queue.map((i) => i.id);
|
||||
expect(ids).toEqual(["c", "a", "b"]);
|
||||
});
|
||||
|
||||
it("does nothing for same from/to index", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "focus" }),
|
||||
makeFocusItem({ id: "a" }),
|
||||
makeFocusItem({ id: "b" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().reorderQueue(0, 0);
|
||||
|
||||
const ids = useFocusStore.getState().queue.map((i) => i.id);
|
||||
expect(ids).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("does nothing for out-of-bounds indices", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "focus" }),
|
||||
makeFocusItem({ id: "a" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().reorderQueue(-1, 0);
|
||||
useFocusStore.getState().reorderQueue(0, 5);
|
||||
|
||||
expect(useFocusStore.getState().queue.map((i) => i.id)).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("does not affect current focus", () => {
|
||||
useFocusStore.getState().setItems([
|
||||
makeFocusItem({ id: "focus" }),
|
||||
makeFocusItem({ id: "a" }),
|
||||
makeFocusItem({ id: "b" }),
|
||||
]);
|
||||
|
||||
useFocusStore.getState().reorderQueue(1, 0);
|
||||
|
||||
expect(useFocusStore.getState().current?.id).toBe("focus");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setLoading / setError", () => {
|
||||
it("sets loading state", () => {
|
||||
useFocusStore.getState().setLoading(true);
|
||||
expect(useFocusStore.getState().isLoading).toBe(true);
|
||||
});
|
||||
|
||||
it("sets error state", () => {
|
||||
useFocusStore.getState().setError("something broke");
|
||||
expect(useFocusStore.getState().error).toBe("something broke");
|
||||
});
|
||||
});
|
||||
});
|
||||
28
tests/stores/nav-store.test.ts
Normal file
28
tests/stores/nav-store.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useNavStore } from "@/stores/nav-store";
|
||||
|
||||
describe("useNavStore", () => {
|
||||
beforeEach(() => {
|
||||
useNavStore.setState({ activeView: "focus" });
|
||||
});
|
||||
|
||||
it("defaults to focus view", () => {
|
||||
expect(useNavStore.getState().activeView).toBe("focus");
|
||||
});
|
||||
|
||||
it("switches to queue view", () => {
|
||||
useNavStore.getState().setView("queue");
|
||||
expect(useNavStore.getState().activeView).toBe("queue");
|
||||
});
|
||||
|
||||
it("switches to inbox view", () => {
|
||||
useNavStore.getState().setView("inbox");
|
||||
expect(useNavStore.getState().activeView).toBe("inbox");
|
||||
});
|
||||
|
||||
it("switches back to focus", () => {
|
||||
useNavStore.getState().setView("queue");
|
||||
useNavStore.getState().setView("focus");
|
||||
expect(useNavStore.getState().activeView).toBe("focus");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user