Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/

fix(skills): parse wrapped API response in searchSkills (#146)

The /api/skills/search endpoint returns { skills: [...] } but
searchSkills() treated the response as a plain array. This caused
a TypeError when the combobox tried to .map() the result, crashing
the entire profile page via the error boundary.

Also add Array.isArray guard in the combobox for defensive rendering.

authored by

Guido X Jansen and committed by
GitHub
7600da2b 95555afb

+18 -17
+1 -1
src/components/skill-combobox.tsx
··· 52 52 setLoading(true); 53 53 try { 54 54 const suggestions = await searchSkills(q); 55 - setResults(suggestions); 55 + setResults(Array.isArray(suggestions) ? suggestions : []); 56 56 setIsOpen(true); 57 57 setActiveIndex(-1); 58 58 } catch {
+2 -1
src/lib/profile-api.ts
··· 236 236 `${API_URL}/api/skills/search?q=${encodeURIComponent(query)}&limit=${limit}`, 237 237 ); 238 238 if (!res.ok) return []; 239 - return (await res.json()) as SkillSuggestion[]; 239 + const data = (await res.json()) as { skills: SkillSuggestion[] }; 240 + return data.skills; 240 241 } catch { 241 242 return []; 242 243 }
+8 -8
tests/components/skill-combobox.test.tsx
··· 52 52 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 53 53 mockFetch.mockResolvedValue({ 54 54 ok: true, 55 - json: () => Promise.resolve(mockSuggestions), 55 + json: () => Promise.resolve({ skills: mockSuggestions }), 56 56 }); 57 57 58 58 renderCombobox(); ··· 73 73 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 74 74 mockFetch.mockResolvedValue({ 75 75 ok: true, 76 - json: () => Promise.resolve(mockSuggestions), 76 + json: () => Promise.resolve({ skills: mockSuggestions }), 77 77 }); 78 78 79 79 const { onChange } = renderCombobox(); ··· 93 93 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 94 94 mockFetch.mockResolvedValue({ 95 95 ok: true, 96 - json: () => Promise.resolve(mockSuggestions), 96 + json: () => Promise.resolve({ skills: mockSuggestions }), 97 97 }); 98 98 99 99 renderCombobox(); ··· 116 116 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 117 117 mockFetch.mockResolvedValue({ 118 118 ok: true, 119 - json: () => Promise.resolve(mockSuggestions), 119 + json: () => Promise.resolve({ skills: mockSuggestions }), 120 120 }); 121 121 122 122 renderCombobox(); ··· 139 139 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 140 140 mockFetch.mockResolvedValue({ 141 141 ok: true, 142 - json: () => Promise.resolve(mockSuggestions), 142 + json: () => Promise.resolve({ skills: mockSuggestions }), 143 143 }); 144 144 145 145 const { onChange } = renderCombobox(); ··· 174 174 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 175 175 mockFetch.mockResolvedValue({ 176 176 ok: true, 177 - json: () => Promise.resolve(mockSuggestions), 177 + json: () => Promise.resolve({ skills: mockSuggestions }), 178 178 }); 179 179 180 180 renderCombobox(); ··· 250 250 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 251 251 mockFetch.mockResolvedValue({ 252 252 ok: true, 253 - json: () => Promise.resolve(mockSuggestions), 253 + json: () => Promise.resolve({ skills: mockSuggestions }), 254 254 }); 255 255 256 256 renderCombobox(); ··· 270 270 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 271 271 mockFetch.mockResolvedValue({ 272 272 ok: true, 273 - json: () => Promise.resolve(mockSuggestions), 273 + json: () => Promise.resolve({ skills: mockSuggestions }), 274 274 }); 275 275 276 276 renderCombobox();
+2 -2
tests/components/skill-edit-dialog.test.tsx
··· 80 80 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 81 81 mockFetch.mockResolvedValue({ 82 82 ok: true, 83 - json: () => Promise.resolve(mockSuggestions), 83 + json: () => Promise.resolve({ skills: mockSuggestions }), 84 84 }); 85 85 86 86 renderDialog(); ··· 105 105 const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 106 106 mockFetch.mockResolvedValue({ 107 107 ok: true, 108 - json: () => Promise.resolve(mockSuggestions), 108 + json: () => Promise.resolve({ skills: mockSuggestions }), 109 109 }); 110 110 111 111 renderDialog();
+5 -5
tests/lib/skills-api.test.ts
··· 16 16 mockFetch.mockResolvedValue({ 17 17 ok: true, 18 18 json: () => 19 - Promise.resolve([ 20 - { canonicalName: 'TypeScript', slug: 'typescript', category: 'Technical' }, 21 - ]), 19 + Promise.resolve({ 20 + skills: [{ canonicalName: 'TypeScript', slug: 'typescript', category: 'Technical' }], 21 + }), 22 22 }); 23 23 24 24 const results = await searchSkills('Type', 5); ··· 32 32 it('uses default limit of 10', async () => { 33 33 mockFetch.mockResolvedValue({ 34 34 ok: true, 35 - json: () => Promise.resolve([]), 35 + json: () => Promise.resolve({ skills: [] }), 36 36 }); 37 37 38 38 await searchSkills('React'); ··· 58 58 it('encodes special characters in query', async () => { 59 59 mockFetch.mockResolvedValue({ 60 60 ok: true, 61 - json: () => Promise.resolve([]), 61 + json: () => Promise.resolve({ skills: [] }), 62 62 }); 63 63 64 64 await searchSkills('C++ & C#');