feat: add TrayPopover component for menu bar quick access
Shows THE ONE THING with: - Focus item title, type badge, project, and age - Quick actions: Start, Defer (1h), Skip - Queue and inbox counts - Link to open full window - Empty state when nothing focused Includes 18 tests covering all states and interactions. bd-wlg Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
198
tests/components/TrayPopover.test.tsx
Normal file
198
tests/components/TrayPopover.test.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { TrayPopover } from "@/components/TrayPopover";
|
||||
import { makeFocusItem } from "../helpers/fixtures";
|
||||
|
||||
describe("TrayPopover", () => {
|
||||
describe("with focus item", () => {
|
||||
it("shows 'THE ONE THING' header", () => {
|
||||
const item = makeFocusItem({ title: "Review MR !847" });
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText("THE ONE THING")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows focus item title", () => {
|
||||
const item = makeFocusItem({ title: "Fix auth bug" });
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText("Fix auth bug")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows project path", () => {
|
||||
const item = makeFocusItem({ project: "platform/core" });
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText(/platform\/core/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows type badge", () => {
|
||||
const item = makeFocusItem({ type: "mr_review" });
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText(/review/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Start button", () => {
|
||||
const item = makeFocusItem();
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /start/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Defer button", () => {
|
||||
const item = makeFocusItem();
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /defer/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Skip button", () => {
|
||||
const item = makeFocusItem();
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /skip/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty state", () => {
|
||||
it("shows empty message when no focus item", () => {
|
||||
render(<TrayPopover focusItem={null} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText(/nothing focused/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show action buttons when empty", () => {
|
||||
render(<TrayPopover focusItem={null} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /start/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /defer/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /skip/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("counts", () => {
|
||||
it("shows queue count", () => {
|
||||
render(<TrayPopover focusItem={null} queueCount={4} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText("Queue: 4")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows inbox count", () => {
|
||||
render(<TrayPopover focusItem={null} queueCount={0} inboxCount={3} />);
|
||||
|
||||
expect(screen.getByText("Inbox: 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows both counts", () => {
|
||||
render(<TrayPopover focusItem={null} queueCount={5} inboxCount={2} />);
|
||||
|
||||
expect(screen.getByText("Queue: 5")).toBeInTheDocument();
|
||||
expect(screen.getByText("Inbox: 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("full window link", () => {
|
||||
it("shows keyboard shortcut hint", () => {
|
||||
render(<TrayPopover focusItem={null} queueCount={0} inboxCount={0} />);
|
||||
|
||||
// Should show some reference to opening full window
|
||||
expect(screen.getByText(/full window/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("actions", () => {
|
||||
it("calls onStart when Start clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onStart = vi.fn();
|
||||
const item = makeFocusItem();
|
||||
|
||||
render(
|
||||
<TrayPopover
|
||||
focusItem={item}
|
||||
queueCount={0}
|
||||
inboxCount={0}
|
||||
onStart={onStart}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /start/i }));
|
||||
expect(onStart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onDefer with duration when Defer clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDefer = vi.fn();
|
||||
const item = makeFocusItem();
|
||||
|
||||
render(
|
||||
<TrayPopover
|
||||
focusItem={item}
|
||||
queueCount={0}
|
||||
inboxCount={0}
|
||||
onDefer={onDefer}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /defer/i }));
|
||||
expect(onDefer).toHaveBeenCalledWith("1h");
|
||||
});
|
||||
|
||||
it("calls onSkip when Skip clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSkip = vi.fn();
|
||||
const item = makeFocusItem();
|
||||
|
||||
render(
|
||||
<TrayPopover
|
||||
focusItem={item}
|
||||
queueCount={0}
|
||||
inboxCount={0}
|
||||
onSkip={onSkip}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /skip/i }));
|
||||
expect(onSkip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onOpenFull when full window link clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenFull = vi.fn();
|
||||
|
||||
render(
|
||||
<TrayPopover
|
||||
focusItem={null}
|
||||
queueCount={0}
|
||||
inboxCount={0}
|
||||
onOpenFull={onOpenFull}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText(/full window/i));
|
||||
expect(onOpenFull).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relative time", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-02-26T12:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("shows age for items", () => {
|
||||
const item = makeFocusItem({
|
||||
updatedAt: new Date("2026-02-24T12:00:00Z").toISOString(), // 2 days ago
|
||||
});
|
||||
|
||||
render(<TrayPopover focusItem={item} queueCount={0} inboxCount={0} />);
|
||||
|
||||
expect(screen.getByText(/2d/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user