aboutsummaryrefslogtreecommitdiffstats
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/eslint.config.js7
-rw-r--r--frontend/src/App.test.tsx17
-rw-r--r--frontend/src/components/FeedItem.test.tsx6
-rw-r--r--frontend/src/components/FeedItems.test.tsx31
-rw-r--r--frontend/src/components/FeedItems.tsx3
-rw-r--r--frontend/src/components/FeedList.test.tsx58
-rw-r--r--frontend/src/components/FeedList.tsx4
-rw-r--r--frontend/src/components/Login.test.tsx10
-rw-r--r--frontend/src/components/Login.tsx2
-rw-r--r--frontend/src/components/Settings.test.tsx26
-rw-r--r--frontend/src/components/Settings.tsx7
-rw-r--r--frontend/src/components/TagView.test.tsx25
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}`));
});