diff --git a/package-lock.json b/package-lock.json index 85e3da6..d81ca24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@tauri-apps/cli": "^2.1.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^25.6.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -1793,6 +1794,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", diff --git a/package.json b/package.json index e95d09e..378952b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@tauri-apps/cli": "^2.1.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^25.6.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index 549b660..8f39d94 100644 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -1,4 +1,5 @@ import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { createFixtureFocusReader } from "../api/fixtureFocusReader"; import type { FocusWriter } from "../api/focusWriter"; @@ -56,4 +57,21 @@ describe("App overlay", () => { expect(screen.getByText("API refactor")).toBeInTheDocument(); }); }); + + it("add-task input calls focusWriter.appendTask with selected focus id", async () => { + const writer = noopFocusWriter(); + render(); + + await waitFor(() => { + expect(screen.getByText("Customer X bug")).toBeInTheDocument(); + }); + + // Click the pig to open PigDetail + await userEvent.click(screen.getByText("Customer X bug")); + + const input = screen.getByPlaceholderText("Add task…"); + await userEvent.type(input, "write tests{Enter}"); + + expect(writer.appendTask).toHaveBeenCalledWith("a", "write tests"); + }); }); diff --git a/src/components/App.tsx b/src/components/App.tsx index aac7414..54606ce 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -31,6 +31,15 @@ export function App({ focusReader, focusWriter }: AppProps) { } } + async function handleAddTask(text: string) { + if (!selectedFocus) return; + try { + await focusWriter.appendTask(selectedFocus.id, text); + } catch { + // focusWriter already logs the typed error + } + } + return (
{pigs.map((pig) => ( @@ -53,6 +62,7 @@ export function App({ focusReader, focusWriter }: AppProps) { viewportH={screenH} onClose={() => setSelectedId(null)} onClearTask={handleClearTask} + onAddTask={handleAddTask} /> )}
diff --git a/src/components/PigDetail.test.tsx b/src/components/PigDetail.test.tsx new file mode 100644 index 0000000..50d8791 --- /dev/null +++ b/src/components/PigDetail.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import type { Focus } from "../types/focus"; +import { PigDetail } from "./PigDetail"; + +const baseFocus: Focus = { + id: "pig-a", + title: "Ship it", + description: "", + tasks: [], +}; + +function renderDetail(overrides?: Partial>) { + const props = { + focus: baseFocus, + pigX: 100, + pigY: 100, + viewportW: 1920, + viewportH: 1080, + onClose: vi.fn(), + onClearTask: vi.fn(), + onAddTask: vi.fn(), + ...overrides, + }; + render(); + return props; +} + +describe("PigDetail add-task input", () => { + it("renders an add task input with placeholder", () => { + renderDetail(); + expect(screen.getByPlaceholderText("Add task…")).toBeInTheDocument(); + }); + + it("Enter calls onAddTask with trimmed text", async () => { + const { onAddTask } = renderDetail(); + const input = screen.getByPlaceholderText("Add task…"); + await userEvent.type(input, "fix the thing{Enter}"); + expect(onAddTask).toHaveBeenCalledWith("fix the thing"); + }); + + it("Enter clears the input field", async () => { + renderDetail(); + const input = screen.getByPlaceholderText("Add task…"); + await userEvent.type(input, "some task{Enter}"); + expect(input).toHaveValue(""); + }); + + it("Enter with empty text does not call onAddTask", async () => { + const { onAddTask } = renderDetail(); + const input = screen.getByPlaceholderText("Add task…"); + await userEvent.type(input, " {Enter}"); + expect(onAddTask).not.toHaveBeenCalled(); + }); + + it("Escape closes the card", async () => { + const { onClose } = renderDetail(); + await userEvent.keyboard("{Escape}"); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/src/components/PigDetail.tsx b/src/components/PigDetail.tsx index f2c0220..ed1cb14 100644 --- a/src/components/PigDetail.tsx +++ b/src/components/PigDetail.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { PIG_SIZE } from "../hooks/usePigMovement"; import type { Focus } from "../types/focus"; @@ -10,9 +10,10 @@ export interface PigDetailProps { readonly viewportH: number; readonly onClose: () => void; readonly onClearTask: (index: number) => void; + readonly onAddTask: (text: string) => void; } -const CARD_W = 210; +const CARD_W = 340; const CARD_OFFSET_X = PIG_SIZE + 8; export function PigDetail({ @@ -23,7 +24,10 @@ export function PigDetail({ viewportH, onClose, onClearTask, + onAddTask, }: PigDetailProps) { + const [taskInput, setTaskInput] = useState(""); + useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); @@ -67,6 +71,18 @@ export function PigDetail({ ))} )} + setTaskInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && taskInput.trim()) { + onAddTask(taskInput.trim()); + setTaskInput(""); + } + }} + /> ); diff --git a/src/styles.css b/src/styles.css index f266b3a..17e366e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -429,10 +429,11 @@ body, .pig-detail { position: absolute; pointer-events: auto; - background: rgba(28, 28, 30, 0.96); + background: rgba(20, 20, 20, 1); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 10px; - padding: 10px 12px; + padding: 16px; + min-width: 340px; color: #f5f5f7; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5); @@ -453,11 +454,13 @@ body, .pig-detail-tasks { list-style: none; - margin: 0; + margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 4px; + max-height: 240px; + overflow-y: auto; } .pig-detail-task { @@ -487,3 +490,24 @@ body, .pig-detail-task-clear:hover { opacity: 1; } + +.pig-detail-add-task { + width: 100%; + margin-top: 10px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + color: #f5f5f7; + font-size: 12px; + padding: 5px 8px; + outline: none; + box-sizing: border-box; +} + +.pig-detail-add-task::placeholder { + opacity: 0.45; +} + +.pig-detail-add-task:focus { + border-color: rgba(255, 255, 255, 0.3); +}