diff options
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/FeedList.test.tsx | 79 | ||||
| -rw-r--r-- | frontend/src/components/Settings.test.tsx | 68 | ||||
| -rw-r--r-- | frontend/src/components/Settings.tsx | 3 |
3 files changed, 149 insertions, 1 deletions
diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx index 9ef2349..d3d3a57 100644 --- a/frontend/src/components/FeedList.test.tsx +++ b/frontend/src/components/FeedList.test.tsx @@ -147,4 +147,83 @@ describe('FeedList Component', () => { expect(screen.getByText(/no feeds found/i)).toBeInTheDocument(); }); }); + + it('handles search submission', async () => { + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => [] } as Response); + render( + <BrowserRouter> + <FeedList theme="light" setTheme={() => { }} setSidebarVisible={() => { }} isMobile={false} /> + </BrowserRouter> + ); + + // Wait for load + await waitFor(() => { + expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); + }); + + const searchInput = screen.getByPlaceholderText(/search\.\.\./i); + fireEvent.change(searchInput, { target: { value: 'test search' } }); + fireEvent.submit(searchInput.closest('form')!); + + // Should navigate to include search query + // Since we're using BrowserRouter in test, we can only check if it doesn't crash + // but we can't easily check 'navigate' unless we mock it. + }); + + it('handles logout', async () => { + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => [] } as Response); + + // Mock window.location + const originalLocation = window.location; + const locationMock = new URL('http://localhost/v2/'); + + delete (window as { location?: Location }).location; + (window as { location?: unknown }).location = { + ...originalLocation, + assign: vi.fn(), + replace: vi.fn(), + get href() { return locationMock.href; }, + set href(val: string) { locationMock.href = new URL(val, locationMock.origin).href; } + }; + + render( + <BrowserRouter> + <FeedList theme="light" setTheme={() => { }} setSidebarVisible={() => { }} isMobile={false} /> + </BrowserRouter> + ); + + // Wait for load + await waitFor(() => { + expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); + }); + + const logoutBtn = screen.getByText(/logout/i); + fireEvent.click(logoutBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('/api/logout', expect.any(Object)); + expect(window.location.href).toContain('/v2/login'); + }); + window.location = originalLocation; + }); + + it('closes sidebar on mobile link click', async () => { + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => [] } as Response); + const setSidebarVisible = vi.fn(); + render( + <BrowserRouter> + <FeedList theme="light" setTheme={() => { }} setSidebarVisible={setSidebarVisible} isMobile={true} /> + </BrowserRouter> + ); + + // Wait for load + await waitFor(() => { + expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); + }); + + const unreadLink = screen.getByText(/unread/i); + fireEvent.click(unreadLink); + + expect(setSidebarVisible).toHaveBeenCalledWith(false); + }); }); diff --git a/frontend/src/components/Settings.test.tsx b/frontend/src/components/Settings.test.tsx index a0e7de4..5b0518c 100644 --- a/frontend/src/components/Settings.test.tsx +++ b/frontend/src/components/Settings.test.tsx @@ -136,4 +136,72 @@ describe('Settings Component', () => { expect(screen.getByText('Imported Feed')).toBeInTheDocument(); }); }); + + it('triggers a crawl', async () => { + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => [] } as Response) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({ message: 'crawl started' }) } as Response); // Crawl + + // Mock alert + const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => { }); + + render(<Settings />); + + // Wait for load + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + const crawlBtn = screen.getByText(/crawl all feeds now/i); + fireEvent.click(crawlBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/crawl', + expect.objectContaining({ method: 'POST' }) + ); + expect(alertMock).toHaveBeenCalledWith('Crawl started!'); + }); + alertMock.mockRestore(); + }); + + it('handles API errors', async () => { + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => [] } as Response) // Initial load load + .mockResolvedValueOnce({ ok: false, json: async () => ({}) } as Response); // Add feed error + + render(<Settings />); + + // Wait for load + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + const input = screen.getByPlaceholderText('https://example.com/feed.xml'); + const button = screen.getByText('Add Feed'); + + fireEvent.change(input, { target: { value: 'http://fail.com/rss' } }); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByText(/failed to add feed/i)).toBeInTheDocument(); + }); + }); + + it('handles font theme change', async () => { + const setFontTheme = vi.fn(); + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => [] } as Response); + + render(<Settings fontTheme="default" setFontTheme={setFontTheme} />); + + // Wait for load + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + const select = screen.getByLabelText(/font theme/i); + fireEvent.change(select, { target: { value: 'serif' } }); + + expect(setFontTheme).toHaveBeenCalledWith('serif'); + }); }); diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index ec432ba..3dab77f 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -138,8 +138,9 @@ export default function Settings({ fontTheme, setFontTheme }: SettingsProps) { <div className="appearance-section"> <h3>Appearance</h3> <div className="font-selector"> - <label>Font Theme:</label> + <label htmlFor="font-theme-select">Font Theme:</label> <select + id="font-theme-select" value={fontTheme || 'default'} onChange={(e) => setFontTheme(e.target.value)} className="font-select" |
