The general and quota cards registered their react-hook-form fields under
the dotted settings keys. RHF treats dots as nested-path separators, so
the form DISPLAYED fine (its getter falls back to the literal flat key)
but typing nested the value ({ instance: { name } }) and the api's strict
PATCH schema rejected the body — none of these fields ever saved through
the UI, on any instance. Found by Stefan on a fresh self-hosted install.
- admin-settings-form.ts: dot-free form model with one explicit mapping
to the dotted settings keys and converters in both directions; the
submit now also carries ONLY the settings these cards edit, so the
internal branding metadata keys never ride along.
- Saving invalidates the branding query too — the TopBar reads the
instance name from it and kept the old name until its staleTime ran out.
- admin-settings.spec.ts (new e2e pack, registered in ci.yml): drives the
rename THROUGH THE FORM — success message, TopBar update without
reload, value survives reload, api returns it. Verified locally to fail
against the unfixed page and pass against the fix. Every existing
admin-settings test patched the api directly, which is why this bug was
invisible to CI.
- admin-settings-form.test.ts pins that no form field name contains a dot
and the mapping round-trips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aviRTgWCcAHUh1SBoxf6P
55 lines
2.3 KiB
TypeScript
55 lines
2.3 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
import { contextForUser } from './helpers';
|
|
|
|
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
|
|
|
/**
|
|
* The general admin settings card saves THROUGH THE FORM (issue #322).
|
|
*
|
|
* This must drive the UI, not the api: the bug it fences was invisible to
|
|
* every api-level test — react-hook-form nested the dotted field names on
|
|
* input, the strict PATCH schema rejected the body, and the form looked
|
|
* fine while never saving. Verified end to end: success message, the value
|
|
* survives a full reload, the api returns it, and the TopBar picks it up
|
|
* without a reload (branding query invalidation).
|
|
*/
|
|
test('instance name changed in the general settings form persists', async ({ browser }) => {
|
|
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
|
const before = (
|
|
(await (await admin.request.get('/api/v1/admin/settings')).json()) as Record<string, unknown>
|
|
)['instance.name'] as string;
|
|
const newName = `Renamed ${Date.now()}`;
|
|
|
|
const nameLabel = /^(Instance name|Name der Instanz)$/;
|
|
const page = await admin.newPage();
|
|
try {
|
|
await page.goto('/admin');
|
|
const generalCard = page
|
|
.locator('section.settings-section')
|
|
.filter({ has: page.getByLabel(nameLabel) });
|
|
await page.getByLabel(nameLabel).fill(newName);
|
|
await generalCard.getByRole('button', { name: /^(Save|Speichern)$/ }).click();
|
|
// Scoped to the card: the page has several forms with status regions.
|
|
await expect(generalCard.getByRole('status')).toHaveText(/^(Saved\.|Gespeichert\.)$/);
|
|
|
|
// The TopBar shows the new name without a reload — the save invalidates
|
|
// the branding query the TopBar reads from.
|
|
await expect(page.locator('.topbar__brand')).toHaveText(newName);
|
|
|
|
// The proof the form really persisted: the value survives a reload and
|
|
// the api returns it.
|
|
await page.reload();
|
|
await expect(page.getByLabel(nameLabel)).toHaveValue(newName);
|
|
const stored = (
|
|
(await (await admin.request.get('/api/v1/admin/settings')).json()) as Record<string, unknown>
|
|
)['instance.name'];
|
|
expect(stored).toBe(newName);
|
|
} finally {
|
|
await admin.request.patch('/api/v1/admin/settings', {
|
|
data: { 'instance.name': before },
|
|
});
|
|
await admin.close();
|
|
}
|
|
});
|