Pablan, as it stands
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
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { login, DEV_PASSWORD } from './helpers';
|
||||
|
||||
// Uses max@pablan.dev so a failure here cannot lock the other specs out of
|
||||
// pablo@pablan.dev. The password is changed back at the end (zero residue).
|
||||
|
||||
const NEW_PASSWORD = 'ben-neues-geheimnis';
|
||||
|
||||
test('a user changes their own password and stays signed in', async ({ page }) => {
|
||||
await login(page, 'max@pablan.dev');
|
||||
|
||||
await page.getByTestId('user-menu').click();
|
||||
await page.getByTestId('change-password').click();
|
||||
|
||||
await page.getByLabel('Current password').fill(DEV_PASSWORD);
|
||||
await page.getByLabel('New password', { exact: true }).fill(NEW_PASSWORD);
|
||||
await page.getByLabel('Repeat new password').fill(NEW_PASSWORD);
|
||||
await page.getByTestId('submit-password').click();
|
||||
|
||||
await expect(page.getByTestId('password-changed')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// This session survived the change — no redirect to the login page.
|
||||
await page.reload();
|
||||
await page.locator('body[data-hydrated]').waitFor();
|
||||
await expect(page).toHaveURL('/');
|
||||
|
||||
// The new password is the one that works now.
|
||||
await page.request.post('/api/auth/logout');
|
||||
const stale = await page.request.post('/api/auth/login', {
|
||||
data: { email: 'max@pablan.dev', password: DEV_PASSWORD }
|
||||
});
|
||||
expect(stale.status()).toBe(401);
|
||||
|
||||
// Change it back, so the suite can run again.
|
||||
await login(page, 'max@pablan.dev', NEW_PASSWORD);
|
||||
await page.getByTestId('user-menu').click();
|
||||
await page.getByTestId('change-password').click();
|
||||
await page.getByLabel('Current password').fill(NEW_PASSWORD);
|
||||
await page.getByLabel('New password', { exact: true }).fill(DEV_PASSWORD);
|
||||
await page.getByLabel('Repeat new password').fill(DEV_PASSWORD);
|
||||
await page.getByTestId('submit-password').click();
|
||||
await expect(page.getByTestId('password-changed')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await page.request.post('/api/auth/logout');
|
||||
});
|
||||
|
||||
test('the wrong current password is rejected', async ({ page }) => {
|
||||
await login(page, 'max@pablan.dev');
|
||||
|
||||
await page.getByTestId('user-menu').click();
|
||||
await page.getByTestId('change-password').click();
|
||||
await page.getByLabel('Current password').fill('definitely-wrong');
|
||||
await page.getByLabel('New password', { exact: true }).fill(NEW_PASSWORD);
|
||||
await page.getByLabel('Repeat new password').fill(NEW_PASSWORD);
|
||||
await page.getByTestId('submit-password').click();
|
||||
|
||||
await expect(page.getByRole('alert')).toContainText('current password');
|
||||
// The old password still works.
|
||||
await page.request.post('/api/auth/logout');
|
||||
const still = await page.request.post('/api/auth/login', {
|
||||
data: { email: 'max@pablan.dev', password: DEV_PASSWORD }
|
||||
});
|
||||
expect(still.status()).toBe(200);
|
||||
await page.request.post('/api/auth/logout');
|
||||
});
|
||||
|
||||
test('the theme survives a reload without flashing the other one', async ({ page }) => {
|
||||
await login(page, 'max@pablan.dev');
|
||||
|
||||
await page.getByTestId('user-menu').click();
|
||||
await page.getByTestId('settings-dialog').waitFor();
|
||||
await page.getByTestId('theme-switch').getByRole('button', { name: 'Light' }).click();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
||||
|
||||
// The boot script applies it before hydration, so it is already correct
|
||||
// on the very first frame after a reload.
|
||||
await page.reload();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
||||
await page.locator('body[data-hydrated]').waitFor();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
||||
|
||||
// Back to following the OS, so the next spec starts clean.
|
||||
await page.getByTestId('user-menu').click();
|
||||
await page.getByTestId('theme-switch').getByRole('button', { name: 'System' }).click();
|
||||
await expect(page.locator('html')).not.toHaveAttribute('data-theme', /.*/);
|
||||
await page.request.post('/api/auth/logout');
|
||||
});
|
||||
|
||||
test('the language switch flips the interface in place, with no reload', async ({ page }) => {
|
||||
await login(page, 'max@pablan.dev');
|
||||
|
||||
await page.getByTestId('user-menu').click();
|
||||
const dialog = page.getByTestId('settings-dialog');
|
||||
const locales = page.getByTestId('locale-switch');
|
||||
|
||||
// Start from English, whatever a previous run left behind.
|
||||
await locales.getByRole('button', { name: 'English' }).click();
|
||||
await expect(dialog).toContainText('Settings');
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
|
||||
|
||||
// A marker on window survives a re-render but not a reload, and a typed
|
||||
// value survives neither if the DOM is rebuilt: together they are the
|
||||
// assertion that "no reload artifacts" actually holds.
|
||||
await page.evaluate(() => ((window as unknown as Record<string, string>).__i18nMarker = 'alive'));
|
||||
await page.getByTestId('change-password').click();
|
||||
await page.getByLabel('Current password').fill('typed-before-switch');
|
||||
|
||||
await locales.getByRole('button', { name: 'Deutsch' }).click();
|
||||
|
||||
await expect(dialog).toContainText('Einstellungen');
|
||||
await expect(dialog).toContainText('Passwort ändern');
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
|
||||
expect(
|
||||
await page.evaluate(() => (window as unknown as Record<string, string>).__i18nMarker)
|
||||
).toBe('alive');
|
||||
await expect(page.getByLabel('Aktuelles Passwort')).toHaveValue('typed-before-switch');
|
||||
|
||||
// The choice is on the account, not just in the tab.
|
||||
await expect
|
||||
.poll(async () => (await (await page.request.get('/api/auth/me')).json()).locale)
|
||||
.toBe('de');
|
||||
|
||||
// Zero residue: back to following the browser.
|
||||
await locales.getByRole('button', { name: 'Automatisch' }).click();
|
||||
await expect
|
||||
.poll(async () => (await (await page.request.get('/api/auth/me')).json()).locale)
|
||||
.toBe(null);
|
||||
await page.request.post('/api/auth/logout');
|
||||
});
|
||||
Reference in New Issue
Block a user