From 189de78203be6b07c81622a4b641144352d8a82b Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 21:16:58 -0800 Subject: feat: add username field to login for better password manager support --- frontend/src/components/Login.test.tsx | 12 ++++++++++++ frontend/src/components/Login.tsx | 11 +++++++++++ 2 files changed, 23 insertions(+) (limited to 'frontend/src') diff --git a/frontend/src/components/Login.test.tsx b/frontend/src/components/Login.test.tsx index cf69eb1..47f37e3 100644 --- a/frontend/src/components/Login.test.tsx +++ b/frontend/src/components/Login.test.tsx @@ -23,6 +23,7 @@ describe('Login Component', () => { it('renders login form', () => { renderLogin(); + expect(screen.getByLabelText(/username/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); }); @@ -34,6 +35,7 @@ describe('Login Component', () => { renderLogin(); + fireEvent.change(screen.getByLabelText(/username/i), { target: { value: 'testuser' } }); fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } }); fireEvent.click(screen.getByRole('button', { name: /login/i })); @@ -42,9 +44,17 @@ describe('Login Component', () => { '/api/login', expect.objectContaining({ method: 'POST', + body: expect.any(URLSearchParams), }) ); }); + + // Check if params contained username and password + const callArgs = vi.mocked(global.fetch).mock.calls[0][1]; + const body = callArgs?.body as URLSearchParams; + expect(body.get('username')).toBe('testuser'); + expect(body.get('password')).toBe('secret'); + // Navigation assertion is tricky without mocking useNavigate, // but if no error is shown, we assume success path was taken expect(screen.queryByText(/login failed/i)).not.toBeInTheDocument(); @@ -58,6 +68,7 @@ describe('Login Component', () => { renderLogin(); + fireEvent.change(screen.getByLabelText(/username/i), { target: { value: 'testuser' } }); fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'wrong' } }); fireEvent.click(screen.getByRole('button', { name: /login/i })); @@ -71,6 +82,7 @@ describe('Login Component', () => { renderLogin(); + fireEvent.change(screen.getByLabelText(/username/i), { target: { value: 'testuser' } }); fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } }); fireEvent.click(screen.getByRole('button', { name: /login/i })); diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index b62acea..87694cb 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -5,6 +5,7 @@ import './Login.css'; import { apiFetch } from '../utils'; export default function Login() { + const [username, setUsername] = useState('neko'); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const navigate = useNavigate(); @@ -16,6 +17,7 @@ export default function Login() { try { // Use URLSearchParams to send as form-urlencoded, matching backend expectation const params = new URLSearchParams(); + params.append('username', username); params.append('password', password); const res = await apiFetch('/api/login', { @@ -38,6 +40,15 @@ export default function Login() {

neko rss mode

+
+ + setUsername(e.target.value)} + /> +