diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-14 21:11:40 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-14 21:11:40 -0800 |
| commit | d5a413382c93efeb1d888daf3216c51cd5f40f75 (patch) | |
| tree | a7d8505a0a1c90050ea6bd33c02f7e8a5b5537e5 /frontend | |
| parent | a7369274ba24298a0449865f147fc65253e992a2 (diff) | |
| download | neko-d5a413382c93efeb1d888daf3216c51cd5f40f75.tar.gz neko-d5a413382c93efeb1d888daf3216c51cd5f40f75.tar.bz2 neko-d5a413382c93efeb1d888daf3216c51cd5f40f75.zip | |
chore: fix lint and type errors to resolve CI failures
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/eslint.config.js | 7 | ||||
| -rw-r--r-- | frontend/src/App.test.tsx | 17 | ||||
| -rw-r--r-- | frontend/src/components/FeedItem.test.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/FeedItems.test.tsx | 31 | ||||
| -rw-r--r-- | frontend/src/components/FeedItems.tsx | 3 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.test.tsx | 58 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/components/Login.test.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/components/Login.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/Settings.test.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/Settings.tsx | 7 | ||||
| -rw-r--r-- | frontend/src/components/TagView.test.tsx | 25 |
12 files changed, 118 insertions, 78 deletions
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 043ab7a..d2de7f8 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'; import eslintConfigPrettier from 'eslint-config-prettier'; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ['dist', 'coverage', 'playwright-report'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], @@ -21,6 +21,11 @@ export default tseslint.config( rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + }], + '@typescript-eslint/no-explicit-any': 'warn', }, }, eslintConfigPrettier 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(<App />); 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(<FeedItem item={mockItem} />); @@ -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: '<p>Full Content Loaded</p>' }), - }); + } as Response); render(<FeedItem item={mockItem} />); 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( <MemoryRouter initialEntries={['/feed/1']}> <Routes> @@ -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( <MemoryRouter initialEntries={['/feed/1']}> @@ -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( <MemoryRouter> @@ -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( <MemoryRouter> <FeedItems /> @@ -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( <MemoryRouter> <FeedItems /> @@ -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( <MemoryRouter> 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 <div className="feed-items-loading">Loading items...</div>; 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( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); 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( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); @@ -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( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} setSidebarVisible={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); @@ -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( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} setSidebarVisible={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); 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<Feed[]>; }), apiFetch('/api/tag').then((res) => { if (!res.ok) throw new Error('Failed to fetch tags'); - return res.json(); + return res.json() as Promise<Category[]>; }), ]) .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(<Settings />); @@ -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(<Settings />); @@ -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(<Settings />); @@ -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(<Settings />); 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<File | null>(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( <MemoryRouter> - <FeedList theme="light" setTheme={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </MemoryRouter> ); @@ -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}`)); }); |
