2026-03-02 00:29:54 -08:00
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
2026-06-10 04:25:48 +00:00
|
|
|
import { render, screen, within, fireEvent } from "@testing-library/react";
|
2026-03-02 00:29:54 -08:00
|
|
|
import userEvent from "@testing-library/user-event";
|
|
|
|
|
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
|
2026-05-04 21:54:14 -07:00
|
|
|
import type { DraftIneligibilityReason } from "~/lib/draft-eligibility";
|
2026-03-02 00:29:54 -08:00
|
|
|
|
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;
|
2026-04-24 09:32:11 -07:00
|
|
|
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
|
|
|
}) => {
|
2026-04-24 09:32:11 -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(),
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
2026-03-02 00:29:54 -08:00
|
|
|
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"],
|
2026-03-02 10:13:51 -08:00
|
|
|
userDraftedSportNames: new Set<string>(),
|
2026-03-02 00:29:54 -08:00
|
|
|
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(),
|
2026-04-29 11:49:26 -07:00
|
|
|
watchedParticipantIds: new Set<string>(),
|
|
|
|
|
onToggleWatchlist: vi.fn(),
|
|
|
|
|
showOnlyWatched: false,
|
|
|
|
|
onShowOnlyWatchedChange: vi.fn(),
|
2026-05-10 23:17:29 -07:00
|
|
|
pickAnimationSignal: null,
|
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 }],
|
|
|
|
|
]),
|
2026-03-02 00:29:54 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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>;
|
|
|
|
|
|
2026-05-04 21:54:14 -07:00
|
|
|
function createReason(
|
|
|
|
|
code: DraftIneligibilityReason["code"],
|
|
|
|
|
message: string
|
|
|
|
|
): DraftIneligibilityReason {
|
|
|
|
|
return { code, message };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 00:29:54 -08:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-10 04:25:48 +00:00
|
|
|
it("renders Show Ineligible and Show drafted sports toggles when hasTeam=true", () => {
|
2026-03-02 00:29:54 -08:00
|
|
|
render(
|
|
|
|
|
<AvailableParticipantsSection
|
|
|
|
|
{...defaultProps}
|
|
|
|
|
hasTeam={true}
|
|
|
|
|
eligibility={{ eligibleSportIds: new Set(["s1"]), ineligibleReasons: {} }}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
expect(screen.getByText("Show Ineligible")).toBeInTheDocument();
|
2026-06-10 04:25:48 +00:00
|
|
|
// Show drafted sports lives inside the sport filter dropdown.
|
|
|
|
|
// Use fireEvent instead of user.click to avoid Radix UI timer hang when
|
|
|
|
|
// hasTeam=true renders extra Checkbox components that stall userEvent's
|
|
|
|
|
// act() settlement loop.
|
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: "Filter by sport: All Sports" })[0]);
|
2026-03-02 10:13:51 -08:00
|
|
|
const dialog = screen.getByRole("dialog");
|
|
|
|
|
expect(within(dialog).getByText("Show drafted sports")).toBeInTheDocument();
|
2026-03-02 00:29:54 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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();
|
2026-04-24 09:32:11 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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();
|
2026-03-02 00:29:54 -08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-02 10:13:51 -08:00
|
|
|
it('calls onSportFiltersChange([]) and onHideCompletedSportsChange(false) when "Reset" clicked', async () => {
|
2026-03-02 00:29:54 -08:00
|
|
|
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([]);
|
2026-03-02 10:13:51 -08:00
|
|
|
expect(defaultProps.onHideCompletedSportsChange).toHaveBeenCalledWith(false);
|
2026-03-02 00:29:54 -08:00
|
|
|
});
|
|
|
|
|
|
2026-04-29 11:49:26 -07:00
|
|
|
it("Popover trigger opens and can be closed", async () => {
|
2026-03-02 00:29:54 -08:00
|
|
|
render(<AvailableParticipantsSection {...defaultProps} />);
|
|
|
|
|
await clickFirstSportFilterTrigger("Filter by sport: All Sports");
|
|
|
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
2026-04-29 11:49:26 -07:00
|
|
|
await user.keyboard("{Escape}");
|
2026-03-02 00:29:54 -08:00
|
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-29 11:49:26 -07:00
|
|
|
|
|
|
|
|
describe("Watchlist", () => {
|
|
|
|
|
it("does not render watchlist toggle when hasTeam is false", () => {
|
|
|
|
|
render(
|
|
|
|
|
<AvailableParticipantsSection {...defaultProps} hasTeam={false} />
|
|
|
|
|
);
|
fix: resolve all 48 WCAG 2.2 AA accessibility issues (#439)
* fix: resolve all 48 WCAG 2.2 AA accessibility issues
Critical fixes:
- Add aria-label to all unlabeled inputs/selects in draft dialogs (ParticipantSelectionDialog, TimeBankAdjustmentDialog, AvailableParticipantsSection)
- Add role="dialog" + aria-modal + focus trap to ConnectionOverlay and AuthRecoveryOverlay
- Add aria-live region and connection status announcement to ConnectionOverlay
Serious fixes:
- Add skip-to-content link in root.tsx with id="main-content" on <main>
- Add aria-label to UserMenu trigger button
- Add aria-describedby + role="alert" to all auth form error messages (login, register, onboarding, forgot-password, reset-password)
- Replace emoji column headers in StandingsTable with aria-label + aria-hidden spans
- Add aria-live="assertive" to "It's your turn" desktop and mobile on-clock indicators
- Add aria-live="polite" to draft room countdown timer
- Add pause button to SportTicker (WCAG 2.2.2); add aria-hidden to ticker content
- Fix Footer text contrast (changed from 28% to text-muted-foreground)
- Fix OvernightPauseSettings: add htmlFor/id pairs and role="radiogroup"+aria-checked to mode buttons
- Fix DraftSetupSection: replace broken htmlFor with aria-label on date picker button
- Add aria-label to PeopleSection owner and commissioner selects
- Add labels to ScoringPresetPicker score inputs; add role="radiogroup"+aria-checked to preset buttons
- Add role="radiogroup"+aria-checked to AutodraftSettings option buttons
- Add accessible names, aria-current="step", and <ol> list semantics to WizardStepper
Moderate fixes:
- Add aria-controls to RecentPicksFeed toggle button; wrap picks list in aria-live region
- Add role="tab"+aria-selected+aria-controls to mobile board sub-tabs + role="tabpanel"
- Add role="radiogroup"+aria-checked to TimerModeSelector
- Add aria-current="page" + aria-label to SettingsDesktopNav
- Add aria-label="Admin navigation" to admin sidebar nav
- Add scope="col" + <caption> to StandingsTable and ScoringTables
- Add ARIA table roles (role="table/rowgroup/row/columnheader/rowheader/cell") to DraftSummaryView CSS grid
Minor fixes:
- Add aria-hidden="true" to decorative trend icons in StandingsTable
- Add aria-hidden="true" to desktop column header labels row in AvailableParticipantsSection
- Replace title with aria-label on all icon-only buttons (watchlist, queue) in AvailableParticipantsSection
- Add aria-label to NotificationSettings switchOnly Switch
- Add prefers-reduced-motion check to SlotMachineHeadline JS animation
- Bump --muted-foreground from 55% to 62% opacity for improved contrast margin
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix code review findings from WCAG compliance pass
- Add Arrow key navigation + roving tabindex to all role=radiogroup
components (AutodraftSettings x2, TimerModeSelector,
OvernightPauseSettings, ScoringPresetPicker) per ARIA radio pattern
- Extract shared focus-trap logic into useFocusTrap hook; update
ConnectionOverlay and AuthRecoveryOverlay to use it
- Add tabIndex={-1} to ConnectionOverlay Card so focus can land in
spinner-only state (no interactive children)
- Replace aria-live on loading dots container with sr-only span so
status changes are announced by text content, not aria-label
- Remove contradictory aria-hidden+role=columnheader from
AvailableParticipantsSection visual-only header row
- Remove invalid scope="col" from div[role=columnheader] in
DraftSummaryView (scope is only valid on <th>)
- Remove redundant aria-label from ParticipantSelectionDialog sport
select (htmlFor label is sufficient)
- Change WizardStepper connector <li> to role=presentation
- Revert muted-foreground from 62% to 55% (original already passes
contrast; footer was fixed separately via text-muted-foreground)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix lint error and update tests for WCAG role changes
- Replace el! non-null assertion with optional chaining in useFocusTrap
- Update AutodraftSettings tests to query role="radio" instead of
role="button" (buttons have an explicit radio role since the WCAG pass)
- Update AvailableParticipantsSection watchlist tests to use
getByRole/getAllByRole instead of getByTitle/getAllByTitle (watchlist
buttons now use aria-label instead of title)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 20:11:38 -07:00
|
|
|
expect(screen.queryByRole('button', { name: "Add to watchlist" })).not.toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByRole('button', { name: "Remove from watchlist" })).not.toBeInTheDocument();
|
2026-04-29 11:49:26 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders EyeOff icon for unwatched participants when hasTeam is true", () => {
|
|
|
|
|
render(
|
|
|
|
|
<AvailableParticipantsSection
|
|
|
|
|
{...defaultProps}
|
|
|
|
|
hasTeam={true}
|
|
|
|
|
watchedParticipantIds={new Set<string>()}
|
|
|
|
|
/>
|
|
|
|
|
);
|
fix: resolve all 48 WCAG 2.2 AA accessibility issues (#439)
* fix: resolve all 48 WCAG 2.2 AA accessibility issues
Critical fixes:
- Add aria-label to all unlabeled inputs/selects in draft dialogs (ParticipantSelectionDialog, TimeBankAdjustmentDialog, AvailableParticipantsSection)
- Add role="dialog" + aria-modal + focus trap to ConnectionOverlay and AuthRecoveryOverlay
- Add aria-live region and connection status announcement to ConnectionOverlay
Serious fixes:
- Add skip-to-content link in root.tsx with id="main-content" on <main>
- Add aria-label to UserMenu trigger button
- Add aria-describedby + role="alert" to all auth form error messages (login, register, onboarding, forgot-password, reset-password)
- Replace emoji column headers in StandingsTable with aria-label + aria-hidden spans
- Add aria-live="assertive" to "It's your turn" desktop and mobile on-clock indicators
- Add aria-live="polite" to draft room countdown timer
- Add pause button to SportTicker (WCAG 2.2.2); add aria-hidden to ticker content
- Fix Footer text contrast (changed from 28% to text-muted-foreground)
- Fix OvernightPauseSettings: add htmlFor/id pairs and role="radiogroup"+aria-checked to mode buttons
- Fix DraftSetupSection: replace broken htmlFor with aria-label on date picker button
- Add aria-label to PeopleSection owner and commissioner selects
- Add labels to ScoringPresetPicker score inputs; add role="radiogroup"+aria-checked to preset buttons
- Add role="radiogroup"+aria-checked to AutodraftSettings option buttons
- Add accessible names, aria-current="step", and <ol> list semantics to WizardStepper
Moderate fixes:
- Add aria-controls to RecentPicksFeed toggle button; wrap picks list in aria-live region
- Add role="tab"+aria-selected+aria-controls to mobile board sub-tabs + role="tabpanel"
- Add role="radiogroup"+aria-checked to TimerModeSelector
- Add aria-current="page" + aria-label to SettingsDesktopNav
- Add aria-label="Admin navigation" to admin sidebar nav
- Add scope="col" + <caption> to StandingsTable and ScoringTables
- Add ARIA table roles (role="table/rowgroup/row/columnheader/rowheader/cell") to DraftSummaryView CSS grid
Minor fixes:
- Add aria-hidden="true" to decorative trend icons in StandingsTable
- Add aria-hidden="true" to desktop column header labels row in AvailableParticipantsSection
- Replace title with aria-label on all icon-only buttons (watchlist, queue) in AvailableParticipantsSection
- Add aria-label to NotificationSettings switchOnly Switch
- Add prefers-reduced-motion check to SlotMachineHeadline JS animation
- Bump --muted-foreground from 55% to 62% opacity for improved contrast margin
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix code review findings from WCAG compliance pass
- Add Arrow key navigation + roving tabindex to all role=radiogroup
components (AutodraftSettings x2, TimerModeSelector,
OvernightPauseSettings, ScoringPresetPicker) per ARIA radio pattern
- Extract shared focus-trap logic into useFocusTrap hook; update
ConnectionOverlay and AuthRecoveryOverlay to use it
- Add tabIndex={-1} to ConnectionOverlay Card so focus can land in
spinner-only state (no interactive children)
- Replace aria-live on loading dots container with sr-only span so
status changes are announced by text content, not aria-label
- Remove contradictory aria-hidden+role=columnheader from
AvailableParticipantsSection visual-only header row
- Remove invalid scope="col" from div[role=columnheader] in
DraftSummaryView (scope is only valid on <th>)
- Remove redundant aria-label from ParticipantSelectionDialog sport
select (htmlFor label is sufficient)
- Change WizardStepper connector <li> to role=presentation
- Revert muted-foreground from 62% to 55% (original already passes
contrast; footer was fixed separately via text-muted-foreground)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix lint error and update tests for WCAG role changes
- Replace el! non-null assertion with optional chaining in useFocusTrap
- Update AutodraftSettings tests to query role="radio" instead of
role="button" (buttons have an explicit radio role since the WCAG pass)
- Update AvailableParticipantsSection watchlist tests to use
getByRole/getAllByRole instead of getByTitle/getAllByTitle (watchlist
buttons now use aria-label instead of title)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 20:11:38 -07:00
|
|
|
const buttons = screen.getAllByRole('button', { name: "Add to watchlist" });
|
2026-04-29 11:49:26 -07:00
|
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders EyeOff icon for watched participants", () => {
|
|
|
|
|
render(
|
|
|
|
|
<AvailableParticipantsSection
|
|
|
|
|
{...defaultProps}
|
|
|
|
|
hasTeam={true}
|
|
|
|
|
watchedParticipantIds={new Set(["1"])}
|
|
|
|
|
/>
|
|
|
|
|
);
|
fix: resolve all 48 WCAG 2.2 AA accessibility issues (#439)
* fix: resolve all 48 WCAG 2.2 AA accessibility issues
Critical fixes:
- Add aria-label to all unlabeled inputs/selects in draft dialogs (ParticipantSelectionDialog, TimeBankAdjustmentDialog, AvailableParticipantsSection)
- Add role="dialog" + aria-modal + focus trap to ConnectionOverlay and AuthRecoveryOverlay
- Add aria-live region and connection status announcement to ConnectionOverlay
Serious fixes:
- Add skip-to-content link in root.tsx with id="main-content" on <main>
- Add aria-label to UserMenu trigger button
- Add aria-describedby + role="alert" to all auth form error messages (login, register, onboarding, forgot-password, reset-password)
- Replace emoji column headers in StandingsTable with aria-label + aria-hidden spans
- Add aria-live="assertive" to "It's your turn" desktop and mobile on-clock indicators
- Add aria-live="polite" to draft room countdown timer
- Add pause button to SportTicker (WCAG 2.2.2); add aria-hidden to ticker content
- Fix Footer text contrast (changed from 28% to text-muted-foreground)
- Fix OvernightPauseSettings: add htmlFor/id pairs and role="radiogroup"+aria-checked to mode buttons
- Fix DraftSetupSection: replace broken htmlFor with aria-label on date picker button
- Add aria-label to PeopleSection owner and commissioner selects
- Add labels to ScoringPresetPicker score inputs; add role="radiogroup"+aria-checked to preset buttons
- Add role="radiogroup"+aria-checked to AutodraftSettings option buttons
- Add accessible names, aria-current="step", and <ol> list semantics to WizardStepper
Moderate fixes:
- Add aria-controls to RecentPicksFeed toggle button; wrap picks list in aria-live region
- Add role="tab"+aria-selected+aria-controls to mobile board sub-tabs + role="tabpanel"
- Add role="radiogroup"+aria-checked to TimerModeSelector
- Add aria-current="page" + aria-label to SettingsDesktopNav
- Add aria-label="Admin navigation" to admin sidebar nav
- Add scope="col" + <caption> to StandingsTable and ScoringTables
- Add ARIA table roles (role="table/rowgroup/row/columnheader/rowheader/cell") to DraftSummaryView CSS grid
Minor fixes:
- Add aria-hidden="true" to decorative trend icons in StandingsTable
- Add aria-hidden="true" to desktop column header labels row in AvailableParticipantsSection
- Replace title with aria-label on all icon-only buttons (watchlist, queue) in AvailableParticipantsSection
- Add aria-label to NotificationSettings switchOnly Switch
- Add prefers-reduced-motion check to SlotMachineHeadline JS animation
- Bump --muted-foreground from 55% to 62% opacity for improved contrast margin
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix code review findings from WCAG compliance pass
- Add Arrow key navigation + roving tabindex to all role=radiogroup
components (AutodraftSettings x2, TimerModeSelector,
OvernightPauseSettings, ScoringPresetPicker) per ARIA radio pattern
- Extract shared focus-trap logic into useFocusTrap hook; update
ConnectionOverlay and AuthRecoveryOverlay to use it
- Add tabIndex={-1} to ConnectionOverlay Card so focus can land in
spinner-only state (no interactive children)
- Replace aria-live on loading dots container with sr-only span so
status changes are announced by text content, not aria-label
- Remove contradictory aria-hidden+role=columnheader from
AvailableParticipantsSection visual-only header row
- Remove invalid scope="col" from div[role=columnheader] in
DraftSummaryView (scope is only valid on <th>)
- Remove redundant aria-label from ParticipantSelectionDialog sport
select (htmlFor label is sufficient)
- Change WizardStepper connector <li> to role=presentation
- Revert muted-foreground from 62% to 55% (original already passes
contrast; footer was fixed separately via text-muted-foreground)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix lint error and update tests for WCAG role changes
- Replace el! non-null assertion with optional chaining in useFocusTrap
- Update AutodraftSettings tests to query role="radio" instead of
role="button" (buttons have an explicit radio role since the WCAG pass)
- Update AvailableParticipantsSection watchlist tests to use
getByRole/getAllByRole instead of getByTitle/getAllByTitle (watchlist
buttons now use aria-label instead of title)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 20:11:38 -07:00
|
|
|
const buttons = screen.getAllByRole('button', { name: "Remove from watchlist" });
|
2026-04-29 11:49:26 -07:00
|
|
|
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>()}
|
|
|
|
|
/>
|
|
|
|
|
);
|
fix: resolve all 48 WCAG 2.2 AA accessibility issues (#439)
* fix: resolve all 48 WCAG 2.2 AA accessibility issues
Critical fixes:
- Add aria-label to all unlabeled inputs/selects in draft dialogs (ParticipantSelectionDialog, TimeBankAdjustmentDialog, AvailableParticipantsSection)
- Add role="dialog" + aria-modal + focus trap to ConnectionOverlay and AuthRecoveryOverlay
- Add aria-live region and connection status announcement to ConnectionOverlay
Serious fixes:
- Add skip-to-content link in root.tsx with id="main-content" on <main>
- Add aria-label to UserMenu trigger button
- Add aria-describedby + role="alert" to all auth form error messages (login, register, onboarding, forgot-password, reset-password)
- Replace emoji column headers in StandingsTable with aria-label + aria-hidden spans
- Add aria-live="assertive" to "It's your turn" desktop and mobile on-clock indicators
- Add aria-live="polite" to draft room countdown timer
- Add pause button to SportTicker (WCAG 2.2.2); add aria-hidden to ticker content
- Fix Footer text contrast (changed from 28% to text-muted-foreground)
- Fix OvernightPauseSettings: add htmlFor/id pairs and role="radiogroup"+aria-checked to mode buttons
- Fix DraftSetupSection: replace broken htmlFor with aria-label on date picker button
- Add aria-label to PeopleSection owner and commissioner selects
- Add labels to ScoringPresetPicker score inputs; add role="radiogroup"+aria-checked to preset buttons
- Add role="radiogroup"+aria-checked to AutodraftSettings option buttons
- Add accessible names, aria-current="step", and <ol> list semantics to WizardStepper
Moderate fixes:
- Add aria-controls to RecentPicksFeed toggle button; wrap picks list in aria-live region
- Add role="tab"+aria-selected+aria-controls to mobile board sub-tabs + role="tabpanel"
- Add role="radiogroup"+aria-checked to TimerModeSelector
- Add aria-current="page" + aria-label to SettingsDesktopNav
- Add aria-label="Admin navigation" to admin sidebar nav
- Add scope="col" + <caption> to StandingsTable and ScoringTables
- Add ARIA table roles (role="table/rowgroup/row/columnheader/rowheader/cell") to DraftSummaryView CSS grid
Minor fixes:
- Add aria-hidden="true" to decorative trend icons in StandingsTable
- Add aria-hidden="true" to desktop column header labels row in AvailableParticipantsSection
- Replace title with aria-label on all icon-only buttons (watchlist, queue) in AvailableParticipantsSection
- Add aria-label to NotificationSettings switchOnly Switch
- Add prefers-reduced-motion check to SlotMachineHeadline JS animation
- Bump --muted-foreground from 55% to 62% opacity for improved contrast margin
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix code review findings from WCAG compliance pass
- Add Arrow key navigation + roving tabindex to all role=radiogroup
components (AutodraftSettings x2, TimerModeSelector,
OvernightPauseSettings, ScoringPresetPicker) per ARIA radio pattern
- Extract shared focus-trap logic into useFocusTrap hook; update
ConnectionOverlay and AuthRecoveryOverlay to use it
- Add tabIndex={-1} to ConnectionOverlay Card so focus can land in
spinner-only state (no interactive children)
- Replace aria-live on loading dots container with sr-only span so
status changes are announced by text content, not aria-label
- Remove contradictory aria-hidden+role=columnheader from
AvailableParticipantsSection visual-only header row
- Remove invalid scope="col" from div[role=columnheader] in
DraftSummaryView (scope is only valid on <th>)
- Remove redundant aria-label from ParticipantSelectionDialog sport
select (htmlFor label is sufficient)
- Change WizardStepper connector <li> to role=presentation
- Revert muted-foreground from 62% to 55% (original already passes
contrast; footer was fixed separately via text-muted-foreground)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix lint error and update tests for WCAG role changes
- Replace el! non-null assertion with optional chaining in useFocusTrap
- Update AutodraftSettings tests to query role="radio" instead of
role="button" (buttons have an explicit radio role since the WCAG pass)
- Update AvailableParticipantsSection watchlist tests to use
getByRole/getAllByRole instead of getByTitle/getAllByTitle (watchlist
buttons now use aria-label instead of title)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 20:11:38 -07:00
|
|
|
const buttons = screen.getAllByRole('button', { name: "Add to watchlist" });
|
2026-04-29 11:49:26 -07:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-05-04 21:54:14 -07:00
|
|
|
|
|
|
|
|
describe("Ineligible reasons", () => {
|
|
|
|
|
it("renders inline out-of-flex-spots copy for ineligible participants", () => {
|
|
|
|
|
render(
|
|
|
|
|
<AvailableParticipantsSection
|
|
|
|
|
{...defaultProps}
|
|
|
|
|
hasTeam={true}
|
|
|
|
|
canPick={true}
|
|
|
|
|
eligibility={{
|
|
|
|
|
eligibleSportIds: new Set(["s2"]),
|
|
|
|
|
ineligibleReasons: {
|
|
|
|
|
s1: createReason("no_flex_spots", "Out of flex spots (2/2)"),
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getAllByText("Out of flex spots (2/2)").length).toBeGreaterThan(0);
|
|
|
|
|
expect(screen.getAllByTitle("Out of flex spots (2/2)").length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders inline no-extras copy for ineligible participants", () => {
|
|
|
|
|
render(
|
|
|
|
|
<AvailableParticipantsSection
|
|
|
|
|
{...defaultProps}
|
|
|
|
|
hasTeam={true}
|
|
|
|
|
canPick={true}
|
|
|
|
|
eligibility={{
|
|
|
|
|
eligibleSportIds: new Set(["s2"]),
|
|
|
|
|
ineligibleReasons: {
|
|
|
|
|
s1: createReason("no_extra_participants", "Not enough NFL participants for remaining teams"),
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getAllByText("Not enough NFL participants for remaining teams").length).toBeGreaterThan(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-03-02 00:29:54 -08:00
|
|
|
});
|