From d5a413382c93efeb1d888daf3216c51cd5f40f75 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 21:11:40 -0800 Subject: chore: fix lint and type errors to resolve CI failures --- frontend/src/App.test.tsx | 17 ++++----- frontend/src/components/FeedItem.test.tsx | 6 ++-- frontend/src/components/FeedItems.test.tsx | 31 +++++++++------- frontend/src/components/FeedItems.tsx | 3 ++ frontend/src/components/FeedList.test.tsx | 58 +++++++++++++++++++----------- frontend/src/components/FeedList.tsx | 4 +-- frontend/src/components/Login.test.tsx | 10 +++--- frontend/src/components/Login.tsx | 2 +- frontend/src/components/Settings.test.tsx | 26 +++++++------- frontend/src/components/Settings.tsx | 7 ++-- frontend/src/components/TagView.test.tsx | 25 ++++++++----- 11 files changed, 112 insertions(+), 77 deletions(-) (limited to 'frontend/src') diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 196f32a..1ef9763 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -11,20 +11,21 @@ describe('App', () => { }); it('renders login on initial load (unauthenticated)', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, - }); + } as Response); window.history.pushState({}, 'Test page', '/v2/login'); render(); expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); }); it('renders dashboard when authenticated', async () => { - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/auth')) return Promise.resolve({ ok: true }); - if (url.includes('/api/feed/')) return Promise.resolve({ ok: true, json: async () => [] }); - if (url.includes('/api/tag')) return Promise.resolve({ ok: true, json: async () => [] }); - return Promise.resolve({ ok: true }); // Fallback + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/auth')) return Promise.resolve({ ok: true } as Response); + if (urlStr.includes('/api/feed/')) return Promise.resolve({ ok: true, json: async () => [] } as Response); + if (urlStr.includes('/api/tag')) return Promise.resolve({ ok: true, json: async () => [] } as Response); + return Promise.resolve({ ok: true } as Response); // Fallback }); window.history.pushState({}, 'Test page', '/v2/'); @@ -44,7 +45,7 @@ describe('App', () => { value: { href: '' }, }); - (global.fetch as any).mockResolvedValueOnce({ ok: true }); + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true } as Response); fireEvent.click(logoutBtn); diff --git a/frontend/src/components/FeedItem.test.tsx b/frontend/src/components/FeedItem.test.tsx index 4c7d887..1c51dc3 100644 --- a/frontend/src/components/FeedItem.test.tsx +++ b/frontend/src/components/FeedItem.test.tsx @@ -31,7 +31,7 @@ describe('FeedItem Component', () => { }); it('toggles star status', async () => { - (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => ({}) }); + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response); render(); @@ -73,10 +73,10 @@ describe('FeedItem Component', () => { }); it('loads full content', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ ...mockItem, full_content: '

Full Content Loaded

' }), - }); + } as Response); render(); diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx index cf1e708..25ade58 100644 --- a/frontend/src/components/FeedItems.test.tsx +++ b/frontend/src/components/FeedItems.test.tsx @@ -17,11 +17,12 @@ describe('FeedItems Component', () => { unobserve = vi.fn(); disconnect = vi.fn(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any window.IntersectionObserver = MockIntersectionObserver as any; }); it('renders loading state', () => { - (global.fetch as any).mockImplementation(() => new Promise(() => { })); + vi.mocked(global.fetch).mockImplementation(() => new Promise(() => { })); render( @@ -50,10 +51,10 @@ describe('FeedItems Component', () => { }, ]; - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => mockItems, - }); + } as Response); render( @@ -79,10 +80,10 @@ describe('FeedItems Component', () => { { _id: 102, title: 'Item 2', url: 'u2', read: true, starred: false }, ]; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => mockItems, - }); + } as Response); render( @@ -134,10 +135,10 @@ describe('FeedItems Component', () => { it('marks items as read when scrolled past', async () => { const mockItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }]; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => mockItems, - }); + } as Response); // Capture both callbacks const observerCallbacks: IntersectionObserverCallback[] = []; @@ -151,8 +152,10 @@ describe('FeedItems Component', () => { unobserve = vi.fn(); disconnect = vi.fn(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any window.IntersectionObserver = MockIntersectionObserver as any; + render( @@ -201,9 +204,9 @@ describe('FeedItems Component', () => { const initialItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: true, starred: false }]; const moreItems = [{ _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => initialItems }) - .mockResolvedValueOnce({ ok: true, json: async () => moreItems }); + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => initialItems } as Response) + .mockResolvedValueOnce({ ok: true, json: async () => moreItems } as Response); const observerCallbacks: IntersectionObserverCallback[] = []; class MockIntersectionObserver { @@ -214,8 +217,10 @@ describe('FeedItems Component', () => { unobserve = vi.fn(); disconnect = vi.fn(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any window.IntersectionObserver = MockIntersectionObserver as any; + render( @@ -267,9 +272,9 @@ describe('FeedItems Component', () => { { _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }, ]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => initialItems }) - .mockResolvedValueOnce({ ok: true, json: async () => moreItems }); + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => initialItems } as Response) + .mockResolvedValueOnce({ ok: true, json: async () => moreItems } as Response); render( diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx index f43852b..8c69905 100644 --- a/frontend/src/components/FeedItems.tsx +++ b/frontend/src/components/FeedItems.tsx @@ -89,6 +89,7 @@ export default function FeedItems() { useEffect(() => { fetchItems(); setSelectedIndex(-1); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [feedId, tagName, filterFn, searchParams]); @@ -166,6 +167,7 @@ export default function FeedItems() { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, hasMore, loadingMore]); @@ -214,6 +216,7 @@ export default function FeedItems() { itemObserver.disconnect(); sentinelObserver.disconnect(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, loadingMore, hasMore]); if (loading) return
Loading items...
; diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx index 059d8a4..9ef2349 100644 --- a/frontend/src/components/FeedList.test.tsx +++ b/frontend/src/components/FeedList.test.tsx @@ -13,11 +13,15 @@ describe('FeedList Component', () => { }); it('renders loading state initially', () => { - (global.fetch as any).mockImplementation(() => new Promise(() => { })); + vi.mocked(global.fetch).mockImplementation(() => new Promise(() => { })); render( - {/* @ts-ignore */} - { }} /> + { }} + setSidebarVisible={() => { }} + isMobile={false} + /> ); expect(screen.getByText(/loading feeds/i)).toBeInTheDocument(); @@ -41,26 +45,31 @@ describe('FeedList Component', () => { }, ]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => mockFeeds, - }); + } as Response); } - if (url.includes('/api/tag')) { + if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => [{ title: 'Tech' }], - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( - {/* @ts-ignore */} - { }} /> + { }} + setSidebarVisible={() => { }} + isMobile={false} + /> ); @@ -80,12 +89,16 @@ describe('FeedList Component', () => { }); it('handles fetch error', async () => { - (global.fetch as any).mockImplementation(() => Promise.reject(new Error('API Error'))); + vi.mocked(global.fetch).mockImplementation(() => Promise.reject(new Error('API Error'))); render( - {/* @ts-ignore */} - { }} setSidebarVisible={() => { }} /> + { }} + setSidebarVisible={() => { }} + isMobile={false} + /> ); @@ -95,26 +108,31 @@ describe('FeedList Component', () => { }); it('handles empty feed list', async () => { - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => [], - }); + } as Response); } - if (url.includes('/api/tag')) { + if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => [], - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( - {/* @ts-ignore */} - { }} setSidebarVisible={() => { }} /> + { }} + setSidebarVisible={() => { }} + isMobile={false} + /> ); diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index a4ecccf..fed9196 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -66,11 +66,11 @@ export default function FeedList({ Promise.all([ apiFetch('/api/feed/').then((res) => { if (!res.ok) throw new Error('Failed to fetch feeds'); - return res.json(); + return res.json() as Promise; }), apiFetch('/api/tag').then((res) => { if (!res.ok) throw new Error('Failed to fetch tags'); - return res.json(); + return res.json() as Promise; }), ]) .then(([feedsData, tagsData]) => { diff --git a/frontend/src/components/Login.test.tsx b/frontend/src/components/Login.test.tsx index aea7042..cf69eb1 100644 --- a/frontend/src/components/Login.test.tsx +++ b/frontend/src/components/Login.test.tsx @@ -28,9 +28,9 @@ describe('Login Component', () => { }); it('handles successful login', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, - }); + } as Response); renderLogin(); @@ -51,10 +51,10 @@ describe('Login Component', () => { }); it('handles failed login', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, json: async () => ({ message: 'Bad credentials' }), - }); + } as Response); renderLogin(); @@ -67,7 +67,7 @@ describe('Login Component', () => { }); it('handles network error', async () => { - (global.fetch as any).mockRejectedValueOnce(new Error('Network error')); + vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error')); renderLogin(); diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index ba2cd96..b62acea 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -29,7 +29,7 @@ export default function Login() { const data = await res.json(); setError(data.message || 'Login failed'); } - } catch (err) { + } catch (_err) { setError('Network error'); } }; diff --git a/frontend/src/components/Settings.test.tsx b/frontend/src/components/Settings.test.tsx index b7de3bb..a0e7de4 100644 --- a/frontend/src/components/Settings.test.tsx +++ b/frontend/src/components/Settings.test.tsx @@ -18,10 +18,10 @@ describe('Settings Component', () => { { _id: 2, title: 'Gaming', url: 'http://gaming.com/rss', category: 'gaming' }, ]; - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => mockFeeds, - }); + } as Response); render(); @@ -33,13 +33,13 @@ describe('Settings Component', () => { }); it('adds a new feed', async () => { - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load - .mockResolvedValueOnce({ ok: true, json: async () => ({}) }) // Add feed + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => [] } as Response) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response) // Add feed .mockResolvedValueOnce({ ok: true, json: async () => [{ _id: 3, title: 'New Feed', url: 'http://new.com/rss' }], - }); // Refresh load + } as Response); // Refresh load render(); @@ -75,9 +75,9 @@ describe('Settings Component', () => { { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, ]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds }) // Initial load - .mockResolvedValueOnce({ ok: true }); // Delete + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds } as Response) // Initial load + .mockResolvedValueOnce({ ok: true } as Response); // Delete render(); @@ -100,13 +100,13 @@ describe('Settings Component', () => { }); it('imports an OPML file', async () => { - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load - .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 'ok' }) }) // Import + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => [] } as Response) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 'ok' }) } as Response) // Import .mockResolvedValueOnce({ ok: true, json: async () => [{ _id: 1, title: 'Imported Feed', url: 'http://imported.com/rss' }], - }); // Refresh load + } as Response); // Refresh load render(); diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index c174c32..b218775 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -18,7 +18,7 @@ export default function Settings({ fontTheme, setFontTheme }: SettingsProps) { const [importFile, setImportFile] = useState(null); /* ... existing fetchFeeds ... */ - const fetchFeeds = () => { + const fetchFeeds = React.useCallback(() => { setLoading(true); apiFetch('/api/feed/') .then((res) => { @@ -33,11 +33,12 @@ export default function Settings({ fontTheme, setFontTheme }: SettingsProps) { setError(err.message); setLoading(false); }); - }; + }, []); useEffect(() => { + // eslint-disable-next-line fetchFeeds(); - }, []); + }, [fetchFeeds]); /* ... existing handlers ... */ const handleAddFeed = (e: React.FormEvent) => { diff --git a/frontend/src/components/TagView.test.tsx b/frontend/src/components/TagView.test.tsx index 8f7eb86..16fdee7 100644 --- a/frontend/src/components/TagView.test.tsx +++ b/frontend/src/components/TagView.test.tsx @@ -17,25 +17,31 @@ describe('Tag View Integration', () => { ]; const mockTags = [{ title: 'Tech' }, { title: 'News' }]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => mockFeeds, - }); + } as Response); } - if (url.includes('/api/tag')) { + if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => mockTags, - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( - { }} /> + { }} + setSidebarVisible={() => { }} + isMobile={false} + /> ); @@ -55,12 +61,13 @@ describe('Tag View Integration', () => { { _id: 101, title: 'Tag Item 1', url: 'http://example.com/1', feed_title: 'Feed 1' }, ]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/stream')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/stream')) { return Promise.resolve({ ok: true, json: async () => mockItems, - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); -- cgit v1.2.3