mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
190 lines
8.2 KiB
TypeScript
190 lines
8.2 KiB
TypeScript
// FE-COMP-TODO-001 to FE-COMP-TODO-015
|
|
import { render, screen, waitFor } 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 { 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(() => {
|
|
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(<TodoListPanel tripId={1} items={items} />);
|
|
expect(screen.getByText('Book hotel')).toBeInTheDocument();
|
|
expect(screen.getByText('Buy tickets')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-TODO-002: shows Add new task button', () => {
|
|
render(<TodoListPanel tripId={1} items={[]} />);
|
|
expect(screen.getByText('Add new task...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-TODO-003: sidebar filter buttons are rendered', () => {
|
|
render(<TodoListPanel tripId={1} items={[]} />);
|
|
// 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(<TodoListPanel tripId={1} items={items} />);
|
|
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(<TodoListPanel tripId={1} items={items} />);
|
|
// 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(<TodoListPanel tripId={1} items={items} />);
|
|
// 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(<TodoListPanel tripId={1} items={items} />);
|
|
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(<TodoListPanel tripId={1} items={items} />);
|
|
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(<TodoListPanel tripId={1} items={items} />);
|
|
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(<TodoListPanel tripId={1} items={items} />);
|
|
// 1/2 = 50% completed
|
|
expect(screen.getByText(/50%/)).toBeInTheDocument();
|
|
expect(screen.getByText(/1 \/ 2 completed/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-TODO-011: clicking Add new task opens detail form', async () => {
|
|
const user = userEvent.setup();
|
|
render(<TodoListPanel tripId={1} items={[]} />);
|
|
await user.click(screen.getByText('Add new task...'));
|
|
// The detail pane shows "Create task" button
|
|
await screen.findByText('Create task');
|
|
});
|
|
|
|
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/toggle', () => {
|
|
putCalled = true;
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
const items = [buildTodoItem({ id: 5, name: 'Toggle Me', checked: 0 })];
|
|
render(<TodoListPanel tripId={1} items={items} />);
|
|
// Click the checkbox button (Square icon)
|
|
const checkboxes = screen.getAllByRole('button');
|
|
// Find the checkbox button near the item
|
|
const checkboxBtn = checkboxes.find(btn => {
|
|
const parent = btn.closest('[style*="cursor: pointer"]');
|
|
return parent && parent.textContent?.includes('Toggle Me');
|
|
});
|
|
if (checkboxBtn) {
|
|
await user.click(checkboxBtn);
|
|
await waitFor(() => expect(putCalled).toBe(true));
|
|
}
|
|
});
|
|
|
|
it('FE-COMP-TODO-013: clicking a task row opens its detail pane', async () => {
|
|
const user = userEvent.setup();
|
|
const items = [buildTodoItem({ id: 7, name: 'Click Me', checked: 0 })];
|
|
render(<TodoListPanel tripId={1} items={items} />);
|
|
await user.click(screen.getByText('Click Me'));
|
|
// Detail pane should open showing the task title
|
|
await screen.findByText('Task');
|
|
});
|
|
|
|
it('FE-COMP-TODO-014: category filter appears in sidebar for items with categories', () => {
|
|
const items = [buildTodoItem({ name: 'JobTask', category: 'JobCat', checked: 0 })];
|
|
render(<TodoListPanel tripId={1} items={items} />);
|
|
// The category filter button shows category name (as text or title)
|
|
const catEls = screen.getAllByText(/JobCat/);
|
|
expect(catEls.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('FE-COMP-TODO-015: category filter button is accessible and clickable', async () => {
|
|
const user = userEvent.setup();
|
|
const items = [
|
|
buildTodoItem({ name: 'JobTask', category: 'JobCat', checked: 0 }),
|
|
buildTodoItem({ name: 'HomeTask', category: 'HomeCat', checked: 0 }),
|
|
];
|
|
render(<TodoListPanel tripId={1} items={items} />);
|
|
// Both visible initially in 'all' filter (shows unchecked)
|
|
expect(screen.getByText('JobTask')).toBeInTheDocument();
|
|
expect(screen.getByText('HomeTask')).toBeInTheDocument();
|
|
// Category buttons exist in sidebar (by accessible name or text)
|
|
const catBtn = screen.getByRole('button', { name: /JobCat/ });
|
|
expect(catBtn).toBeInTheDocument();
|
|
// Clicking the category button should work without throwing
|
|
await user.click(catBtn);
|
|
// Task with category 'JobCat' remains visible
|
|
expect(screen.getByText('JobTask')).toBeInTheDocument();
|
|
});
|
|
});
|