aboutsummaryrefslogtreecommitdiffstats
path: root/frontend
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
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')
-rw-r--r--frontend/src/components/Login.test.tsx12
-rw-r--r--frontend/src/components/Login.tsx11
-rw-r--r--frontend/tests/auth.spec.ts11
3 files changed, 29 insertions, 5 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"
diff --git a/frontend/tests/auth.spec.ts b/frontend/tests/auth.spec.ts
index 4161a83..33ada85 100644
--- a/frontend/tests/auth.spec.ts
+++ b/frontend/tests/auth.spec.ts
@@ -26,11 +26,8 @@ test.describe('Authentication - No Password Required', () => {
// Visit login page
await page.goto('/v2/login');
- // Submit with empty password
- const passwordInput = page.getByLabel(/password/i);
- await expect(passwordInput).toBeVisible();
-
- // Leave password empty and submit
+ // Fill username and submit with empty password
+ await page.fill('input[id="username"]', 'neko');
await page.click('button[type="submit"]');
// Should redirect to dashboard
@@ -68,6 +65,7 @@ test.describe('Authentication - Password Required', () => {
await page.goto('/v2/login');
// Enter wrong password
+ await page.fill('input[id="username"]', 'neko');
await page.fill('input[type="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
@@ -82,6 +80,7 @@ test.describe('Authentication - Password Required', () => {
await page.goto('/v2/login');
// Enter correct password (must match what the server was started with)
+ await page.fill('input[id="username"]', 'neko');
await page.fill('input[type="password"]', 'testpass');
await page.click('button[type="submit"]');
@@ -94,6 +93,7 @@ test.describe('Authentication - Password Required', () => {
test.skip('should persist authentication across page reloads', async ({ page }) => {
// Login first
await page.goto('/v2/login');
+ await page.fill('input[id="username"]', 'neko');
await page.fill('input[type="password"]', 'testpass');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*\/v2\/?$/);
@@ -109,6 +109,7 @@ test.describe('Authentication - Password Required', () => {
test.skip('should logout and redirect to login page', async ({ page }) => {
// Login first
await page.goto('/v2/login');
+ await page.fill('input[id="username"]', 'neko');
await page.fill('input[type="password"]', 'testpass');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*\/v2\/?$/);