* Redesign autodraft queue system with three-state control and queue-only constraint Core Logic & Database: - Add `queue_only` boolean column to `autodraft_settings` (migration 0031) - Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks) - `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled - `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3) - `autodraft-updated` socket event now includes `queueOnly` field Mobile UI Overhaul: - Rename "Lobby" tab → "Available" (AC6) - Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5) - Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab - Turn indicator appears on both Available and Queue tabs Components: - `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2) - `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock Desktop (AC4): - Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons Tests (AC7): - `autodraft.test.ts`: updated for queueOnly field and socket event shape - `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB * fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests - Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit (line 488) — was dead code since the column is NOT NULL, but semantically wrong and would have caused client-side UI desync if the type ever relaxed - Remove `?? false` default on the next_pick auto-disable path for consistency - Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly constraint: empty queue, all items drafted, partial queue skip, and EV fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add missing queueOnly prop to AutodraftSettings test fixtures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: rewrite AutodraftSettings tests for three-state button group UI The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5 new tests covering the queue-only toggle and the All Picks/Off button interactions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
282 lines
10 KiB
TypeScript
282 lines
10 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { AutodraftSettings } from '~/components/AutodraftSettings';
|
|
|
|
// Mock fetch
|
|
global.fetch = vi.fn();
|
|
|
|
describe('AutodraftSettings Component', () => {
|
|
const defaultProps = {
|
|
seasonId: 'season-123',
|
|
teamId: 'team-456',
|
|
isEnabled: false,
|
|
mode: 'next_pick' as const,
|
|
isMyTurn: false,
|
|
queueOnly: false,
|
|
onUpdate: vi.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
(global.fetch as any).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ success: true }),
|
|
});
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render three autodraft state buttons', () => {
|
|
render(<AutodraftSettings {...defaultProps} />);
|
|
|
|
expect(screen.getByRole('button', { name: 'Off' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Next Pick' })).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', () => {
|
|
render(<AutodraftSettings {...defaultProps} isEnabled={true} />);
|
|
|
|
expect(screen.getByRole('switch')).toBeInTheDocument();
|
|
expect(screen.getByText('Only autodraft from queue')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should show disabled message when it is the user\'s turn', () => {
|
|
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
|
|
|
|
expect(screen.getByText(/Autodraft settings are disabled during your pick/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should mark Off as active when isEnabled=false', () => {
|
|
render(<AutodraftSettings {...defaultProps} isEnabled={false} />);
|
|
|
|
const offButton = screen.getByRole('button', { name: 'Off' });
|
|
expect(offButton.className).toContain('bg-electric');
|
|
});
|
|
|
|
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('should mark All Picks as active when isEnabled=true and mode=while_on', () => {
|
|
render(<AutodraftSettings {...defaultProps} isEnabled={true} mode="while_on" />);
|
|
|
|
const allPicksButton = screen.getByRole('button', { name: 'All Picks' });
|
|
expect(allPicksButton.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} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
'/api/autodraft/update',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(onUpdate).toHaveBeenCalledWith(true, 'next_pick', false);
|
|
});
|
|
});
|
|
|
|
it('should switch to All Picks when that button is clicked', async () => {
|
|
const onUpdate = vi.fn();
|
|
render(<AutodraftSettings {...defaultProps} isEnabled={false} onUpdate={onUpdate} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'All Picks' }));
|
|
|
|
await waitFor(() => {
|
|
expect(onUpdate).toHaveBeenCalledWith(true, 'while_on', false);
|
|
});
|
|
});
|
|
|
|
it('should switch to Off when that button is clicked from an active state', async () => {
|
|
const onUpdate = vi.fn();
|
|
render(<AutodraftSettings {...defaultProps} isEnabled={true} onUpdate={onUpdate} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
|
|
|
|
await waitFor(() => {
|
|
expect(onUpdate).toHaveBeenCalledWith(false, 'next_pick', false);
|
|
});
|
|
});
|
|
|
|
it('should disable all buttons when it is the user\'s turn', () => {
|
|
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: 'All Picks' })).toBeDisabled();
|
|
});
|
|
|
|
it('should not fire API call when a button is clicked during user\'s turn', () => {
|
|
render(<AutodraftSettings {...defaultProps} isMyTurn={true} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
|
|
|
|
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' }),
|
|
});
|
|
|
|
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');
|
|
});
|
|
|
|
it('should disable buttons during an in-flight API call', async () => {
|
|
let resolve: any;
|
|
(global.fetch as any).mockReturnValueOnce(new Promise((r) => { resolve = r; }));
|
|
|
|
render(<AutodraftSettings {...defaultProps} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
it('should only process the first click when buttons are clicked rapidly', async () => {
|
|
render(<AutodraftSettings {...defaultProps} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Next Pick' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'All Picks' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Off' }));
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|
|
});
|