|
1 | | -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; |
2 | | -import { vi } from 'vitest'; |
| 1 | +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; |
| 2 | +import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| 3 | +import { Provider } from 'react-redux'; |
| 4 | +import { configureStore } from '@reduxjs/toolkit'; |
3 | 5 | import Collaboration from '../Collaboration'; |
4 | | -import { ApiEndpoint } from '~/utils/URL'; |
5 | 6 |
|
6 | | -/* ================= MOCKS ================= */ |
7 | | - |
8 | | -vi.mock('react-redux', () => ({ |
9 | | - useSelector: vi.fn(fn => fn({ theme: { darkMode: false } })), |
10 | | -})); |
11 | | - |
12 | | -vi.mock('react-toastify', () => ({ |
13 | | - toast: { error: vi.fn(), success: vi.fn() }, |
14 | | -})); |
15 | | - |
16 | | -global.fetch = vi.fn(); |
17 | | - |
18 | | -/* ================= TEST DATA ================= */ |
19 | | - |
20 | | -const mockCategories = { |
21 | | - categories: ['Engineering', 'Art'], |
22 | | -}; |
| 7 | +const mockStore = configureStore({ |
| 8 | + reducer: { |
| 9 | + theme: () => ({ darkMode: false }), |
| 10 | + }, |
| 11 | +}); |
23 | 12 |
|
24 | | -const mockJobs = { |
25 | | - jobs: [ |
26 | | - { |
27 | | - _id: '1', |
28 | | - title: 'Frontend Engineer', |
29 | | - category: 'Engineering', |
30 | | - position: 'Frontend Engineer', |
31 | | - description: 'Test description', |
32 | | - }, |
33 | | - ], |
| 13 | +const renderWithProviders = ui => { |
| 14 | + return render(<Provider store={mockStore}>{ui}</Provider>); |
34 | 15 | }; |
35 | 16 |
|
36 | | -/* ================= TEST SUITE ================= */ |
37 | | - |
38 | 17 | describe('Collaboration Component', () => { |
39 | 18 | beforeEach(() => { |
40 | | - vi.clearAllMocks(); |
41 | | - |
42 | | - fetch.mockImplementation(async url => { |
43 | | - if (url.includes('/jobs/categories')) { |
44 | | - return { ok: true, json: async () => mockCategories }; |
45 | | - } |
46 | | - |
47 | | - if (url.includes('/jobs?')) { |
48 | | - return { ok: true, json: async () => mockJobs }; |
49 | | - } |
50 | | - |
51 | | - if (url.includes('/jobs/summaries')) { |
52 | | - return { ok: true, json: async () => ({ jobs: [] }) }; |
53 | | - } |
54 | | - |
55 | | - if (url.includes('/jobs/positions')) { |
56 | | - return { ok: true, json: async () => ({ positions: ['Frontend Engineer'] }) }; |
57 | | - } |
58 | | - |
59 | | - return { ok: true, json: async () => ({}) }; |
60 | | - }); |
61 | | - }); |
62 | | - |
63 | | - /* ================= BASIC ================= */ |
64 | | - |
65 | | - test('renders logo', () => { |
66 | | - render(<Collaboration />); |
67 | | - expect(screen.getByAltText('One Community Logo')).toBeInTheDocument(); |
68 | | - }); |
69 | | - |
70 | | - test('fetches categories on mount', async () => { |
71 | | - render(<Collaboration />); |
72 | | - |
73 | | - await waitFor(() => { |
74 | | - expect(fetch).toHaveBeenCalledWith(`${ApiEndpoint}/jobs/categories`); |
75 | | - }); |
76 | | - }); |
77 | | - |
78 | | - /* ================= SEARCH ================= */ |
79 | | - |
80 | | - test('search updates input and triggers fetch', async () => { |
81 | | - render(<Collaboration />); |
82 | | - |
83 | | - const input = screen.getByPlaceholderText('Search by title...'); |
84 | | - |
85 | | - fireEvent.change(input, { target: { value: 'engineer' } }); |
86 | | - expect(input.value).toBe('engineer'); |
87 | | - |
88 | | - fireEvent.click(screen.getByText('Go')); |
89 | | - |
90 | | - await waitFor(() => { |
91 | | - expect(fetch).toHaveBeenCalled(); |
92 | | - }); |
93 | | - }); |
94 | | - |
95 | | - /* ================= CATEGORY ================= */ |
96 | | - |
97 | | - test('select category updates UI', async () => { |
98 | | - render(<Collaboration />); |
99 | | - |
100 | | - fireEvent.click(screen.getByText('Select Categories ▼')); |
101 | | - |
102 | | - const category = await screen.findByText('Engineering'); |
103 | | - fireEvent.click(category); |
104 | | - |
105 | | - expect(screen.getByText('Engineering ▼')).toBeInTheDocument(); |
106 | | - }); |
107 | | - |
108 | | - /* ================= POSITION ================= */ |
109 | | - |
110 | | - test('select position after selecting category', async () => { |
111 | | - render(<Collaboration />); |
112 | | - |
113 | | - // Select category first |
114 | | - fireEvent.click(screen.getByText('Select Categories ▼')); |
115 | | - fireEvent.click(await screen.findByText('Engineering')); |
116 | | - |
117 | | - // Open positions dropdown |
118 | | - fireEvent.click(screen.getByText('Select Positions ▼')); |
119 | | - |
120 | | - // Find dropdown container WITHOUT DOM traversal |
121 | | - const dropdownContainer = await screen.findByText('Frontend Engineer'); |
122 | | - |
123 | | - // Click directly on option (now visible) |
124 | | - fireEvent.click(dropdownContainer); |
125 | | - |
126 | | - expect(screen.getByText('Frontend Engineer ▼')).toBeInTheDocument(); |
127 | | - }); |
128 | | - |
129 | | - /* ================= JOB CLICK ================= */ |
130 | | - |
131 | | - test('opens job modal on click', async () => { |
132 | | - render(<Collaboration />); |
133 | | - |
134 | | - const job = await screen.findByText('Frontend Engineer'); |
135 | | - fireEvent.click(job); |
136 | | - |
137 | | - expect(await screen.findByText('Test description')).toBeInTheDocument(); |
| 19 | + vi.stubGlobal( |
| 20 | + 'fetch', |
| 21 | + vi.fn(url => { |
| 22 | + const urlString = url.toString(); |
| 23 | + |
| 24 | + if (urlString.includes('/jobs/categories')) { |
| 25 | + return Promise.resolve({ |
| 26 | + ok: true, |
| 27 | + json: () => Promise.resolve({ categories: ['Engineering'] }), |
| 28 | + }); |
| 29 | + } |
| 30 | + |
| 31 | + return Promise.resolve({ |
| 32 | + ok: true, |
| 33 | + json: () => |
| 34 | + Promise.resolve({ |
| 35 | + jobs: [ |
| 36 | + { |
| 37 | + _id: '1', |
| 38 | + title: 'Frontend Engineer', |
| 39 | + category: 'Engineering', |
| 40 | + description: 'Build UI components', |
| 41 | + }, |
| 42 | + ], |
| 43 | + }), |
| 44 | + }); |
| 45 | + }), |
| 46 | + ); |
138 | 47 | }); |
139 | 48 |
|
140 | | - test('closes job modal', async () => { |
141 | | - render(<Collaboration />); |
142 | | - |
143 | | - fireEvent.click(await screen.findByText('Frontend Engineer')); |
144 | | - |
145 | | - fireEvent.click(await screen.findByText('×')); |
| 49 | + it('renders main heading and initial jobs', async () => { |
| 50 | + renderWithProviders(<Collaboration />); |
| 51 | + expect(await screen.findByText(/LIKE TO WORK WITH US/i)).toBeInTheDocument(); |
146 | 52 |
|
147 | | - await waitFor(() => { |
148 | | - expect(screen.queryByText('Test description')).not.toBeInTheDocument(); |
149 | | - }); |
| 53 | + // Using regex to handle potential element splitting |
| 54 | + expect(await screen.findByText(/Frontend Engineer/i)).toBeInTheDocument(); |
150 | 55 | }); |
151 | 56 |
|
152 | | - /* ================= CLEAR FILTER ================= */ |
153 | | - |
154 | | - test('clear all filters resets UI', async () => { |
155 | | - render(<Collaboration />); |
156 | | - |
157 | | - fireEvent.click(screen.getByText('Select Categories ▼')); |
158 | | - fireEvent.click(await screen.findByText('Engineering')); |
| 57 | + it('updates search term on form submission', async () => { |
| 58 | + renderWithProviders(<Collaboration />); |
159 | 59 |
|
160 | | - fireEvent.click(screen.getByText('Clear All')); |
| 60 | + const input = screen.getByPlaceholderText(/search by title/i); |
| 61 | + fireEvent.change(input, { target: { value: 'React' } }); |
161 | 62 |
|
162 | | - expect(screen.getByText('Listing all job ads.')).toBeInTheDocument(); |
163 | | - }); |
164 | | - |
165 | | - /* ================= PAGINATION ================= */ |
166 | | - |
167 | | - test('pagination renders', async () => { |
168 | | - render(<Collaboration />); |
| 63 | + // FIX: Instead of .closest('form'), we find the button by its role/text |
| 64 | + // This adheres to testing-library/no-node-access |
| 65 | + const goButton = screen.getByRole('button', { name: /go/i }); |
| 66 | + fireEvent.click(goButton); |
169 | 67 |
|
| 68 | + // Verify that the search parameter was included in at least one fetch call |
170 | 69 | await waitFor(() => { |
171 | | - expect(screen.getByText('1')).toBeInTheDocument(); |
| 70 | + const calls = vi.mocked(global.fetch).mock.calls; |
| 71 | + const hasSearchCall = calls.some(call => call[0].includes('search=React')); |
| 72 | + expect(hasSearchCall).toBe(true); |
172 | 73 | }); |
173 | 74 | }); |
174 | 75 |
|
175 | | - /* ================= SUMMARIES ================= */ |
176 | | - |
177 | | - test('fetch summaries and display', async () => { |
178 | | - render(<Collaboration />); |
179 | | - |
180 | | - fireEvent.click(screen.getByText('Show Summaries')); |
181 | | - |
182 | | - await waitFor(() => { |
183 | | - expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/jobs/summaries')); |
184 | | - }); |
| 76 | + it('switches to summaries view and back', async () => { |
| 77 | + vi.stubGlobal( |
| 78 | + 'fetch', |
| 79 | + vi.fn(url => { |
| 80 | + if (url.includes('/summaries')) { |
| 81 | + return Promise.resolve({ |
| 82 | + ok: true, |
| 83 | + json: () => |
| 84 | + Promise.resolve({ |
| 85 | + jobs: [{ _id: 's1', title: 'Summary Job', description: 'Quick summary' }], |
| 86 | + }), |
| 87 | + }); |
| 88 | + } |
| 89 | + return Promise.resolve({ ok: true, json: () => Promise.resolve({ jobs: [] }) }); |
| 90 | + }), |
| 91 | + ); |
| 92 | + |
| 93 | + renderWithProviders(<Collaboration />); |
| 94 | + |
| 95 | + const summariesBtn = screen.getByText(/Show Summaries/i); |
| 96 | + fireEvent.click(summariesBtn); |
| 97 | + |
| 98 | + expect(await screen.findByText('Job Summaries')).toBeInTheDocument(); |
| 99 | + |
| 100 | + const backBtn = screen.getByText(/Back to Job Listings/i); |
| 101 | + fireEvent.click(backBtn); |
| 102 | + |
| 103 | + expect(await screen.findByText(/LIKE TO WORK WITH US/i)).toBeInTheDocument(); |
185 | 104 | }); |
186 | 105 | }); |
0 commit comments