feat(bd-1cu): implement FocusView container with focus selection
Add suggestion state when no focus is set but items exist in queue: - FocusView now shows SuggestionCard when current is null but queue has items - SuggestionCard displays suggested item with 'Set as focus' button - Clicking 'Set as focus' promotes the suggestion to current focus - Auto-advances to next item after completing current focus - Shows empty state celebration when all items are complete TDD: 14 tests covering focus, suggestion, empty states, and actions
This commit is contained in:
223
tests/components/FocusView.test.tsx
Normal file
223
tests/components/FocusView.test.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* FocusView tests -- the main focus container.
|
||||
*
|
||||
* Tests:
|
||||
* 1. Shows FocusCard when focus is set
|
||||
* 2. Shows empty state when no focus and no items
|
||||
* 3. Shows suggestion when no focus but items exist
|
||||
* 4. Auto-advances to next item after complete
|
||||
* 5. Shows celebration on last item complete
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { FocusView } from "@/components/FocusView";
|
||||
import { useFocusStore } from "@/stores/focus-store";
|
||||
import { makeFocusItem } from "../helpers/fixtures";
|
||||
|
||||
// Mock the shell plugin for URL opening - must return Promise
|
||||
vi.mock("@tauri-apps/plugin-shell", () => ({
|
||||
open: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
describe("FocusView", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
useFocusStore.setState({
|
||||
current: null,
|
||||
queue: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("with focus set", () => {
|
||||
it("shows FocusCard when focus is set", () => {
|
||||
const item = makeFocusItem({ id: "1", title: "Test Item" });
|
||||
useFocusStore.setState({ current: item, queue: [] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
expect(screen.getByText("Test Item")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /start/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows queue summary when items exist in queue", () => {
|
||||
const current = makeFocusItem({ id: "1", title: "Current" });
|
||||
const queued = makeFocusItem({ id: "2", title: "Queued", type: "issue" });
|
||||
useFocusStore.setState({ current, queue: [queued] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
expect(screen.getByText(/Queue:/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1 issue/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty state", () => {
|
||||
it("shows empty state when no focus and no items", () => {
|
||||
useFocusStore.setState({ current: null, queue: [] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
expect(screen.getByText(/all clear/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/nothing needs your attention/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows celebration message in empty state", () => {
|
||||
useFocusStore.setState({ current: null, queue: [] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
expect(screen.getByText(/nice work/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("suggestion state", () => {
|
||||
it("shows suggestion when no focus but items exist in queue", () => {
|
||||
const item = makeFocusItem({ id: "1", title: "Suggested Item" });
|
||||
useFocusStore.setState({ current: null, queue: [item] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
// Should show the item as a suggestion
|
||||
expect(screen.getByText("Suggested Item")).toBeInTheDocument();
|
||||
// Should have a "Set as focus" or "Start" button
|
||||
expect(
|
||||
screen.getByRole("button", { name: /set as focus|start/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("promotes suggestion to focus when user clicks set as focus", async () => {
|
||||
const user = userEvent.setup();
|
||||
const item = makeFocusItem({ id: "1", title: "Suggested Item" });
|
||||
useFocusStore.setState({ current: null, queue: [item] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
// Click the set as focus button
|
||||
await user.click(screen.getByRole("button", { name: /set as focus|start/i }));
|
||||
|
||||
// Item should now be the current focus
|
||||
expect(useFocusStore.getState().current?.id).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto-advance behavior", () => {
|
||||
it("auto-advances to next item after complete", async () => {
|
||||
const user = userEvent.setup();
|
||||
const item1 = makeFocusItem({ id: "1", title: "First Item" });
|
||||
const item2 = makeFocusItem({ id: "2", title: "Second Item" });
|
||||
useFocusStore.setState({ current: item1, queue: [item2] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
// Complete current focus by clicking start (which advances)
|
||||
await user.click(screen.getByRole("button", { name: /start/i }));
|
||||
|
||||
// Should show next item
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Second Item")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows empty state after last item complete", async () => {
|
||||
const user = userEvent.setup();
|
||||
const item = makeFocusItem({ id: "1", title: "Only Item" });
|
||||
useFocusStore.setState({ current: item, queue: [] });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
// Complete the only item
|
||||
await user.click(screen.getByRole("button", { name: /start/i }));
|
||||
|
||||
// Should show empty/celebration state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/all clear/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("focus selection", () => {
|
||||
it("allows selecting a specific item as focus via setFocus", () => {
|
||||
const item1 = makeFocusItem({ id: "1", title: "First" });
|
||||
const item2 = makeFocusItem({ id: "2", title: "Second" });
|
||||
const item3 = makeFocusItem({ id: "3", title: "Third" });
|
||||
useFocusStore.setState({ current: item1, queue: [item2, item3] });
|
||||
|
||||
// Use setFocus to promote item3
|
||||
useFocusStore.getState().setFocus("3");
|
||||
|
||||
const state = useFocusStore.getState();
|
||||
expect(state.current?.id).toBe("3");
|
||||
expect(state.queue.map((i) => i.id)).toContain("1");
|
||||
expect(state.queue.map((i) => i.id)).toContain("2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loading and error states", () => {
|
||||
it("shows loading state", () => {
|
||||
useFocusStore.setState({ isLoading: true });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows error state", () => {
|
||||
useFocusStore.setState({ error: "Something went wrong" });
|
||||
|
||||
render(<FocusView />);
|
||||
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("action handlers", () => {
|
||||
it("calls act with start action when Start is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const item = makeFocusItem({ id: "1", title: "Test" });
|
||||
|
||||
// Create a mock act function to track calls
|
||||
const mockAct = vi.fn((_action: string, _reason?: string) => null);
|
||||
useFocusStore.setState({ current: item, queue: [], act: mockAct });
|
||||
|
||||
render(<FocusView />);
|
||||
await user.click(screen.getByRole("button", { name: /start/i }));
|
||||
|
||||
// act is called with "start" action
|
||||
expect(mockAct).toHaveBeenCalledWith("start");
|
||||
});
|
||||
|
||||
it("calls act with defer_1h action when 1 hour is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const item = makeFocusItem({ id: "1", title: "Test" });
|
||||
|
||||
const mockAct = vi.fn((_action: string, _reason?: string) => null);
|
||||
useFocusStore.setState({ current: item, queue: [], act: mockAct });
|
||||
|
||||
render(<FocusView />);
|
||||
await user.click(screen.getByRole("button", { name: /1 hour/i }));
|
||||
|
||||
expect(mockAct).toHaveBeenCalledWith("defer_1h");
|
||||
});
|
||||
|
||||
it("calls act with skip action when Skip is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const item = makeFocusItem({ id: "1", title: "Test" });
|
||||
|
||||
const mockAct = vi.fn((_action: string, _reason?: string) => null);
|
||||
useFocusStore.setState({ current: item, queue: [], act: mockAct });
|
||||
|
||||
render(<FocusView />);
|
||||
await user.click(screen.getByRole("button", { name: /skip/i }));
|
||||
|
||||
expect(mockAct).toHaveBeenCalledWith("skip");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user