diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-15 14:22:31 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-15 14:22:31 -0800 |
| commit | cc74caf9d9b66711f4aab793966c1f8a5663e1b9 (patch) | |
| tree | 8602af15cd7e36f1ce1f8a5c9f6753b7f108adbe /frontend/src | |
| parent | 9f64d3a18eb92c4845dac38265acecef2931490b (diff) | |
| download | neko-cc74caf9d9b66711f4aab793966c1f8a5663e1b9.tar.gz neko-cc74caf9d9b66711f4aab793966c1f8a5663e1b9.tar.bz2 neko-cc74caf9d9b66711f4aab793966c1f8a5663e1b9.zip | |
Switch to HashRouter to fix page reload issues (NK-hy162w)
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/App.test.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 8 | ||||
| -rw-r--r-- | frontend/src/Navigation.test.tsx | 45 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.test.tsx | 3 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.tsx | 2 |
5 files changed, 37 insertions, 25 deletions
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 1ef9763..27b9da2 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -16,7 +16,7 @@ describe('App', () => { } as Response); window.history.pushState({}, 'Test page', '/v2/login'); render(<App />); - expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /login/i })).toBeInTheDocument(); }); it('renders dashboard when authenticated', async () => { @@ -54,7 +54,7 @@ describe('App', () => { '/api/logout', expect.objectContaining({ method: 'POST' }) ); - expect(window.location.href).toBe('/v2/login'); + expect(window.location.href).toBe('/v2/#/login'); }); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 478444c..cc45949 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { HashRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import Login from './components/Login'; import './App.css'; import { apiFetch } from './utils'; @@ -113,10 +113,10 @@ function App() { localStorage.setItem('neko-font-theme', newFontTheme); }; - const basename = window.location.pathname.startsWith('/v2') ? '/v2' : '/'; + return ( - <BrowserRouter basename={basename}> + <HashRouter> <Routes> <Route path="/login" element={<Login />} /> <Route @@ -133,7 +133,7 @@ function App() { } /> </Routes> - </BrowserRouter> + </HashRouter> ); } diff --git a/frontend/src/Navigation.test.tsx b/frontend/src/Navigation.test.tsx index 7d73249..b0bae86 100644 --- a/frontend/src/Navigation.test.tsx +++ b/frontend/src/Navigation.test.tsx @@ -27,7 +27,7 @@ describe('Navigation and Filtering', () => { it('preserves "all" filter when clicking a feed', async () => { Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1024 }); - window.history.pushState({}, '', '/'); + window.history.pushState({}, '', '/#/'); render(<App />); // Wait for sidebar to load and feeds section to be visible @@ -47,7 +47,9 @@ describe('Navigation and Filtering', () => { fireEvent.click(allFilter); // Verify URL has filter=all - expect(window.location.search).toContain('filter=all'); + await waitFor(() => { + expect(window.location.hash).toContain('filter=all'); + }); // Click Feed 1 const feed1Link = screen.getByText('Feed 1'); @@ -55,8 +57,8 @@ describe('Navigation and Filtering', () => { // Verify URL is /feed/1?filter=all (or similar) await waitFor(() => { - expect(window.location.pathname).toContain('/feed/1'); - expect(window.location.search).toContain('filter=all'); + expect(window.location.hash).toContain('/feed/1'); + expect(window.location.hash).toContain('filter=all'); }); // Click Feed 2 @@ -65,48 +67,57 @@ describe('Navigation and Filtering', () => { // Verify URL is /feed/2?filter=all await waitFor(() => { - expect(window.location.pathname).toContain('/feed/2'); - expect(window.location.search).toContain('filter=all'); + expect(window.location.hash).toContain('/feed/2'); + expect(window.location.hash).toContain('filter=all'); }); }); it('highlights the correct filter link', async () => { - window.history.pushState({}, '', '/'); + window.history.pushState({}, '', '/#/'); render(<App />); await waitFor(() => { - expect(screen.getByText('unread')).toHaveClass('active'); + const unreadLink = screen.getByText('unread'); + expect(unreadLink.className).toContain('active'); }); fireEvent.click(screen.getByText('all')); await waitFor(() => { - expect(screen.getByText('all')).toHaveClass('active'); - expect(screen.getByText('unread')).not.toHaveClass('active'); + const allLink = screen.getByText('all'); + const unreadLink = screen.getByText('unread'); + expect(allLink.className).toContain('active'); + expect(unreadLink.className).not.toContain('active'); }); }); it('highlights "unread" as active even when on a feed page without filter param', async () => { - window.history.pushState({}, '', '/feed/1'); + window.history.pushState({}, '', '/#/feed/1'); render(<App />); await waitFor(() => { - expect(screen.getByText('unread')).toHaveClass('active'); + const unreadLink = screen.getByText('unread'); + expect(unreadLink.className).toContain('active'); }); }); it('preserves search query when clicking a feed', async () => { - window.history.pushState({}, '', '/?q=linux'); + window.history.pushState({}, '', '/#/?q=linux'); render(<App />); - await screen.findByRole('heading', { name: /Feeds/i, level: 4 }); - fireEvent.click(screen.getByRole('heading', { name: /Feeds/i, level: 4 })); + // Wait for load + await waitFor(() => { + expect(screen.queryByText(/Loading feeds/i)).not.toBeInTheDocument(); + }); + + const feedsHeader = await screen.findByRole('heading', { name: /Feeds/i, level: 4 }); + fireEvent.click(feedsHeader); await screen.findByText('Feed 1'); fireEvent.click(screen.getByText('Feed 1')); await waitFor(() => { - expect(window.location.pathname).toContain('/feed/1'); - expect(window.location.search).toContain('q=linux'); + expect(window.location.hash).toContain('/feed/1'); + expect(window.location.hash).toContain('q=linux'); }); }); }); diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx index d3d3a57..d4e72cc 100644 --- a/frontend/src/components/FeedList.test.tsx +++ b/frontend/src/components/FeedList.test.tsx @@ -202,8 +202,9 @@ describe('FeedList Component', () => { await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('/api/logout', expect.any(Object)); - expect(window.location.href).toContain('/v2/login'); + expect(window.location.href).toContain('/v2/#/login'); }); + // @ts-expect-error - restoring window.location window.location = originalLocation; }); diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index 556ce6e..ce83333 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -111,7 +111,7 @@ export default function FeedList({ if (error) return <div className="feed-list-error">Error: {error}</div>; const handleLogout = () => { - apiFetch('/api/logout', { method: 'POST' }).then(() => (window.location.href = '/v2/login')); + apiFetch('/api/logout', { method: 'POST' }).then(() => (window.location.href = '/v2/#/login')); }; return ( |
