aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-14 21:16:58 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-14 21:16:58 -0800
commit189de78203be6b07c81622a4b641144352d8a82b (patch)
tree41562388a7bfc9460da3b76830433e7af30e9325 /frontend/src
parent25f817030dc2bf2c8d53f7c57c17a387263be667 (diff)
downloadneko-189de78203be6b07c81622a4b641144352d8a82b.tar.gz
neko-189de78203be6b07c81622a4b641144352d8a82b.tar.bz2
neko-189de78203be6b07c81622a4b641144352d8a82b.zip
feat: add username field to login for better password manager support
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/Login.test.tsx12
-rw-r--r--frontend/src/components/Login.tsx11
2 files changed, 23 insertions, 0 deletions
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', {
@@ -39,6 +41,15 @@ export default function Login() {
<form onSubmit={handleSubmit} className="login-form">
<h1>neko rss mode</h1>
<div className="form-group">
+ <label htmlFor="username">username</label>
+ <input
+ id="username"
+ type="text"
+ value={username}
+ onChange={(e) => setUsername(e.target.value)}
+ />
+ </div>
+ <div className="form-group">
<label htmlFor="password">password</label>
<input
id="password"