Spaces:
Sleeping
Sleeping
File size: 5,994 Bytes
766d85d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | // FE-COMP-CAT-001 to FE-COMP-CAT-012
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 { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildUser, buildCategory } from '../../../tests/helpers/factories';
import CategoryManager from './CategoryManager';
import { ToastContainer } from '../shared/Toast';
beforeEach(() => {
resetAllStores();
server.use(
http.get('/api/categories', () =>
HttpResponse.json({ categories: [] })
),
);
seedStore(useAuthStore, { user: buildUser({ role: 'admin' }), isAuthenticated: true });
});
describe('CategoryManager', () => {
it('FE-COMP-CAT-001: renders without crashing', () => {
render(<CategoryManager />);
expect(document.body).toBeInTheDocument();
});
it('FE-COMP-CAT-002: shows Categories title', async () => {
render(<CategoryManager />);
await screen.findByText('Categories');
});
it('FE-COMP-CAT-003: shows empty state when no categories', async () => {
render(<CategoryManager />);
await screen.findByText('No categories yet');
});
it('FE-COMP-CAT-004: shows New Category button', async () => {
render(<CategoryManager />);
await screen.findByText('New Category');
});
it('FE-COMP-CAT-005: clicking New Category shows form', async () => {
const user = userEvent.setup();
render(<CategoryManager />);
await screen.findByText('New Category');
await user.click(screen.getByText('New Category'));
expect(screen.getByPlaceholderText('Category name')).toBeInTheDocument();
});
it('FE-COMP-CAT-006: shows existing categories from API', async () => {
server.use(
http.get('/api/categories', () =>
HttpResponse.json({
categories: [
buildCategory({ name: 'Museum' }),
buildCategory({ name: 'Restaurant' }),
],
})
)
);
render(<CategoryManager />);
await screen.findByText('Museum');
expect(screen.getByText('Restaurant')).toBeInTheDocument();
});
it('FE-COMP-CAT-007: clicking Create submits POST API', async () => {
const user = userEvent.setup();
let postCalled = false;
server.use(
http.post('/api/categories', async ({ request }) => {
postCalled = true;
const body = await request.json() as Record<string, unknown>;
return HttpResponse.json({
category: buildCategory({ name: String(body.name) }),
});
})
);
render(<><ToastContainer /><CategoryManager /></>);
await screen.findByText('New Category');
await user.click(screen.getByText('New Category'));
const nameInput = screen.getByPlaceholderText('Category name');
await user.type(nameInput, 'Parks');
await user.click(screen.getByText('Create'));
await waitFor(() => expect(postCalled).toBe(true));
});
it('FE-COMP-CAT-008: edit button shows form for existing category', async () => {
const user = userEvent.setup();
server.use(
http.get('/api/categories', () =>
HttpResponse.json({ categories: [buildCategory({ id: 5, name: 'Hotels' })] })
)
);
render(<CategoryManager />);
await screen.findByText('Hotels');
// Edit button is icon-only (no title) — find all buttons and click the first action button
const buttons = screen.getAllByRole('button');
// Buttons: [New Category, ...action buttons for the category]
// The edit button is the first action button in the category row (Edit2 icon)
const actionBtns = buttons.filter(b => !b.textContent?.includes('New Category'));
await user.click(actionBtns[0]);
// Name input pre-filled with category name
expect(screen.getByDisplayValue('Hotels')).toBeInTheDocument();
});
it('FE-COMP-CAT-009: delete button triggers DELETE API', async () => {
const user = userEvent.setup();
let deleteCalled = false;
server.use(
http.get('/api/categories', () =>
HttpResponse.json({ categories: [buildCategory({ id: 9, name: 'Parks' })] })
),
http.delete('/api/categories/9', () => {
deleteCalled = true;
return HttpResponse.json({ success: true });
})
);
vi.spyOn(window, 'confirm').mockReturnValue(true);
render(<><ToastContainer /><CategoryManager /></>);
await screen.findByText('Parks');
// Delete button is icon-only (Trash2, no title) — find the second action button
const buttons = screen.getAllByRole('button');
const actionBtns = buttons.filter(b => !b.textContent?.includes('New Category'));
await user.click(actionBtns[1]);
await waitFor(() => expect(deleteCalled).toBe(true));
vi.restoreAllMocks();
});
it('FE-COMP-CAT-010: shows subtitle text', async () => {
render(<CategoryManager />);
await screen.findByText('Manage categories for places');
});
it('FE-COMP-CAT-011: category count is shown', async () => {
server.use(
http.get('/api/categories', () =>
HttpResponse.json({
categories: [buildCategory({ name: 'Cat1' }), buildCategory({ name: 'Cat2' })],
})
)
);
render(<CategoryManager />);
await screen.findByText('Cat1');
await screen.findByText('Cat2');
// Both categories rendered
expect(screen.getAllByRole('button').length).toBeGreaterThan(0);
});
it('FE-COMP-CAT-012: Cancel button in form hides the form', async () => {
const user = userEvent.setup();
render(<CategoryManager />);
await screen.findByText('New Category');
await user.click(screen.getByText('New Category'));
expect(screen.getByPlaceholderText('Category name')).toBeInTheDocument();
await user.click(screen.getByText('Cancel'));
expect(screen.queryByPlaceholderText('Category name')).not.toBeInTheDocument();
});
});
|