Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
72 lines
3.3 KiB
TypeScript
72 lines
3.3 KiB
TypeScript
// Shared navigation helpers. Every spec used to inline these; they live
|
|
// here so a change to the app shell is a one-file fix.
|
|
|
|
import { expect, type Page } from '@playwright/test';
|
|
|
|
export const DEV_PASSWORD = 'pablan-dev';
|
|
|
|
export async function login(page: Page, email: string, password = DEV_PASSWORD) {
|
|
await page.goto('/login');
|
|
await page.locator('body[data-hydrated]').waitFor();
|
|
await page.getByLabel('Email').fill(email);
|
|
await page.getByLabel('Password').fill(password);
|
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
await expect(page).toHaveURL('/');
|
|
// The specs assert English copy and pin it with Accept-Language, which is
|
|
// the strategy for a visitor with no ACCOUNT preference. A language picked
|
|
// in the settings dialog outranks it (that is the product rule), so a dev
|
|
// stack where somebody switched to German would fail every spec that reads
|
|
// a label. Hand the account back to "follow the browser" on the way in.
|
|
await page.request.put('/api/account/locale', { data: { locale: null } });
|
|
// Wait for the landing page to hydrate: filling an input before Svelte
|
|
// binds it leaves the state empty and submit buttons disabled.
|
|
await page.locator('body[data-hydrated]').waitFor();
|
|
}
|
|
|
|
/** Log out through the sidebar user menu. */
|
|
export async function logout(page: Page) {
|
|
// Login/logout are full document reloads (session state must not survive
|
|
// the boundary), so the page may be freshly server-rendered and not yet
|
|
// interactive when a test reaches here; a click before hydration does
|
|
// nothing. A real user is far slower than hydration.
|
|
await page.locator('body[data-hydrated]').waitFor();
|
|
await page.getByTestId('user-menu').click();
|
|
await page.getByTestId('logout').click();
|
|
await expect(page).toHaveURL(/\/login$/);
|
|
}
|
|
|
|
export async function openChat(page: Page) {
|
|
// By testid: an untitled conversation row carries the same label.
|
|
await page.getByTestId('sidebar-new-conversation').click();
|
|
await expect(page).toHaveURL(/\/chat/);
|
|
}
|
|
|
|
export async function openDocuments(page: Page) {
|
|
await page.getByTestId('sidebar').getByRole('link', { name: 'Documents' }).click();
|
|
await expect(page).toHaveURL(/\/documents$/);
|
|
}
|
|
|
|
export async function openAdmin(page: Page) {
|
|
await page.getByTestId('sidebar').getByRole('link', { name: 'Admin' }).click();
|
|
await expect(page).toHaveURL(/\/admin$/);
|
|
}
|
|
|
|
/** The admin page is four unrelated jobs behind four tabs; every spec has to
|
|
* say which one it is doing.
|
|
*
|
|
* By testid, not by label: the account's own language setting outranks the
|
|
* Accept-Language this suite pins (that is the product rule — a language a
|
|
* person chose follows them), and the admin used here has picked one. */
|
|
export async function openAdminTab(page: Page, tab: 'people' | 'templates' | 'llm' | 'prompts') {
|
|
await page.locator('body[data-hydrated]').waitFor();
|
|
await page.getByTestId(`tab-${tab}`).click();
|
|
}
|
|
|
|
/** Call right after triggering a turn: waits for it to actually start (the
|
|
* stop button appears) and then to finish. Waiting on the send button alone
|
|
* races, because it is still visible for a moment after the click. */
|
|
export async function waitForTurnEnd(page: Page) {
|
|
await expect(page.getByTestId('stop-button')).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByTestId('stop-button')).toHaveCount(0, { timeout: 120_000 });
|
|
}
|