brackt/app/components/__tests__/AvailableParticipantsSection.test.tsx

510 lines
17 KiB
TypeScript
Raw Normal View History

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";
New design (#309) * Redesign home page with new layout and component system - Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack - LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar - MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader - UpcomingEventsCard: vertical timeline with grouped multi-league events - Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants - Button default variant updated to green→cyan gradient - Navbar: plain nav links with gradient hover, support/admin icon buttons - Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements - Storybook stories for all new components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Responsive league row layout and mobile polish - League rows stack avatar+name on top, stats full-width below on mobile - Stats spread to right side on sm+ screens with border separator on mobile - Tighter padding on mobile (px-3/py-3), full padding on sm+ - Card headers and content use px-3 sm:px-6 to reduce mobile gutters - Two-column home layout deferred to lg breakpoint (tablet gets stacked) - Active leagues sorted by completion percentage descending - Default rank 1 / 0 points for active leagues with no scoring events yet - Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators - Remove dead StatDivider className prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve claude file. * Add StandingsPreview card component with podium row styling - New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update components on league homepage. * Finish up league page styling. * Work on standings page. * Add story for RecentScoresCard * Update Point Progression Chart. * Sort point progression legend by ranking and add team links to standings rows * Fix standings discrepancy on change. * Create draft cell component. * Update draft board page * Draft room improvements. * Update some draft room styling. * Fix context menu missing. * Move tab navigation and autodraft to header row, narrow sidebar * Virtualize available participants list, memoize draft room props Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update draft room UI. * More draft room fixes. * Draft room tweaks. * Fix Rosters page. * Queue Section fixes. * Mobile Draft fixes. * Fix draft board page. * Create bracket look. * Bracket work. * Finish bracket page. * Homepage initial styling * homepage copy * Add privacy policy. Fixes #88. * how to play copy * rules copy * Fix brackets on homepage. * Add footer to website. * Glow on dots. * Landing page copy. * Fix sidebar. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
vi.mock("@tanstack/react-virtual", () => ({
useVirtualizer: ({
count,
estimateSize,
}: {
count: number;
estimateSize: (index: number) => number;
New design (#309) * Redesign home page with new layout and component system - Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack - LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar - MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader - UpcomingEventsCard: vertical timeline with grouped multi-league events - Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants - Button default variant updated to green→cyan gradient - Navbar: plain nav links with gradient hover, support/admin icon buttons - Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements - Storybook stories for all new components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Responsive league row layout and mobile polish - League rows stack avatar+name on top, stats full-width below on mobile - Stats spread to right side on sm+ screens with border separator on mobile - Tighter padding on mobile (px-3/py-3), full padding on sm+ - Card headers and content use px-3 sm:px-6 to reduce mobile gutters - Two-column home layout deferred to lg breakpoint (tablet gets stacked) - Active leagues sorted by completion percentage descending - Default rank 1 / 0 points for active leagues with no scoring events yet - Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators - Remove dead StatDivider className prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve claude file. * Add StandingsPreview card component with podium row styling - New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update components on league homepage. * Finish up league page styling. * Work on standings page. * Add story for RecentScoresCard * Update Point Progression Chart. * Sort point progression legend by ranking and add team links to standings rows * Fix standings discrepancy on change. * Create draft cell component. * Update draft board page * Draft room improvements. * Update some draft room styling. * Fix context menu missing. * Move tab navigation and autodraft to header row, narrow sidebar * Virtualize available participants list, memoize draft room props Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update draft room UI. * More draft room fixes. * Draft room tweaks. * Fix Rosters page. * Queue Section fixes. * Mobile Draft fixes. * Fix draft board page. * Create bracket look. * Bracket work. * Finish bracket page. * Homepage initial styling * homepage copy * Add privacy policy. Fixes #88. * how to play copy * rules copy * Fix brackets on homepage. * Add footer to website. * Glow on dots. * Landing page copy. * Fix sidebar. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
}) => {
const size = estimateSize(0);
New design (#309) * Redesign home page with new layout and component system - Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack - LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar - MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader - UpcomingEventsCard: vertical timeline with grouped multi-league events - Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants - Button default variant updated to green→cyan gradient - Navbar: plain nav links with gradient hover, support/admin icon buttons - Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements - Storybook stories for all new components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Responsive league row layout and mobile polish - League rows stack avatar+name on top, stats full-width below on mobile - Stats spread to right side on sm+ screens with border separator on mobile - Tighter padding on mobile (px-3/py-3), full padding on sm+ - Card headers and content use px-3 sm:px-6 to reduce mobile gutters - Two-column home layout deferred to lg breakpoint (tablet gets stacked) - Active leagues sorted by completion percentage descending - Default rank 1 / 0 points for active leagues with no scoring events yet - Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators - Remove dead StatDivider className prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve claude file. * Add StandingsPreview card component with podium row styling - New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update components on league homepage. * Finish up league page styling. * Work on standings page. * Add story for RecentScoresCard * Update Point Progression Chart. * Sort point progression legend by ranking and add team links to standings rows * Fix standings discrepancy on change. * Create draft cell component. * Update draft board page * Draft room improvements. * Update some draft room styling. * Fix context menu missing. * Move tab navigation and autodraft to header row, narrow sidebar * Virtualize available participants list, memoize draft room props Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update draft room UI. * More draft room fixes. * Draft room tweaks. * Fix Rosters page. * Queue Section fixes. * Mobile Draft fixes. * Fix draft board page. * Create bracket look. * Bracket work. * Finish bracket page. * Homepage initial styling * homepage copy * Add privacy policy. Fixes #88. * how to play copy * rules copy * Fix brackets on homepage. * Add footer to website. * Glow on dots. * Landing page copy. * Fix sidebar. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
const items = Array.from({ length: count }, (_, i) => ({
index: i,
start: i * size,
size,
key: i,
}));
return {
getTotalSize: () => count * size,
getVirtualItems: () => items,
measureElement: vi.fn(),
};
},
}));
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"],
userDraftedSportNames: new Set<string>(),
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(),
watchedParticipantIds: new Set<string>(),
onToggleWatchlist: vi.fn(),
showOnlyWatched: false,
onShowOnlyWatchedChange: vi.fn(),
New design (#309) * Redesign home page with new layout and component system - Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack - LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar - MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader - UpcomingEventsCard: vertical timeline with grouped multi-league events - Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants - Button default variant updated to green→cyan gradient - Navbar: plain nav links with gradient hover, support/admin icon buttons - Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements - Storybook stories for all new components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Responsive league row layout and mobile polish - League rows stack avatar+name on top, stats full-width below on mobile - Stats spread to right side on sm+ screens with border separator on mobile - Tighter padding on mobile (px-3/py-3), full padding on sm+ - Card headers and content use px-3 sm:px-6 to reduce mobile gutters - Two-column home layout deferred to lg breakpoint (tablet gets stacked) - Active leagues sorted by completion percentage descending - Default rank 1 / 0 points for active leagues with no scoring events yet - Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators - Remove dead StatDivider className prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve claude file. * Add StandingsPreview card component with podium row styling - New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update components on league homepage. * Finish up league page styling. * Work on standings page. * Add story for RecentScoresCard * Update Point Progression Chart. * Sort point progression legend by ranking and add team links to standings rows * Fix standings discrepancy on change. * Create draft cell component. * Update draft board page * Draft room improvements. * Update some draft room styling. * Fix context menu missing. * Move tab navigation and autodraft to header row, narrow sidebar * Virtualize available participants list, memoize draft room props Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update draft room UI. * More draft room fixes. * Draft room tweaks. * Fix Rosters page. * Queue Section fixes. * Mobile Draft fixes. * Fix draft board page. * Create bracket look. * Bracket work. * Finish bracket page. * Homepage initial styling * homepage copy * Add privacy policy. Fixes #88. * how to play copy * rules copy * Fix brackets on homepage. * Add footer to website. * Glow on dots. * Landing page copy. * Fix sidebar. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
participantRanks: new Map([
["1", { overallRank: 1, sportRank: 1 }],
["2", { overallRank: 2, sportRank: 1 }],
["3", { overallRank: 3, sportRank: 2 }],
]),
};
// 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", async () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
eligibility={{ eligibleSportIds: new Set(["s1"]), ineligibleReasons: {} }}
/>
);
expect(screen.getByText("Show Ineligible")).toBeInTheDocument();
// Show drafted sports lives inside the sport filter dropdown
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
const dialog = screen.getByRole("dialog");
expect(within(dialog).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("Projected Pick Dividers", () => {
const manyParticipants = Array.from({ length: 30 }, (_, i) => ({
id: String(i + 1),
name: `Player ${i + 1}`,
sport: { id: "s1", name: "NFL" },
}));
const manyRanks = new Map(
Array.from({ length: 30 }, (_, i) => [
String(i + 1),
{ overallRank: i + 1, sportRank: i + 1 },
])
);
it("renders projected pick divider at correct position", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
projectedPicks={[
{ round: 3, picksFromNow: 10 },
{ round: 4, picksFromNow: 20 },
]}
/>
);
expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument();
expect(screen.getByText("Projected Round 4 Pick")).toBeInTheDocument();
});
it("does not render dividers when projectedPicks is undefined", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
/>
);
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
});
it("does not render dividers when projectedPicks is empty", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
projectedPicks={[]}
/>
);
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
});
it("skips dividers where picksFromNow exceeds list length", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
projectedPicks={[
{ round: 3, picksFromNow: 10 },
{ round: 10, picksFromNow: 100 },
]}
/>
);
expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument();
expect(screen.queryByText("Projected Round 10 Pick")).not.toBeInTheDocument();
});
it("skips dividers where picksFromNow is zero or negative", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
projectedPicks={[
{ round: 1, picksFromNow: 0 },
{ round: 2, picksFromNow: 5 },
]}
/>
);
expect(screen.queryByText("Projected Round 1 Pick")).not.toBeInTheDocument();
expect(screen.getByText("Projected Round 2 Pick")).toBeInTheDocument();
});
it("still renders all participants alongside dividers", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
/>
);
expect(screen.getAllByText("Player 1").length).toBeGreaterThan(0);
expect(screen.getAllByText("Player 10").length).toBeGreaterThan(0);
expect(screen.getAllByText("Player 30").length).toBeGreaterThan(0);
});
it("suppresses dividers when search query is active", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
searchQuery="Player"
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
/>
);
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
});
it("suppresses dividers when sport filter is active", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
sportFilters={["NFL"]}
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
/>
);
expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument();
});
it("suppresses dividers when hideDrafted is active", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
participants={manyParticipants}
participantRanks={manyRanks}
hideDrafted
projectedPicks={[{ round: 3, picksFromNow: 10 }]}
/>
);
expect(screen.queryByText(/Projected Round \d+ Pick/)).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([]) and onHideCompletedSportsChange(false) 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([]);
expect(defaultProps.onHideCompletedSportsChange).toHaveBeenCalledWith(false);
});
it("Popover trigger opens and can be closed", async () => {
render(<AvailableParticipantsSection {...defaultProps} />);
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
expect(screen.getByRole("dialog")).toBeInTheDocument();
await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
describe("Watchlist", () => {
it("does not render watchlist toggle when hasTeam is false", () => {
render(
<AvailableParticipantsSection {...defaultProps} hasTeam={false} />
);
expect(screen.queryByTitle("Add to watchlist")).not.toBeInTheDocument();
expect(screen.queryByTitle("Remove from watchlist")).not.toBeInTheDocument();
});
it("renders EyeOff icon for unwatched participants when hasTeam is true", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
watchedParticipantIds={new Set<string>()}
/>
);
const buttons = screen.getAllByTitle("Add to watchlist");
expect(buttons.length).toBeGreaterThan(0);
});
it("renders EyeOff icon for watched participants", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
watchedParticipantIds={new Set(["1"])}
/>
);
const buttons = screen.getAllByTitle("Remove from watchlist");
expect(buttons.length).toBeGreaterThan(0);
});
it("calls onToggleWatchlist when eye button clicked", async () => {
const onToggleWatchlist = vi.fn();
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
onToggleWatchlist={onToggleWatchlist}
watchedParticipantIds={new Set<string>()}
/>
);
const buttons = screen.getAllByTitle("Add to watchlist");
await user.click(buttons[0]);
expect(onToggleWatchlist).toHaveBeenCalledWith("1");
});
it("applies emerald highlight to watched participants", () => {
const { container } = render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
watchedParticipantIds={new Set(["1"])}
/>
);
const highlighted = container.querySelector(".bg-emerald-500\\/10");
expect(highlighted).toBeInTheDocument();
});
it("does not highlight unwatched participants", () => {
const { container } = render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
watchedParticipantIds={new Set<string>()}
/>
);
const highlighted = container.querySelector(".bg-emerald-500\\/10");
expect(highlighted).not.toBeInTheDocument();
});
it("shows Watched Only checkbox when hasTeam", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
/>
);
expect(screen.getByText("Watched Only")).toBeInTheDocument();
});
it("does not show Watched Only checkbox when hasTeam is false", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={false}
/>
);
expect(screen.queryByText("Watched Only")).not.toBeInTheDocument();
});
it("disables Watched Only checkbox when no participants are watched", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
watchedParticipantIds={new Set<string>()}
/>
);
expect(screen.getByLabelText("Watched Only")).toBeDisabled();
});
it("calls onShowOnlyWatchedChange when Watched Only checkbox clicked", async () => {
const onShowOnlyWatchedChange = vi.fn();
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
watchedParticipantIds={new Set(["1"])}
onShowOnlyWatchedChange={onShowOnlyWatchedChange}
/>
);
await user.click(screen.getByLabelText("Watched Only"));
expect(onShowOnlyWatchedChange).toHaveBeenCalledWith(true);
});
});
});