feat(FN-970): add mission badge to TaskCard with navigation to MissionManager

- Add mission badge UI to TaskCard showing linked mission name with colored indicator
- Wire up onOpenMission prop through App → Board → Column → WorktreeGroup → TaskCard
- Add CSS styles for mission badge with hover and click states
- Update MissionManager with openMission callback support for navigation from cards
- Add comprehensive tests for mission badge rendering, click behavior, and edge cases
This commit is contained in:
gsxdsm
2026-04-05 12:03:50 -07:00
parent f78845c2cd
commit 64dbbdd5ba
9 changed files with 165 additions and 6 deletions

View File

@@ -43,6 +43,7 @@ vi.mock("lucide-react", () => ({
Maximize2: ({ size }: { size?: number }) => <span data-testid="maximize-icon"></span>,
GitPullRequest: ({ size }: { size?: number }) => <span data-testid="git-pr-icon">🔀</span>,
CircleDot: ({ size }: { size?: number }) => <span data-testid="circle-dot-icon"></span>,
Target: ({ size }: { size?: number }) => <span data-testid="target-icon">🎯</span>,
}));
beforeEach(() => {
@@ -3226,3 +3227,77 @@ describe("TaskCard files-changed in done column", () => {
expect(screen.getByText("3 files changed")).toBeInTheDocument();
});
});
describe("TaskCard mission badge", () => {
const createTask = (overrides: Partial<Task> = {}): Task => ({
id: "FN-001",
description: "Test task",
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
columnMovedAt: "2026-01-01T00:00:00Z",
...overrides,
} as Task);
it("renders mission badge when task has missionId", () => {
const task = createTask({ missionId: "MSN-001" });
render(<TaskCard task={task} onOpenDetail={vi.fn()} addToast={vi.fn()} />);
const badge = screen.getByTitle("Mission: MSN-001");
expect(badge).toBeInTheDocument();
expect(badge).toHaveClass("card-mission-badge");
expect(badge).toHaveTextContent("MSN-001");
});
it("does not render mission badge when task has no missionId", () => {
const task = createTask();
render(<TaskCard task={task} onOpenDetail={vi.fn()} addToast={vi.fn()} />);
expect(screen.queryByTitle(/Mission:/)).not.toBeInTheDocument();
expect(screen.queryByTestId("target-icon")).not.toBeInTheDocument();
});
it("calls onOpenMission when mission badge is clicked", async () => {
const onOpenMission = vi.fn();
const task = createTask({ missionId: "MSN-042" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={vi.fn()}
onOpenMission={onOpenMission}
/>
);
const badge = screen.getByTitle("Mission: MSN-042");
await userEvent.click(badge);
expect(onOpenMission).toHaveBeenCalledOnce();
expect(onOpenMission).toHaveBeenCalledWith("MSN-042");
});
it("stops propagation when mission badge is clicked", async () => {
const onOpenDetail = vi.fn();
const onOpenMission = vi.fn();
const task = createTask({ missionId: "MSN-001" });
render(
<TaskCard
task={task}
onOpenDetail={onOpenDetail}
addToast={vi.fn()}
onOpenMission={onOpenMission}
/>
);
const badge = screen.getByTitle("Mission: MSN-001");
await userEvent.click(badge);
// onOpenDetail should NOT be called since click is stopped
expect(onOpenDetail).not.toHaveBeenCalled();
expect(onOpenMission).toHaveBeenCalledWith("MSN-001");
});
});