// FE-COMP-TODO-001 to FE-COMP-TODO-079 import { render, screen, waitFor, fireEvent, within } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { server } from '../../../tests/helpers/msw/server'; import { useAuthStore } from '../../store/authStore'; import { useTripStore } from '../../store/tripStore'; import { usePermissionsStore } from '../../store/permissionsStore'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser, buildTrip, buildTodoItem } from '../../../tests/helpers/factories'; import TodoListPanel from './TodoListPanel'; beforeEach(() => { resetAllStores(); // Simulate desktop width so sidebar labels are rendered (not mobile icon-only mode) Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true, configurable: true }); server.use( http.get('/api/trips/:id/members', () => HttpResponse.json({ owner: null, members: [], current_user_id: 1 }) ), ); seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true }); seedStore(useTripStore, { trip: buildTrip({ id: 1 }) }); }); afterEach(() => { vi.useRealTimers(); Object.defineProperty(window, 'innerWidth', { value: 0, writable: true, configurable: true }); }); describe('TodoListPanel', () => { it('FE-COMP-TODO-001: renders todo items by name', () => { const items = [ buildTodoItem({ name: 'Book hotel', checked: 0 }), buildTodoItem({ name: 'Buy tickets', checked: 0 }), ]; render(); expect(screen.getByText('Book hotel')).toBeInTheDocument(); expect(screen.getByText('Buy tickets')).toBeInTheDocument(); }); it('FE-COMP-TODO-002: raising addItemSignal opens the new task form', async () => { const { rerender } = render(); rerender(); expect(await screen.findByText('Create task')).toBeInTheDocument(); }); it('FE-COMP-TODO-003: sidebar filter buttons are rendered', () => { render(); // Filter buttons exist — match by title (mobile mode, jsdom innerWidth=0) or text (desktop) const allButtons = screen.getAllByRole('button'); const buttonTitlesAndTexts = allButtons.map(b => (b.textContent || '') + (b.getAttribute('title') || '')); expect(buttonTitlesAndTexts.some(t => t.includes('All'))).toBe(true); expect(buttonTitlesAndTexts.some(t => t.includes('My Tasks'))).toBe(true); expect(buttonTitlesAndTexts.some(t => t.includes('Done'))).toBe(true); expect(buttonTitlesAndTexts.some(t => t.includes('Overdue'))).toBe(true); }); it('FE-COMP-TODO-004: unchecked items are shown in All filter', () => { const items = [buildTodoItem({ name: 'Open Task', checked: 0 })]; render(); expect(screen.getByText('Open Task')).toBeInTheDocument(); }); it('FE-COMP-TODO-005: checked items are hidden in All filter (All shows unchecked)', () => { const items = [ buildTodoItem({ name: 'Done Task', checked: 1 }), buildTodoItem({ name: 'Open Task', checked: 0 }), ]; render(); // All filter by default shows only unchecked expect(screen.queryByText('Done Task')).not.toBeInTheDocument(); expect(screen.getByText('Open Task')).toBeInTheDocument(); }); it('FE-COMP-TODO-006: Done filter shows only checked items', async () => { const user = userEvent.setup(); const items = [ buildTodoItem({ name: 'Completed Task', checked: 1 }), buildTodoItem({ name: 'Pending Task', checked: 0 }), ]; render(); // Find the Done filter button by title (mobile mode) or text (desktop) const doneBtn = screen.queryByTitle('Done') || screen.getAllByRole('button').find( b => b.textContent?.trim() === 'Done' ); if (doneBtn) { await user.click(doneBtn); await screen.findByText('Completed Task'); expect(screen.queryByText('Pending Task')).not.toBeInTheDocument(); } }); it('FE-COMP-TODO-007: shows P1 priority badge for priority=1 items', () => { const items = [buildTodoItem({ name: 'Urgent Task', priority: 1, checked: 0 })]; render(); expect(screen.getByText('P1')).toBeInTheDocument(); }); it('FE-COMP-TODO-008: shows P2 priority badge for priority=2 items', () => { const items = [buildTodoItem({ name: 'Normal Task', priority: 2, checked: 0 })]; render(); expect(screen.getByText('P2')).toBeInTheDocument(); }); it('FE-COMP-TODO-009: items with no priority show no priority badge', () => { const items = [buildTodoItem({ name: 'Low Priority', priority: 0, checked: 0 })]; render(); expect(screen.queryByText('P1')).not.toBeInTheDocument(); expect(screen.queryByText('P2')).not.toBeInTheDocument(); expect(screen.queryByText('P3')).not.toBeInTheDocument(); }); it('FE-COMP-TODO-010: progress bar shows completion percentage', () => { const items = [ buildTodoItem({ name: 'Done Task', checked: 1 }), buildTodoItem({ name: 'Open Task', checked: 0 }), ]; render(); // 1/2 = 50% completed expect(screen.getByText(/50%/)).toBeInTheDocument(); expect(screen.getByText(/1 \/ 2 completed/i)).toBeInTheDocument(); }); it('FE-COMP-TODO-011: raising addItemSignal opens detail form with Create task button', async () => { const { rerender } = render(); rerender(); expect(await screen.findByText('Create task')).toBeInTheDocument(); }); it('FE-COMP-TODO-012: toggling item calls toggleTodoItem action', async () => { const user = userEvent.setup(); let putCalled = false; server.use( http.put('/api/trips/1/todo/:id', () => { putCalled = true; return HttpResponse.json({ success: true }); }) ); const items = [buildTodoItem({ id: 5, name: 'Toggle Me', checked: 0 })]; render(); // The checkbox is the row's own