5.9 KiB
5.9 KiB
Task 1: Login Page And App-Level Session
Files:
- Modify:
apps/web/src/features/auth/ServerLogin.svelte - Modify:
apps/web/src/app/App.svelte - Test:
apps/web/src/features/auth/ServerLogin.test.ts
Interfaces:
-
Produces:
ServerLoginSvelte component eventlogin: { apiBase: string; account: string } -
Produces: app state fields
apiBase: string,currentUser: { account: string } | null -
Consumes:
localStorage['apiBase'] -
Step 1: Write failing login component tests
Create apps/web/src/features/auth/ServerLogin.test.ts:
import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';
import ServerLogin from './ServerLogin.svelte';
describe('ServerLogin', () => {
it('renders server, account, and password fields', () => {
render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
expect(screen.getByLabelText('Server IP or domain')).toBeInTheDocument();
expect(screen.getByLabelText('Email or username')).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
});
it('normalizes server address and emits login details', async () => {
const onLogin = vi.fn();
render(ServerLogin, {
props: {
apiBase: 'localhost:8080',
onLogin,
},
});
await fireEvent.input(screen.getByLabelText('Server IP or domain'), {
target: { value: '10.0.0.12:8080' },
});
await fireEvent.input(screen.getByLabelText('Email or username'), {
target: { value: 'david@example.com' },
});
await fireEvent.input(screen.getByLabelText('Password'), {
target: { value: 'secret' },
});
await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
expect(onLogin).toHaveBeenCalledWith({
apiBase: 'http://10.0.0.12:8080',
account: 'david@example.com',
});
});
it('shows an error when account or password is missing', async () => {
render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
expect(screen.getByText('Enter an account and password.')).toBeInTheDocument();
});
});
- Step 2: Run the new test to verify it fails
Run:
Set-Location D:\work\senlinai\agent\apps\web
npm test -- --run src/features/auth/ServerLogin.test.ts
Expected: FAIL because ServerLogin does not render account/password fields and does not expose onLogin.
- Step 3: Replace
ServerLogin.sveltewith the complete login page
Implement apps/web/src/features/auth/ServerLogin.svelte:
<script lang="ts">
export let apiBase: string;
export let onLogin: (detail: { apiBase: string; account: string }) => void = () => {};
let server = apiBase;
let account = '';
let password = '';
let error = '';
function normalizeServer(value: string) {
const trimmed = value.trim();
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
return trimmed;
}
return `http://${trimmed}`;
}
function submitLogin() {
error = '';
if (!account.trim() || !password.trim()) {
error = 'Enter an account and password.';
return;
}
const normalized = normalizeServer(server);
server = normalized;
onLogin({ apiBase: normalized, account: account.trim() });
}
</script>
<main class="login-page">
<section class="login-panel" aria-labelledby="login-title">
<div class="login-brand" aria-hidden="true">SA</div>
<div class="login-heading">
<p>Private workbench</p>
<h1 id="login-title">SenlinAI Workbench</h1>
</div>
<form class="server-login" aria-label="Server login" on:submit|preventDefault={submitLogin}>
<label for="server-address">Server IP or domain</label>
<input id="server-address" name="server" bind:value={server} placeholder="http://localhost:8080" />
<label for="login-account">Email or username</label>
<input id="login-account" name="account" bind:value={account} autocomplete="username" />
<label for="login-password">Password</label>
<input
id="login-password"
name="password"
type="password"
bind:value={password}
autocomplete="current-password"
/>
{#if error}
<p class="form-error" aria-live="polite">{error}</p>
{/if}
<button type="submit">Log in</button>
<p class="login-note">The server address is remembered on this device.</p>
</form>
</section>
</main>
- Step 4: Update
App.svelteto switch from login to workbench
Temporarily render a simple logged-in placeholder; the full workbench arrives in Task 3.
<script lang="ts">
import ServerLogin from '../features/auth/ServerLogin.svelte';
let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
let currentUser: { account: string } | null = null;
function handleLogin(detail: { apiBase: string; account: string }) {
apiBase = detail.apiBase;
localStorage.setItem('apiBase', apiBase);
currentUser = { account: detail.account };
}
</script>
{#if currentUser}
<main class="workbench-placeholder" aria-label="Project workbench">
<h1>SenlinAI Workbench</h1>
<p>Signed in as {currentUser.account}</p>
</main>
{:else}
<ServerLogin {apiBase} onLogin={handleLogin} />
{/if}
- Step 5: Run tests
Run:
Set-Location D:\work\senlinai\agent\apps\web
npm test -- --run src/features/auth/ServerLogin.test.ts
Expected: PASS.
- Step 6: Commit
Run:
Set-Location D:\work\senlinai\agent
git add apps\web\src\features\auth\ServerLogin.svelte apps\web\src\features\auth\ServerLogin.test.ts apps\web\src\app\App.svelte
git commit -m "feat: add full workbench login page"