/* global describe, it, expect, vi, beforeEach */ import React from 'react' import { render, screen, act } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ScenarioQueue } from './ScenarioQueue' import type { Scenario } from './types' // ── dnd-kit mock ────────────────────────────────────────────────────────────── // @dnd-kit/react's onDragEnd event uses source.initialIndex / source.index, // not source.id vs target.id. We capture the callback here so tests can // call it directly with the correct event shape. let capturedDragEnd: ((event: { canceled: boolean operation: { source: { id: unknown; index: number; initialIndex: number } | null } }) => void) | undefined vi.mock('@dnd-kit/react', () => ({ DragDropProvider: ({ children, onDragEnd, }: { children: React.ReactNode onDragEnd: typeof capturedDragEnd }) => { capturedDragEnd = onDragEnd return React.createElement(React.Fragment, null, children) }, })) vi.mock('@dnd-kit/react/sortable', () => ({ useSortable: () => ({ ref: () => undefined, handleRef: () => undefined, isDragging: false, }), })) // ── Fixtures ────────────────────────────────────────────────────────────────── // Mirrors the mock data in ScenarioQueue.stories.tsx const MOCK_QUEUE: Scenario[] = [ { id: 'ux-research', title: 'UX Research & Design Interview', category: 'Design', difficulty: 'beginner', duration: '10-15 min', status: 'queued', }, { id: 'biz-analysis', title: 'Business Analysis ROI Design Presentation', category: 'Business', difficulty: 'intermediate', duration: '15-25 min', status: 'queued', }, { id: 'product-redesign', title: 'Product Redesign Challenge', category: 'Product', difficulty: 'advanced', duration: '25-35 min', status: 'queued', }, ] const MOCK_COMPLETED: Scenario[] = [ { id: 'stakeholder-mgmt', title: 'Stakeholder Management Scenario', category: 'Leadership', difficulty: 'intermediate', duration: '20-30 min', status: 'completed', }, { id: 'agile-sprint', title: 'Agile Sprint Planning Session', category: 'Process', difficulty: 'beginner', duration: '10-15 min', status: 'completed', }, ] const ALL_SCENARIOS = [...MOCK_QUEUE, ...MOCK_COMPLETED] // ── Helpers ─────────────────────────────────────────────────────────────────── function simulateDrag(initialIndex: number, newIndex: number) { act(() => { capturedDragEnd?.({ canceled: false, operation: { source: { id: MOCK_QUEUE[initialIndex]?.id ?? 'item', initialIndex, index: newIndex }, }, }) }) } // ── Tests ───────────────────────────────────────────────────────────────────── describe('ScenarioQueue', () => { beforeEach(() => { capturedDragEnd = undefined }) // ── Rendering ─────────────────────────────────────────────────────────────── describe('Rendering', () => { it('queue tab is the default active tab', () => { render() const queueTab = screen.getByRole('tab', { name: 'In Queue' }) expect(queueTab).toHaveAttribute('aria-selected', 'true') }) it('queue tab renders 3 scenario cards by default', () => { render() expect(screen.getByText('UX Research & Design Interview')).toBeInTheDocument() expect( screen.getByText('Business Analysis ROI Design Presentation'), ).toBeInTheDocument() expect(screen.getByText('Product Redesign Challenge')).toBeInTheDocument() }) it('switching to Completed tab makes 2 completed cards visible', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('tab', { name: 'Completed' })) expect(screen.getByText('Stakeholder Management Scenario')).toBeVisible() expect(screen.getByText('Agile Sprint Planning Session')).toBeVisible() }) it('header count reflects total number of scenarios', () => { render() expect(screen.getByText('5 scenarios')).toBeInTheDocument() }) it('queue tab cards show drag handle, position badge, title, difficulty, duration', () => { render() // Drag handles — one per queued card expect(screen.getAllByLabelText('Drag to reorder')).toHaveLength(3) // Position badges expect(screen.getByLabelText('Position 1')).toBeInTheDocument() expect(screen.getByLabelText('Position 2')).toBeInTheDocument() expect(screen.getByLabelText('Position 3')).toBeInTheDocument() // Titles, difficulty labels, durations expect(screen.getByText('UX Research & Design Interview')).toBeInTheDocument() expect(screen.getByText('Beginner')).toBeInTheDocument() expect(screen.getByText('10-15 min')).toBeInTheDocument() }) it('completed tab cards show title, difficulty, duration and re-queue toggle — no drag handle, no position badge', async () => { const user = userEvent.setup() // Render only completed — ensures zero drag handles exist anywhere in DOM render() await user.click(screen.getByRole('tab', { name: 'Completed' })) // Cards are present expect(screen.getByText('Stakeholder Management Scenario')).toBeInTheDocument() expect(screen.getByText('Agile Sprint Planning Session')).toBeInTheDocument() // Re-queue toggles present expect( screen.getByLabelText('Re-queue Stakeholder Management Scenario'), ).toBeInTheDocument() expect( screen.getByLabelText('Re-queue Agile Sprint Planning Session'), ).toBeInTheDocument() // No drag handles or position badges anywhere expect(screen.queryByLabelText('Drag to reorder')).not.toBeInTheDocument() expect(screen.queryByLabelText(/^Position \d/)).not.toBeInTheDocument() }) it('empty queue tab shows empty state message', () => { render() expect(screen.getByText('No scenarios in queue')).toBeInTheDocument() }) it('empty completed tab shows empty state message', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('tab', { name: 'Completed' })) expect(screen.getByText('No completed scenarios')).toBeInTheDocument() }) }) // ── Position Numbering & Active Card Styling ──────────────────────────────── describe('Position Numbering', () => { it('shows sequential position numbers 1, 2, 3 matching array order', () => { render() expect(screen.getByLabelText('Position 1')).toHaveTextContent('1') expect(screen.getByLabelText('Position 2')).toHaveTextContent('2') expect(screen.getByLabelText('Position 3')).toHaveTextContent('3') }) it('position badges are in the correct DOM order (1 before 2 before 3)', () => { const { container } = render() const badges = container.querySelectorAll('[aria-label^="Position"]') expect(badges[0]).toHaveAccessibleName('Position 1') expect(badges[1]).toHaveAccessibleName('Position 2') expect(badges[2]).toHaveAccessibleName('Position 3') }) }) // ── Difficulty Badge Attributes ───────────────────────────────────────────── describe('Difficulty Badge Attributes', () => { it('beginner card renders data-difficulty="beginner" on its badge', () => { const { container } = render() // At least one element (difficultyBadge) must carry data-difficulty="beginner" expect( container.querySelector('[data-difficulty="beginner"]'), ).toBeInTheDocument() }) it('intermediate card renders data-difficulty="intermediate" on its badge', () => { const { container } = render() expect( container.querySelector('[data-difficulty="intermediate"]'), ).toBeInTheDocument() }) it('advanced card renders data-difficulty="advanced" on its badge', () => { const { container } = render() expect( container.querySelector('[data-difficulty="advanced"]'), ).toBeInTheDocument() }) it('difficulty label text matches the difficulty value', () => { render() expect(screen.getByText('Beginner')).toBeInTheDocument() expect(screen.getByText('Intermediate')).toBeInTheDocument() expect(screen.getByText('Advanced')).toBeInTheDocument() }) }) // ── Re-queue Flow ─────────────────────────────────────────────────────────── describe('Re-queue Flow', () => { it('toggling the re-queue switch calls onRequeue with the scenario ID', async () => { const user = userEvent.setup() const onRequeue = vi.fn() render() await user.click(screen.getByRole('tab', { name: 'Completed' })) // Switch starts checked (defaultChecked). Click to uncheck → triggers re-queue. const switchEl = screen.getByLabelText('Re-queue Stakeholder Management Scenario') await user.click(switchEl) expect(onRequeue).toHaveBeenCalledWith('stakeholder-mgmt') expect(onRequeue).toHaveBeenCalledTimes(1) }) it('re-queued card with wasRequeued=true shows Repeat badge in queue tab', () => { const requeued: Scenario = { ...MOCK_COMPLETED[0], status: 'queued', wasRequeued: true, } render() expect(screen.getByText('Repeat')).toBeInTheDocument() }) it('re-queued card appears as position 4 at end of queue', () => { const requeued: Scenario = { ...MOCK_COMPLETED[0], status: 'queued', wasRequeued: true, } render() expect(screen.getByLabelText('Position 4')).toHaveTextContent('4') }) it('cards without wasRequeued flag do not show Repeat badge', () => { render() expect(screen.queryByText('Repeat')).not.toBeInTheDocument() }) it('only the re-queued card shows the Repeat badge, not other queue cards', () => { const requeued: Scenario = { ...MOCK_COMPLETED[0], status: 'queued', wasRequeued: true, } render() // Exactly one Repeat badge expect(screen.getAllByText('Repeat')).toHaveLength(1) }) }) // ── Drag-Drop Reorder ─────────────────────────────────────────────────────── describe('Drag-Drop Reorder', () => { it('simulated reorder calls onReorder with new ID order', () => { const onReorder = vi.fn() render() // Move card at index 0 (ux-research) to index 1 simulateDrag(0, 1) expect(onReorder).toHaveBeenCalledWith([ 'biz-analysis', 'ux-research', 'product-redesign', ]) }) it('position badge numbers update to reflect new array order after reorder', () => { render() // Move card 0 (ux-research) to index 2 (last position) simulateDrag(0, 2) // The 3 positions should still exist (numbers 1, 2, 3) expect(screen.getByLabelText('Position 1')).toHaveTextContent('1') expect(screen.getByLabelText('Position 2')).toHaveTextContent('2') expect(screen.getByLabelText('Position 3')).toHaveTextContent('3') }) it('after reorder, position badges are in the correct DOM order', () => { const { container } = render() simulateDrag(0, 2) // The DOM order of position badges should be 1 → 2 → 3 top to bottom const badges = container.querySelectorAll('[aria-label^="Position"]') expect(badges[0]).toHaveAccessibleName('Position 1') expect(badges[1]).toHaveAccessibleName('Position 2') expect(badges[2]).toHaveAccessibleName('Position 3') }) it('canceled drag does not update positions or call onReorder', () => { const onReorder = vi.fn() render() act(() => { capturedDragEnd?.({ canceled: true, operation: { source: { id: 'ux-research', initialIndex: 0, index: 1 }, }, }) }) expect(onReorder).not.toHaveBeenCalled() // Positions unchanged expect(screen.getByLabelText('Position 1')).toHaveTextContent('1') }) it('drag to same index does not update positions or call onReorder', () => { const onReorder = vi.fn() render() simulateDrag(1, 1) // same index — no-op expect(onReorder).not.toHaveBeenCalled() }) it('drag with null source does not throw or call onReorder', () => { const onReorder = vi.fn() render() expect(() => { act(() => { capturedDragEnd?.({ canceled: false, operation: { source: null } }) }) }).not.toThrow() expect(onReorder).not.toHaveBeenCalled() }) }) })