feat(bd-318): implement QueueView container with filtering and batch support

QueueView now supports:
- Filtering items via CommandPalette (Cmd+K)
- Hide snoozed items by default (showSnoozed prop)
- Show snooze count indicator when items are hidden
- Support batch mode entry for sections with 2+ items
- Filter by type prop for programmatic filtering

Added snoozedUntil field to FocusItem type and updated fixtures.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 11:00:15 -05:00
parent d7056cc86f
commit d4b8a4baea
4 changed files with 541 additions and 98 deletions

View File

@@ -1,8 +1,9 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueueView } from "@/components/QueueView";
import { useFocusStore } from "@/stores/focus-store";
import { useBatchStore } from "@/stores/batch-store";
import { makeFocusItem } from "../helpers/fixtures";
describe("QueueView", () => {
@@ -13,6 +14,14 @@ describe("QueueView", () => {
isLoading: false,
error: null,
});
useBatchStore.setState({
isActive: false,
batchLabel: "",
items: [],
statuses: [],
currentIndex: 0,
startedAt: null,
});
});
it("shows empty state when no items", () => {
@@ -84,8 +93,9 @@ describe("QueueView", () => {
expect(screen.getByText("Queued item")).toBeInTheDocument();
});
it("calls onSetFocus when an item is clicked", async () => {
it("calls onSetFocus and switches to focus when an item is clicked", async () => {
const onSetFocus = vi.fn();
const onSwitchToFocus = vi.fn();
const user = userEvent.setup();
useFocusStore.setState({
@@ -95,10 +105,13 @@ describe("QueueView", () => {
],
});
render(<QueueView onSetFocus={onSetFocus} onSwitchToFocus={vi.fn()} />);
render(
<QueueView onSetFocus={onSetFocus} onSwitchToFocus={onSwitchToFocus} />
);
await user.click(screen.getByText("Click me"));
expect(onSetFocus).toHaveBeenCalledWith("target");
expect(onSwitchToFocus).toHaveBeenCalled();
});
it("marks the current focus item visually", () => {
@@ -113,4 +126,325 @@ describe("QueueView", () => {
expect(container.querySelector("[data-focused='true']")).toBeTruthy();
});
// -- Snoozed items filtering --
describe("snoozed items", () => {
it("hides snoozed items by default", () => {
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
useFocusStore.setState({
current: makeFocusItem({ id: "active", title: "Active item" }),
queue: [
makeFocusItem({
id: "snoozed",
type: "issue",
title: "Snoozed item",
snoozedUntil: future,
}),
makeFocusItem({
id: "visible",
type: "issue",
title: "Visible item",
snoozedUntil: null,
}),
],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
expect(screen.queryByText("Snoozed item")).not.toBeInTheDocument();
expect(screen.getByText("Visible item")).toBeInTheDocument();
});
it("shows snoozed items when showSnoozed is true", () => {
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
useFocusStore.setState({
current: makeFocusItem({ id: "active", title: "Active item" }),
queue: [
makeFocusItem({
id: "snoozed",
type: "issue",
title: "Snoozed item",
snoozedUntil: future,
}),
],
});
render(
<QueueView
onSetFocus={vi.fn()}
onSwitchToFocus={vi.fn()}
showSnoozed={true}
/>
);
expect(screen.getByText("Snoozed item")).toBeInTheDocument();
});
it("shows items with expired snooze time", () => {
const past = new Date(Date.now() - 60 * 60 * 1000).toISOString();
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({
id: "expired",
type: "issue",
title: "Expired snooze",
snoozedUntil: past,
}),
],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
expect(screen.getByText("Expired snooze")).toBeInTheDocument();
});
it("shows snooze count indicator when items are hidden", () => {
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
useFocusStore.setState({
current: makeFocusItem({ id: "active", title: "Active item" }),
queue: [
makeFocusItem({
id: "snoozed1",
type: "issue",
title: "Snoozed 1",
snoozedUntil: future,
}),
makeFocusItem({
id: "snoozed2",
type: "mr_review",
title: "Snoozed 2",
snoozedUntil: future,
}),
],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
expect(screen.getByText(/2 snoozed/i)).toBeInTheDocument();
});
});
// -- Filtering via type --
describe("filtering", () => {
it("filters items by type when filter is applied", () => {
useFocusStore.setState({
current: makeFocusItem({ id: "current", type: "mr_review" }),
queue: [
makeFocusItem({ id: "r1", type: "mr_review", title: "Review 1" }),
makeFocusItem({ id: "i1", type: "issue", title: "Issue 1" }),
makeFocusItem({ id: "m1", type: "manual", title: "Task 1" }),
],
});
render(
<QueueView
onSetFocus={vi.fn()}
onSwitchToFocus={vi.fn()}
filterType="issue"
/>
);
expect(screen.queryByText("Review 1")).not.toBeInTheDocument();
expect(screen.getByText("Issue 1")).toBeInTheDocument();
expect(screen.queryByText("Task 1")).not.toBeInTheDocument();
});
it("shows all types when no filter is applied", () => {
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({ id: "r1", type: "mr_review", title: "Review 1" }),
makeFocusItem({ id: "i1", type: "issue", title: "Issue 1" }),
],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
expect(screen.getByText("Review 1")).toBeInTheDocument();
expect(screen.getByText("Issue 1")).toBeInTheDocument();
});
});
// -- Batch mode entry --
describe("batch mode", () => {
it("shows batch button for sections with multiple items", () => {
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({ id: "r1", type: "mr_review", title: "Review 1" }),
makeFocusItem({ id: "r2", type: "mr_review", title: "Review 2" }),
makeFocusItem({ id: "r3", type: "mr_review", title: "Review 3" }),
],
});
render(
<QueueView
onSetFocus={vi.fn()}
onSwitchToFocus={vi.fn()}
onStartBatch={vi.fn()}
/>
);
const batchButton = screen.getByRole("button", { name: /batch/i });
expect(batchButton).toBeInTheDocument();
});
it("does not show batch button for sections with single item", () => {
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({ id: "r1", type: "mr_review", title: "Review 1" }),
makeFocusItem({ id: "i1", type: "issue", title: "Issue 1" }),
],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
// Neither section has multiple items
expect(
screen.queryByRole("button", { name: /batch/i })
).not.toBeInTheDocument();
});
it("calls onStartBatch with section items when batch button clicked", async () => {
const onStartBatch = vi.fn();
const user = userEvent.setup();
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({ id: "r1", type: "mr_review", title: "Review 1" }),
makeFocusItem({ id: "r2", type: "mr_review", title: "Review 2" }),
],
});
render(
<QueueView
onSetFocus={vi.fn()}
onSwitchToFocus={vi.fn()}
onStartBatch={onStartBatch}
/>
);
const batchButton = screen.getByRole("button", { name: /batch/i });
await user.click(batchButton);
expect(onStartBatch).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ id: "r1" }),
expect.objectContaining({ id: "r2" }),
]),
"REVIEWS"
);
});
});
// -- Command palette integration --
describe("command palette", () => {
it("opens command palette on Cmd+K", async () => {
const user = userEvent.setup();
useFocusStore.setState({
current: makeFocusItem({ id: "current" }),
queue: [],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
await user.keyboard("{Meta>}k{/Meta}");
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
it("filters items when command is selected", async () => {
const user = userEvent.setup();
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({ id: "r1", type: "mr_review", title: "Review 1" }),
makeFocusItem({ id: "i1", type: "issue", title: "Issue 1" }),
],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
// Open palette
await user.keyboard("{Meta>}k{/Meta}");
// Type filter command prefix to enter command mode
const input = screen.getByRole("textbox");
await user.type(input, "type:");
// Click the type:issue option directly
const issueOption = screen.getByRole("option", { name: /type:issue/i });
await user.click(issueOption);
// Should only show issues
expect(screen.queryByText("Review 1")).not.toBeInTheDocument();
expect(screen.getByText("Issue 1")).toBeInTheDocument();
});
});
// -- Header actions --
describe("header", () => {
it("shows Back to Focus button", () => {
useFocusStore.setState({
current: makeFocusItem({ id: "current" }),
queue: [],
});
render(<QueueView onSetFocus={vi.fn()} onSwitchToFocus={vi.fn()} />);
expect(
screen.getByRole("button", { name: /back to focus/i })
).toBeInTheDocument();
});
it("calls onSwitchToFocus when Back to Focus clicked", async () => {
const onSwitchToFocus = vi.fn();
const user = userEvent.setup();
useFocusStore.setState({
current: makeFocusItem({ id: "current" }),
queue: [],
});
render(
<QueueView onSetFocus={vi.fn()} onSwitchToFocus={onSwitchToFocus} />
);
await user.click(screen.getByRole("button", { name: /back to focus/i }));
expect(onSwitchToFocus).toHaveBeenCalled();
});
it("shows filter indicator when filter is active", () => {
useFocusStore.setState({
current: null,
queue: [
makeFocusItem({ id: "i1", type: "issue", title: "Issue 1" }),
],
});
render(
<QueueView
onSetFocus={vi.fn()}
onSwitchToFocus={vi.fn()}
filterType="issue"
/>
);
expect(screen.getByText(/filtered/i)).toBeInTheDocument();
});
});
});

View File

@@ -4,7 +4,7 @@
* Centralized here to avoid duplication across test files.
*/
import type { FocusItem } from "@/lib/types";
import type { FocusItem, InboxItem } from "@/lib/types";
/** Create a FocusItem with sensible defaults, overridable per field. */
export function makeFocusItem(
@@ -20,6 +20,23 @@ export function makeFocusItem(
updatedAt: new Date().toISOString(),
contextQuote: null,
requestedBy: null,
snoozedUntil: null,
...overrides,
};
}
/** Create an InboxItem with sensible defaults, overridable per field. */
export function makeInboxItem(
overrides: Partial<InboxItem> = {}
): InboxItem {
return {
id: "inbox-item-1",
title: "You were mentioned in #312",
type: "mention",
triaged: false,
createdAt: new Date().toISOString(),
snippet: "@user can you look at this?",
actor: "alice",
...overrides,
};
}