aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-14 21:34:49 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-14 21:34:49 -0800
commit6e28d1530aa08b878f5082bbcd85a95f84f830e8 (patch)
tree7f6b1fb3a74166d97f2ba74f50d3cd787ec163dd /frontend/src/components
parent5e2b1b2de36fc63cfa677705388f5701c62ee138 (diff)
downloadneko-6e28d1530aa08b878f5082bbcd85a95f84f830e8.tar.gz
neko-6e28d1530aa08b878f5082bbcd85a95f84f830e8.tar.bz2
neko-6e28d1530aa08b878f5082bbcd85a95f84f830e8.zip
chore: update build artifacts and finalize test improvements
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/FeedList.test.tsx79
-rw-r--r--frontend/src/components/Settings.test.tsx68
-rw-r--r--frontend/src/components/Settings.tsx3
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"