Replaces the single-sport native <select> with a popover (desktop) and bottom sheet (mobile) checkbox multi-select. No sports selected shows all participants; selecting one or more narrows the list. Trigger label adapts to reflect current filter state and a Reset button appears when filters are active. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
7.7 KiB
TypeScript
206 lines
7.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
|
|
|
|
const defaultProps = {
|
|
participants: [
|
|
{ id: "1", name: "Player A", sport: { id: "s1", name: "NFL" } },
|
|
{ id: "2", name: "Player B", sport: { id: "s2", name: "NBA" } },
|
|
{ id: "3", name: "Player C", sport: { id: "s1", name: "NFL" } },
|
|
],
|
|
searchQuery: "",
|
|
sportFilters: [] as string[],
|
|
hideDrafted: false,
|
|
hideIneligible: false,
|
|
hideCompletedSports: false,
|
|
uniqueSports: ["NBA", "NFL"],
|
|
draftedParticipantIds: new Set<string>(),
|
|
queue: [] as Array<{ id: string; participantId: string }>,
|
|
eligibility: null,
|
|
canPick: false,
|
|
hasTeam: false,
|
|
onSearchChange: vi.fn(),
|
|
onSportFiltersChange: vi.fn(),
|
|
onHideDraftedChange: vi.fn(),
|
|
onHideIneligibleChange: vi.fn(),
|
|
onHideCompletedSportsChange: vi.fn(),
|
|
onMakePick: vi.fn(),
|
|
onAddToQueue: vi.fn(),
|
|
onRemoveFromQueue: vi.fn(),
|
|
};
|
|
|
|
// Both the mobile Sheet trigger and desktop Popover trigger are rendered in jsdom
|
|
// (CSS breakpoints are not applied). Click the first one to open the Sheet.
|
|
let user: ReturnType<typeof userEvent.setup>;
|
|
|
|
function clickFirstSportFilterTrigger(name: string | RegExp) {
|
|
return user.click(screen.getAllByRole("button", { name })[0]);
|
|
}
|
|
|
|
describe("AvailableParticipantsSection", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
user = userEvent.setup();
|
|
});
|
|
|
|
describe("Rendering", () => {
|
|
it("renders all participants by name", () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
expect(screen.getAllByText("Player A").length).toBeGreaterThan(0);
|
|
expect(screen.getAllByText("Player B").length).toBeGreaterThan(0);
|
|
expect(screen.getAllByText("Player C").length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("renders search input", () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
expect(
|
|
screen.getByPlaceholderText("Search participants...")
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the Show Drafted toggle", () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
expect(screen.getByText("Show Drafted")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders Show Ineligible and Show drafted sports toggles when hasTeam=true", () => {
|
|
render(
|
|
<AvailableParticipantsSection
|
|
{...defaultProps}
|
|
hasTeam={true}
|
|
eligibility={{ eligibleSportIds: new Set(["s1"]), ineligibleReasons: {} }}
|
|
/>
|
|
);
|
|
expect(screen.getByText("Show Ineligible")).toBeInTheDocument();
|
|
expect(screen.getByText("Show drafted sports")).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not render Show Ineligible when hasTeam=false", () => {
|
|
render(
|
|
<AvailableParticipantsSection
|
|
{...defaultProps}
|
|
hasTeam={false}
|
|
eligibility={{ eligibleSportIds: new Set(), ineligibleReasons: {} }}
|
|
/>
|
|
);
|
|
expect(screen.queryByText("Show Ineligible")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders empty state when no participants", () => {
|
|
render(
|
|
<AvailableParticipantsSection {...defaultProps} participants={[]} />
|
|
);
|
|
expect(screen.getAllByText("No participants found.").length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("Sport Filter - trigger label", () => {
|
|
it('shows "All Sports" in aria-label when no filters selected', () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
// Both mobile and desktop triggers share the same label
|
|
const triggers = screen.getAllByRole("button", {
|
|
name: "Filter by sport: All Sports",
|
|
});
|
|
expect(triggers.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("shows sport name in aria-label when exactly one filter selected", () => {
|
|
render(
|
|
<AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
|
|
);
|
|
const triggers = screen.getAllByRole("button", {
|
|
name: "Filter by sport: NFL",
|
|
});
|
|
expect(triggers.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("shows count in aria-label when multiple filters selected", () => {
|
|
render(
|
|
<AvailableParticipantsSection
|
|
{...defaultProps}
|
|
sportFilters={["NFL", "NBA"]}
|
|
/>
|
|
);
|
|
const triggers = screen.getAllByRole("button", {
|
|
name: "Filter by sport: 2 sports selected",
|
|
});
|
|
expect(triggers.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("does not render active-filter badges", () => {
|
|
render(
|
|
<AvailableParticipantsSection
|
|
{...defaultProps}
|
|
sportFilters={["NFL", "NBA"]}
|
|
/>
|
|
);
|
|
expect(screen.queryByLabelText(/Remove .* filter/)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Sport Filter - sheet/popover", () => {
|
|
it("opens and shows checkboxes for each sport", async () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
|
const dialog = screen.getByRole("dialog");
|
|
expect(within(dialog).getByText("NBA")).toBeInTheDocument();
|
|
expect(within(dialog).getByText("NFL")).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onSportFiltersChange with sport added when checkbox clicked", async () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
|
const dialog = screen.getByRole("dialog");
|
|
await user.click(within(dialog).getByLabelText("NFL"));
|
|
expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith(["NFL"]);
|
|
});
|
|
|
|
it("calls onSportFiltersChange with sport removed when unchecked", async () => {
|
|
render(
|
|
<AvailableParticipantsSection
|
|
{...defaultProps}
|
|
sportFilters={["NFL", "NBA"]}
|
|
/>
|
|
);
|
|
await clickFirstSportFilterTrigger("Filter by sport: 2 sports selected");
|
|
const dialog = screen.getByRole("dialog");
|
|
await user.click(within(dialog).getByLabelText("NFL"));
|
|
expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith(["NBA"]);
|
|
});
|
|
|
|
it('does not show "Reset" when no filters active', async () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
|
const dialog = screen.getByRole("dialog");
|
|
expect(within(dialog).queryByText("Reset")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "Reset" button when filters exist', async () => {
|
|
render(
|
|
<AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
|
|
);
|
|
await clickFirstSportFilterTrigger("Filter by sport: NFL");
|
|
const dialog = screen.getByRole("dialog");
|
|
expect(within(dialog).getByText("Reset")).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onSportFiltersChange([]) when "Reset" clicked', async () => {
|
|
render(
|
|
<AvailableParticipantsSection {...defaultProps} sportFilters={["NFL"]} />
|
|
);
|
|
await clickFirstSportFilterTrigger("Filter by sport: NFL");
|
|
const dialog = screen.getByRole("dialog");
|
|
await user.click(within(dialog).getByText("Reset"));
|
|
expect(defaultProps.onSportFiltersChange).toHaveBeenCalledWith([]);
|
|
});
|
|
|
|
it("Done button closes the sheet", async () => {
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
await user.click(screen.getByRole("button", { name: "Done" }));
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|