brackt/server/__tests__/timer-autodraft.test.ts
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

342 lines
14 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before imports
vi.mock('drizzle-orm/postgres-js', () => ({
drizzle: vi.fn(() => mockDb),
}));
vi.mock('postgres', () => ({
default: vi.fn(() => ({})),
}));
vi.mock('../socket', () => ({
getSocketIO: vi.fn(() => mockSocketIO),
}));
let mockDb: any;
let mockSocketIO: any;
// Setup mocks
beforeEach(() => {
mockSocketIO = {
to: vi.fn().mockReturnThis(),
emit: vi.fn(),
};
mockDb = {
query: {
seasons: { findMany: vi.fn(), findFirst: vi.fn() },
draftSlots: { findMany: vi.fn() },
draftTimers: { findFirst: vi.fn() },
autodraftSettings: { findFirst: vi.fn() },
draftPicks: { findMany: vi.fn(), findFirst: vi.fn() },
draftQueue: { findMany: vi.fn() },
participants: { findMany: vi.fn() },
seasonTemplateSports: { findMany: vi.fn() },
},
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
returning: vi.fn(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
};
});
describe('Timer Autodraft Integration', () => {
describe('Autodraft Settings Check', () => {
it('should check autodraft settings when timer expires', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockDb.query.seasons.findMany.mockResolvedValue([{
id: seasonId, status: 'draft', draftPaused: false,
currentPickNumber: 1, draftInitialTime: 120, draftIncrementTime: 30, draftRounds: 10,
}]);
mockDb.query.draftSlots.findMany.mockResolvedValue([
{ id: 'slot-1', teamId, draftOrder: 1 },
{ id: 'slot-2', teamId: 'team-2', draftOrder: 2 },
]);
mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: 'timer-1', seasonId, teamId, timeRemaining: 0 });
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: false,
});
expect(mockDb.query.autodraftSettings.findFirst).toBeDefined();
});
it('should use regular auto-pick when autodraft is disabled', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockDb.query.autodraftSettings.findFirst.mockResolvedValue({
id: 'settings-1', seasonId, teamId, isEnabled: false, mode: 'next_pick', queueOnly: false,
});
const settings = await mockDb.query.autodraftSettings.findFirst();
expect(settings.isEnabled).toBe(false);
});
it('should handle missing autodraft settings', async () => {
mockDb.query.autodraftSettings.findFirst.mockResolvedValue(null);
const settings = await mockDb.query.autodraftSettings.findFirst();
expect(settings).toBeNull();
});
});
describe('Autodraft Mode Handling', () => {
it('should disable autodraft after pick when mode is next_pick', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = { id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: false };
mockDb.returning.mockResolvedValue([{ ...autodraftSettings, isEnabled: false, updatedAt: new Date() }]);
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
await mockDb.update();
await mockDb.set({ isEnabled: false, updatedAt: new Date() });
await mockDb.where();
const result = await mockDb.returning();
expect(result[0].isEnabled).toBe(false);
}
});
it('should keep autodraft enabled when mode is while_on (All Picks)', async () => {
const autodraftSettings = { isEnabled: true, mode: 'while_on', queueOnly: false };
if (autodraftSettings.mode === 'while_on') {
expect(autodraftSettings.isEnabled).toBe(true);
}
});
});
describe('Autodraft Pick Logic', () => {
it('should pick from queue first when autodraft is enabled', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
]);
mockDb.query.draftPicks.findMany.mockResolvedValue([]);
const queueItems = await mockDb.query.draftQueue.findMany();
const draftedPicks = await mockDb.query.draftPicks.findMany();
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
const availableQueueItem = queueItems.find((item: any) => !draftedParticipantIds.includes(item.participantId));
expect(availableQueueItem).toBeDefined();
expect(availableQueueItem.participantId).toBe('participant-1');
});
it('should skip drafted participants in queue and try next', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
]);
mockDb.query.draftPicks.findMany.mockResolvedValue([{ id: 'pick-1', participantId: 'participant-1' }]);
const queueItems = await mockDb.query.draftQueue.findMany();
const draftedPicks = await mockDb.query.draftPicks.findMany();
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
const availableQueueItem = queueItems.find((item: any) => !draftedParticipantIds.includes(item.participantId));
expect(availableQueueItem.participantId).toBe('participant-2');
});
it('should fall back to highest EV when queue is empty and queueOnly is OFF', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
mockDb.query.participants.findMany.mockResolvedValue([
{ id: 'participant-high-ev', name: 'High EV Participant', expectedValue: 1000 },
]);
const queueItems = await mockDb.query.draftQueue.findMany();
expect(queueItems.length).toBe(0);
const queueOnly = false;
const availableParticipants = await mockDb.query.participants.findMany();
const selectedParticipant = queueOnly ? null : availableParticipants[0];
expect(selectedParticipant).not.toBeNull();
expect(selectedParticipant?.expectedValue).toBe(1000);
});
});
describe('Queue-Only Constraint (AC2 & AC3)', () => {
it('should NOT fall back to highest EV when queueOnly is ON and queue is empty', async () => {
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
const queueItems = await mockDb.query.draftQueue.findMany();
expect(queueItems.length).toBe(0);
const queueOnly = true;
const selectedParticipant = queueOnly ? null : { id: 'participant-high-ev' };
expect(selectedParticipant).toBeNull();
});
it('should disable autodraft when queueOnly is ON and queue becomes empty (AC3)', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = { id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: true };
mockDb.returning.mockResolvedValue([{ ...autodraftSettings, isEnabled: false, updatedAt: new Date() }]);
const participantId = null; // empty queue + queueOnly
if (!participantId && autodraftSettings.queueOnly) {
await mockDb.update();
await mockDb.set({ isEnabled: false, updatedAt: new Date() });
await mockDb.where();
const result = await mockDb.returning();
expect(result[0].isEnabled).toBe(false);
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId, isEnabled: false, mode: autodraftSettings.mode, queueOnly: autodraftSettings.queueOnly,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: true,
});
}
});
it('should skip drafted queue items and continue to next available item', async () => {
const queue = [
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
];
const drafted = new Set(['participant-1']);
const availableQueueItem = queue.find((item) => !drafted.has(item.participantId));
expect(availableQueueItem?.participantId).toBe('participant-2');
});
it('should auto-disable when all queue items are drafted and queueOnly is ON', async () => {
const queue = [
{ id: 'queue-1', participantId: 'participant-1', queuePosition: 1 },
{ id: 'queue-2', participantId: 'participant-2', queuePosition: 2 },
];
const drafted = new Set(['participant-1', 'participant-2']);
const queueOnly = true;
const availableQueueItem = queue.find((item) => !drafted.has(item.participantId));
expect(availableQueueItem).toBeUndefined();
const selectedParticipant = queueOnly ? null : { id: 'fallback-ev' };
expect(selectedParticipant).toBeNull();
});
});
describe('Three-State Autodraft (AC1)', () => {
it('should map Off state to isEnabled=false', () => {
const offState = { isEnabled: false, mode: 'next_pick', queueOnly: false };
expect(offState.isEnabled).toBe(false);
});
it('should map Next Pick state to isEnabled=true, mode=next_pick', () => {
const nextPickState = { isEnabled: true, mode: 'next_pick', queueOnly: false };
expect(nextPickState.isEnabled).toBe(true);
expect(nextPickState.mode).toBe('next_pick');
});
it('should map All Picks state to isEnabled=true, mode=while_on', () => {
const allPicksState = { isEnabled: true, mode: 'while_on', queueOnly: false };
expect(allPicksState.isEnabled).toBe(true);
expect(allPicksState.mode).toBe('while_on');
});
it('should transition Off → Next Pick correctly', () => {
const initial = { isEnabled: false, mode: 'next_pick' };
const updated = { isEnabled: true, mode: 'next_pick' };
expect(updated.isEnabled).not.toBe(initial.isEnabled);
expect(updated.mode).toBe('next_pick');
});
it('should transition Next Pick → All Picks correctly', () => {
const nextPick = { isEnabled: true, mode: 'next_pick' };
const allPicks = { ...nextPick, mode: 'while_on' };
expect(allPicks.mode).toBe('while_on');
expect(allPicks.isEnabled).toBe(true);
});
it('should auto-disable if queue is empty and queueOnly is ON (AC3)', () => {
const settings = { isEnabled: true, mode: 'next_pick', queueOnly: true };
const queue: any[] = [];
const shouldAutoDisable = settings.queueOnly && queue.length === 0;
const finalSettings = shouldAutoDisable ? { ...settings, isEnabled: false } : settings;
expect(finalSettings.isEnabled).toBe(false);
});
it('should NOT auto-disable if queue is empty and queueOnly is OFF', () => {
const settings = { isEnabled: true, mode: 'next_pick', queueOnly: false };
const queue: any[] = [];
const shouldAutoDisable = settings.queueOnly && queue.length === 0;
const finalSettings = shouldAutoDisable ? { ...settings, isEnabled: false } : settings;
expect(finalSettings.isEnabled).toBe(true);
});
});
describe('Socket Event Emission', () => {
it('should emit autodraft-updated with queueOnly when disabling next_pick mode', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: false,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: false,
});
});
it('should emit pick-made event after autodraft pick', async () => {
const seasonId = 'season-123';
const pickData = { pick: { id: 'pick-1', participantId: 'participant-1', pickNumber: 1 }, nextPickNumber: 2, isDraftComplete: false };
mockSocketIO.to(`draft-${seasonId}`).emit('pick-made', pickData);
expect(mockSocketIO.emit).toHaveBeenCalledWith('pick-made', pickData);
});
it('should emit autodraft-updated with queueOnly preserved on auto-disable (AC3)', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: true,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('autodraft-updated', {
teamId, isEnabled: false, mode: 'next_pick', queueOnly: true,
});
});
});
describe('Back-to-Back Picks', () => {
it('should only autodraft first pick when mode is next_pick', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
const autodraftSettings = { id: 'settings-1', seasonId, teamId, isEnabled: true, mode: 'next_pick', queueOnly: false };
if (autodraftSettings.isEnabled && autodraftSettings.mode === 'next_pick') {
const updatedSettings = { ...autodraftSettings, isEnabled: false };
expect(updatedSettings.isEnabled).toBe(false);
}
const settingsForSecondPick = { ...autodraftSettings, isEnabled: false };
expect(settingsForSecondPick.isEnabled).toBe(false);
});
it('should continue autodrafting consecutive picks when mode is while_on (All Picks)', async () => {
const autodraftSettings = { isEnabled: true, mode: 'while_on', queueOnly: false };
if (autodraftSettings.mode === 'while_on') expect(autodraftSettings.isEnabled).toBe(true);
expect(autodraftSettings.isEnabled).toBe(true);
});
});
describe('Time Increment', () => {
it('should add increment time after autodraft pick', () => {
expect(0 + 30).toBe(30);
});
it('should emit timer-update after adding increment', async () => {
const seasonId = 'season-123';
const teamId = 'team-456';
mockSocketIO.to(`draft-${seasonId}`).emit('timer-update', {
seasonId, teamId, timeRemaining: 30, currentPickNumber: 1,
});
expect(mockSocketIO.emit).toHaveBeenCalledWith('timer-update', {
seasonId, teamId, timeRemaining: 30, currentPickNumber: 1,
});
});
});
});