feat: enhance autodraft settings with detailed option descriptions an… (#56)

* feat: enhance autodraft settings with detailed option descriptions and improved state handling

* test: update AutodraftSettings tests for button interactions and API payloads
This commit is contained in:
Chris Parsons 2026-03-02 16:26:18 -08:00 committed by GitHub
parent ca2fd288ab
commit 46332ea735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 311 additions and 268 deletions

View file

@ -1,19 +1,85 @@
import { useState, useEffect } from "react";
import { Switch } from "~/components/ui/switch";
import { useState, useEffect, useRef } from "react";
import { Check, Info } from "lucide-react";
import { toast } from "sonner";
import { Label } from "~/components/ui/label";
import { Button } from "~/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
type AutodraftMode = "next_pick" | "while_on";
// The three visible states map to isEnabled + mode combinations:
// "off" → isEnabled: false
// "next_pick" → isEnabled: true, mode: "next_pick"
// "all_picks" → isEnabled: true, mode: "while_on"
type AutodraftState = "off" | "next_pick" | "all_picks";
// 4 visible states map to isEnabled + mode + queueOnly:
// "off" → isEnabled: false
// "next_queue" → isEnabled: true, mode: "next_pick", queueOnly: true
// "all_queue" → isEnabled: true, mode: "while_on", queueOnly: true
// "all_picks" → isEnabled: true, mode: "while_on", queueOnly: false
type AutodraftState = "off" | "next_queue" | "all_queue" | "all_picks";
function toAutodraftState(isEnabled: boolean, mode: AutodraftMode): AutodraftState {
type OptionConfig = {
label: string;
desc: string;
isEnabled: boolean;
mode: AutodraftMode;
queueOnly: boolean;
};
// Record ensures all AutodraftState values are covered — no runtime find() needed
const OPTIONS: Record<AutodraftState, OptionConfig> = {
off: {
label: "Off",
desc: "You pick manually every round.",
isEnabled: false,
mode: "next_pick",
queueOnly: false,
},
next_queue: {
label: "Next in Queue",
desc: "Autodrafts your next pick from your queue, then turns off.",
isEnabled: true,
mode: "next_pick",
queueOnly: true,
},
all_queue: {
label: "All in Queue",
desc: "Keeps autodrafting from your queue until it runs out, then turns off automatically.",
isEnabled: true,
mode: "while_on",
queueOnly: true,
},
all_picks: {
label: "All Picks",
desc: "Keeps autodrafting all your picks. Uses your queue first, then falls back to the highest-ranked undrafted player.",
isEnabled: true,
mode: "while_on",
queueOnly: false,
},
};
const OPTION_ORDER: AutodraftState[] = ["off", "next_queue", "all_queue", "all_picks"];
// border-l-[3px] is applied to ALL options (transparent when inactive) so the
// content never shifts when the active border color is applied.
function getButtonClassName(state: AutodraftState, isActive: boolean): string {
if (!isActive) {
return "border-l-[3px] border-l-transparent text-muted-foreground hover:bg-muted/40";
}
if (state === "off") {
return "border-l-[3px] border-l-muted-foreground/50 text-foreground";
}
return "border-l-[3px] border-l-electric bg-electric text-background";
}
function getCheckClassName(state: AutodraftState): string {
if (state === "off") return "text-muted-foreground";
return "text-background";
}
function toAutodraftState(
isEnabled: boolean,
mode: AutodraftMode,
queueOnly: boolean
): AutodraftState {
if (!isEnabled) return "off";
return mode === "next_pick" ? "next_pick" : "all_picks";
if (mode === "next_pick") return "next_queue";
return queueOnly ? "all_queue" : "all_picks";
}
interface AutodraftSettingsProps {
@ -35,83 +101,99 @@ export function AutodraftSettings({
isMyTurn,
onUpdate,
}: AutodraftSettingsProps) {
const [localState, setLocalState] = useState<AutodraftState>(toAutodraftState(isEnabled, mode));
const [localQueueOnly, setLocalQueueOnly] = useState(queueOnly);
const [isUpdating, setIsUpdating] = useState(false);
const [localState, setLocalState] = useState<AutodraftState>(
toAutodraftState(isEnabled, mode, queueOnly)
);
// Holds the abort controller for any in-flight save so rapid selections
// cancel the previous request rather than racing to update the server.
const abortRef = useRef<AbortController | null>(null);
// Sync local state with props when they change (from socket events)
useEffect(() => {
setLocalState(toAutodraftState(isEnabled, mode));
}, [isEnabled, mode]);
setLocalState(toAutodraftState(isEnabled, mode, queueOnly));
}, [isEnabled, mode, queueOnly]);
useEffect(() => {
setLocalQueueOnly(queueOnly);
}, [queueOnly]);
const sendUpdate = async (newState: AutodraftState) => {
// Cancel any in-flight request before starting a new one
abortRef.current?.abort();
abortRef.current = new AbortController();
const sendUpdate = async (newState: AutodraftState, newQueueOnly: boolean) => {
if (isUpdating) return;
setIsUpdating(true);
const newEnabled = newState !== "off";
const newMode: AutodraftMode = newState === "all_picks" ? "while_on" : "next_pick";
const option = OPTIONS[newState];
try {
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", newEnabled.toString());
formData.append("mode", newMode);
formData.append("queueOnly", newQueueOnly.toString());
formData.append("isEnabled", option.isEnabled.toString());
formData.append("mode", option.mode);
formData.append("queueOnly", option.queueOnly.toString());
const response = await fetch("/api/autodraft/update", {
method: "POST",
body: formData,
signal: abortRef.current.signal,
});
if (response.ok) {
onUpdate(newEnabled, newMode, newQueueOnly);
onUpdate(option.isEnabled, option.mode, option.queueOnly);
toast.success(
option.isEnabled ? `Autodraft set to "${option.label}"` : "Autodraft turned off"
);
} else {
// Revert on error
setLocalState(toAutodraftState(isEnabled, mode));
setLocalQueueOnly(queueOnly);
console.error("Failed to update autodraft settings");
setLocalState(toAutodraftState(isEnabled, mode, queueOnly));
toast.error("Failed to save autodraft settings");
}
} catch (error) {
// Revert on error
setLocalState(toAutodraftState(isEnabled, mode));
setLocalQueueOnly(queueOnly);
console.error("Error updating autodraft settings:", error);
} finally {
setIsUpdating(false);
// Ignore cancellations — a newer selection has already taken over
if (error instanceof Error && error.name === "AbortError") return;
setLocalState(toAutodraftState(isEnabled, mode, queueOnly));
toast.error("Failed to save autodraft settings");
}
};
const handleStateChange = (newState: AutodraftState) => {
if (isMyTurn || isUpdating) return;
if (newState === localState || isMyTurn) return;
setLocalState(newState);
sendUpdate(newState, localQueueOnly);
sendUpdate(newState);
};
const handleQueueOnlyChange = async (checked: boolean) => {
if (isMyTurn || isUpdating) return;
setLocalQueueOnly(checked);
sendUpdate(localState, checked);
};
const isDisabled = isMyTurn || isUpdating;
const isDisabled = isMyTurn;
return (
<div className="border-t pt-4 mt-4">
<Label className="text-sm font-semibold mb-3 block">Autodraft</Label>
{/* Header with info icon inline */}
<div className="flex items-center gap-1.5 mb-3">
<Label className="text-sm font-semibold">Autodraft</Label>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Autodraft options explained"
>
<Info className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-72" align="start">
<p className="text-sm font-semibold mb-2.5">Autodraft Options</p>
<div className="space-y-2.5">
{OPTION_ORDER.map((state) => {
const { label, desc } = OPTIONS[state];
return (
<div key={state}>
<p className="text-xs font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{desc}</p>
</div>
);
})}
</div>
</PopoverContent>
</Popover>
</div>
{/* Three-state button group */}
<div className="flex rounded-lg border overflow-hidden mb-3">
{(["off", "next_pick", "all_picks"] as AutodraftState[]).map((state) => {
const labels: Record<AutodraftState, string> = {
off: "Off",
next_pick: "Next Pick",
all_picks: "All Picks",
};
<div className="flex flex-col rounded-lg border overflow-hidden">
{OPTION_ORDER.map((state) => {
const { label } = OPTIONS[state];
const isActive = localState === state;
return (
<button
@ -119,40 +201,21 @@ export function AutodraftSettings({
type="button"
disabled={isDisabled}
onClick={() => handleStateChange(state)}
className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
isActive
? "bg-electric text-background"
: "bg-transparent text-muted-foreground hover:bg-muted/60"
} ${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
className={`w-full py-2.5 px-3 text-left transition-colors border-b last:border-b-0 flex items-center justify-between gap-2 ${getButtonClassName(state, isActive)} ${
isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
}`}
>
{labels[state]}
<span className="text-xs font-medium">{label}</span>
{isActive && (
<Check className={`h-3.5 w-3.5 shrink-0 ${getCheckClassName(state)}`} />
)}
</button>
);
})}
</div>
{/* Queue-only toggle — only visible when autodraft is active */}
{localState !== "off" && (
<div className="flex items-center justify-between">
<Label
htmlFor="queue-only-switch"
className={`text-xs ${isDisabled ? "text-muted-foreground" : "cursor-pointer"}`}
>
Only autodraft from queue
</Label>
<Switch
id="queue-only-switch"
checked={localQueueOnly}
onCheckedChange={handleQueueOnlyChange}
disabled={isDisabled}
/>
</div>
)}
{isMyTurn && (
<p className="text-xs text-muted-foreground mt-2">
Autodraft settings are disabled during your pick
</p>
<p className="text-xs text-muted-foreground mt-2">You're on the clock!</p>
)}
</div>
);

View file

@ -2,7 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AutodraftSettings } from '~/components/AutodraftSettings';
// Mock fetch
vi.mock('sonner', () => ({
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
}));
global.fetch = vi.fn();
describe('AutodraftSettings Component', () => {
@ -11,8 +14,8 @@ describe('AutodraftSettings Component', () => {
teamId: 'team-456',
isEnabled: false,
mode: 'next_pick' as const,
isMyTurn: false,
queueOnly: false,
isMyTurn: false,
onUpdate: vi.fn(),
};
@ -24,63 +27,59 @@ describe('AutodraftSettings Component', () => {
});
});
// ─── Rendering ────────────────────────────────────────────────────────────
describe('Rendering', () => {
it('should render three autodraft state buttons', () => {
it('renders all four option buttons', () => {
render(<AutodraftSettings {...defaultProps} />);
expect(screen.getByRole('button', { name: 'Off' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Next Pick' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Next in Queue' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'All in Queue' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'All Picks' })).toBeInTheDocument();
});
it('should not show the queue-only toggle when autodraft is Off', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
expect(screen.queryByText('Only autodraft from queue')).not.toBeInTheDocument();
});
it('should show the queue-only toggle when autodraft is active', () => {
it('does not render a queue-only toggle switch', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} />);
expect(screen.getByRole('switch')).toBeInTheDocument();
expect(screen.getByText('Only autodraft from queue')).toBeInTheDocument();
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
});
it('should show disabled message when it is the user\'s turn', () => {
it('shows "You\'re on the clock!" when isMyTurn=true', () => {
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
expect(screen.getByText(/Autodraft settings are disabled during your pick/i)).toBeInTheDocument();
expect(screen.getByText(/You're on the clock!/i)).toBeInTheDocument();
});
it('should mark Off as active when isEnabled=false', () => {
it('marks Off as active (muted border) when isEnabled=false', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
const offButton = screen.getByRole('button', { name: 'Off' });
expect(offButton.className).toContain('bg-electric');
expect(screen.getByRole('button', { name: 'Off' }).className).toContain(
'border-l-muted-foreground'
);
});
it('should mark Next Pick as active when isEnabled=true and mode=next_pick', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" />);
const nextPickButton = screen.getByRole('button', { name: 'Next Pick' });
expect(nextPickButton.className).toContain('bg-electric');
it('marks Next in Queue as active when isEnabled=true, mode=next_pick', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" queueOnly={true} />);
expect(screen.getByRole('button', { name: 'Next in Queue' }).className).toContain('bg-electric');
});
it('should mark All Picks as active when isEnabled=true and mode=while_on', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="while_on" />);
it('marks All in Queue as active when isEnabled=true, mode=while_on, queueOnly=true', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="while_on" queueOnly={true} />);
expect(screen.getByRole('button', { name: 'All in Queue' }).className).toContain('bg-electric');
});
const allPicksButton = screen.getByRole('button', { name: 'All Picks' });
expect(allPicksButton.className).toContain('bg-electric');
it('marks All Picks as active when isEnabled=true, mode=while_on, queueOnly=false', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="while_on" queueOnly={false} />);
expect(screen.getByRole('button', { name: 'All Picks' }).className).toContain('bg-electric');
});
});
describe('Button Group Interaction', () => {
it('should switch to Next Pick when that button is clicked', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={false} onUpdate={onUpdate} />);
// ─── Interaction ──────────────────────────────────────────────────────────
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
describe('Button interaction', () => {
it('switches to Next in Queue and calls onUpdate with correct args', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
@ -88,195 +87,176 @@ describe('AutodraftSettings Component', () => {
expect.objectContaining({ method: 'POST' })
);
});
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(true, 'next_pick', false);
});
await waitFor(() => expect(onUpdate).toHaveBeenCalledWith(true, 'next_pick', true));
});
it('should switch to All Picks when that button is clicked', async () => {
it('switches to All in Queue and calls onUpdate with correct args', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={false} onUpdate={onUpdate} />);
render(<AutodraftSettings {...defaultProps} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('button', { name: 'All in Queue' }));
await waitFor(() => expect(onUpdate).toHaveBeenCalledWith(true, 'while_on', true));
});
it('switches to All Picks and calls onUpdate with correct args', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('button', { name: 'All Picks' }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(true, 'while_on', false);
});
await waitFor(() => expect(onUpdate).toHaveBeenCalledWith(true, 'while_on', false));
});
it('should switch to Off when that button is clicked from an active state', async () => {
it('switches to Off and calls onUpdate with isEnabled=false', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={true} onUpdate={onUpdate} />);
render(
<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" queueOnly={true} onUpdate={onUpdate} />
);
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(false, 'next_pick', false);
});
await waitFor(() => expect(onUpdate).toHaveBeenCalledWith(false, 'next_pick', false));
});
it('should disable all buttons when it is the user\'s turn', () => {
it('disables all buttons when isMyTurn=true', () => {
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
expect(screen.getByRole('button', { name: 'Off' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Next Pick' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Next in Queue' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'All in Queue' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'All Picks' })).toBeDisabled();
});
it('should not fire API call when a button is clicked during user\'s turn', () => {
it('does not call fetch when a button is clicked during the user\'s turn', () => {
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
expect(global.fetch).not.toHaveBeenCalled();
});
it('should handle API errors by reverting state', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
(global.fetch as any).mockResolvedValueOnce({
ok: false,
json: async () => ({ error: 'Server error' }),
});
it('does not re-fire when the already-active option is clicked', async () => {
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalled();
});
// Off button should still be active (reverted)
const offButton = screen.getByRole('button', { name: 'Off' });
expect(offButton.className).toContain('bg-electric');
consoleSpy.mockRestore();
});
it('should handle network errors by reverting state', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalled();
});
consoleSpy.mockRestore();
});
});
describe('Queue-Only Toggle', () => {
it('should send queueOnly=true when the toggle is switched on', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={true} queueOnly={false} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('switch'));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(true, 'next_pick', true);
});
});
it('should send queueOnly=false when the toggle is switched off', async () => {
const onUpdate = vi.fn();
render(<AutodraftSettings {...defaultProps} isEnabled={true} queueOnly={true} onUpdate={onUpdate} />);
fireEvent.click(screen.getByRole('switch'));
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(true, 'next_pick', false);
});
});
it('should disable the queue-only toggle when it is the user\'s turn', () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} isMyTurn={true} />);
expect(screen.getByRole('switch')).toBeDisabled();
});
});
describe('API Integration', () => {
it('should post correct formData when switching to Next Pick', async () => {
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
'/api/autodraft/update',
expect.objectContaining({ method: 'POST', body: expect.any(FormData) })
);
});
const formData = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(formData.get('seasonId')).toBe('season-123');
expect(formData.get('teamId')).toBe('team-456');
expect(formData.get('isEnabled')).toBe('true');
expect(formData.get('mode')).toBe('next_pick');
expect(formData.get('queueOnly')).toBe('false');
});
it('should post mode=while_on when switching to All Picks', async () => {
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
fireEvent.click(screen.getByRole('button', { name: 'All Picks' }));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});
const formData = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(formData.get('mode')).toBe('while_on');
expect(formData.get('isEnabled')).toBe('true');
});
it('should post isEnabled=false when switching to Off', async () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled();
});
const formData = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(formData.get('isEnabled')).toBe('false');
expect(global.fetch).not.toHaveBeenCalled();
});
});
it('should disable buttons during an in-flight API call', async () => {
let resolve: any;
// ─── Optimistic UI ────────────────────────────────────────────────────────
describe('Optimistic UI', () => {
it('does not disable buttons while a fetch is in flight', async () => {
let resolve: (v: any) => void;
(global.fetch as any).mockReturnValueOnce(new Promise((r) => { resolve = r; }));
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
// Buttons stay enabled — optimistic UI does not block interaction
expect(screen.getByRole('button', { name: 'Off' })).not.toBeDisabled();
resolve!({ ok: true, json: async () => ({ success: true }) });
});
it('fires a fetch for every rapid click, aborting previous in-flight requests', async () => {
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
fireEvent.click(screen.getByRole('button', { name: 'All in Queue' }));
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(3));
});
});
// ─── Error handling ───────────────────────────────────────────────────────
describe('Error handling', () => {
it('reverts to the previous state on a non-ok API response', async () => {
(global.fetch as any).mockResolvedValueOnce({ ok: false });
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Off' })).toBeDisabled();
});
resolve({ ok: true, json: async () => ({ success: true }) });
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Off' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Off' }).className).toContain(
'border-l-muted-foreground'
);
});
});
it('should only process the first click when buttons are clicked rapidly', async () => {
render(<AutodraftSettings {...defaultProps} />);
it('reverts to the previous state on a network error', async () => {
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
fireEvent.click(screen.getByRole('button', { name: 'All Picks' }));
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(screen.getByRole('button', { name: 'Off' }).className).toContain(
'border-l-muted-foreground'
);
});
});
});
// ─── API payload ──────────────────────────────────────────────────────────
describe('API payload', () => {
it('posts correct formData for Next in Queue (next_pick, queueOnly=true)', async () => {
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalled());
const body = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(body.get('seasonId')).toBe('season-123');
expect(body.get('teamId')).toBe('team-456');
expect(body.get('isEnabled')).toBe('true');
expect(body.get('mode')).toBe('next_pick');
expect(body.get('queueOnly')).toBe('true');
});
it('posts correct formData for All in Queue (while_on, queueOnly=true)', async () => {
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'All in Queue' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalled());
const body = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(body.get('isEnabled')).toBe('true');
expect(body.get('mode')).toBe('while_on');
expect(body.get('queueOnly')).toBe('true');
});
it('posts correct formData for All Picks (while_on, queueOnly=false)', async () => {
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'All Picks' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalled());
const body = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(body.get('isEnabled')).toBe('true');
expect(body.get('mode')).toBe('while_on');
expect(body.get('queueOnly')).toBe('false');
});
it('posts isEnabled=false for Off', async () => {
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="next_pick" queueOnly={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalled());
const body = (global.fetch as any).mock.calls[0][1].body as FormData;
expect(body.get('isEnabled')).toBe('false');
});
it('passes an AbortSignal to fetch', async () => {
render(<AutodraftSettings {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Next in Queue' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalled());
const options = (global.fetch as any).mock.calls[0][1];
expect(options.signal).toBeInstanceOf(AbortSignal);
});
});
});

View file

@ -698,7 +698,7 @@ export default function DraftRoom() {
if (userTeam && data.teamId === userTeam.id) {
// Detect server-side auto-disable: was enabled with queueOnly, now disabled
const prev = userAutodraftRef.current;
if (!data.isEnabled && prev.isEnabled && prev.queueOnly) {
if (!data.isEnabled && prev.isEnabled && prev.queueOnly && prev.mode === "while_on") {
toast.info("Autodraft disabled — your queue is empty");
}