brackt/app/components/__tests__/AutodraftSettings.test.tsx
Chris Parsons 1ba50828f7
Claude/redesign autodraft queue c4 kp r (#40)
* 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>
2026-02-27 22:16:26 -08:00

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);
});
});
});
});