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();
  });
});