test(admin): Playwright /admin/authors + fix i18n key shape

The earlier en.json shipped namespace-prefixed JSON keys
('ep_admin_authors:title': 'Authors') which is the wrong shape:
i18next splits the lookup on ':' to extract the namespace, then looks
up the bare key in the loaded namespace data. The existing convention
(admin/public/ep_admin_pads/en.json) uses flat keys without the
namespace prefix; matching it makes every
<Trans i18nKey='ep_admin_authors:foo'/> resolve to the intended
translated string. Strings render as English fallback without this
fix; only the page-title test passes (and only by substring accident).

Also adds the Playwright coverage required by Task 8: localized
title, empty-state message on a fresh search tag, disabled banner
toggling with gdprAuthorErasure.enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-05-04 07:42:55 +01:00
parent 35383ba1e9
commit 51ef92a852
2 changed files with 97 additions and 29 deletions

View File

@ -1,31 +1,31 @@
{
"ep_admin_authors:title": "Authors",
"ep_admin_authors:search-placeholder": "Search by name or mapper",
"ep_admin_authors:column.color": "Color",
"ep_admin_authors:column.name": "Name",
"ep_admin_authors:column.mapper": "Mapper",
"ep_admin_authors:column.last-seen": "Last seen",
"ep_admin_authors:column.author-id": "Author ID",
"ep_admin_authors:column.actions": "Actions",
"ep_admin_authors:show-erased": "Show erased authors",
"ep_admin_authors:erase": "Erase",
"ep_admin_authors:erase-disabled-tooltip": "Author erasure is disabled. Set gdprAuthorErasure.enabled = true in settings.json.",
"ep_admin_authors:erased-stub": "(erased)",
"ep_admin_authors:cap-warning": "Showing the first 1000 authors. Narrow your search to see more.",
"ep_admin_authors:feature-disabled-banner": "Author erasure is disabled. Set \"gdprAuthorErasure\": {\"enabled\": true} in settings.json to enable.",
"ep_admin_authors:no-results": "No authors match this search.",
"ep_admin_authors:confirm-preview-title": "Erase author {{name}}",
"ep_admin_authors:confirm-preview-counters": "Will clear {{tokenMappings}} token mappings, {{externalMappings}} mapper bindings, and {{chatMessages}} chat messages across {{affectedPads}} pads.",
"ep_admin_authors:confirm-irreversible": "This cannot be undone.",
"ep_admin_authors:cancel": "Cancel",
"ep_admin_authors:continue": "Continue",
"ep_admin_authors:loading-preview": "Loading preview…",
"ep_admin_authors:erasing": "Erasing…",
"ep_admin_authors:erase-success-toast": "Author {{authorID}} erased.",
"ep_admin_authors:erase-error-toast": "Erase failed: {{error}}",
"ep_admin_authors:no-mappers": "—",
"ep_admin_authors:never-seen": "—",
"ep_admin_authors:prev-page": "Previous Page",
"ep_admin_authors:next-page": "Next Page",
"ep_admin_authors:page-counter": "{{current}} out of {{total}}"
"title": "Authors",
"search-placeholder": "Search by name or mapper",
"column.color": "Color",
"column.name": "Name",
"column.mapper": "Mapper",
"column.last-seen": "Last seen",
"column.author-id": "Author ID",
"column.actions": "Actions",
"show-erased": "Show erased authors",
"erase": "Erase",
"erase-disabled-tooltip": "Author erasure is disabled. Set gdprAuthorErasure.enabled = true in settings.json.",
"erased-stub": "(erased)",
"cap-warning": "Showing the first 1000 authors. Narrow your search to see more.",
"feature-disabled-banner": "Author erasure is disabled. Set \"gdprAuthorErasure\": {\"enabled\": true} in settings.json to enable.",
"no-results": "No authors match this search.",
"confirm-preview-title": "Erase author {{name}}",
"confirm-preview-counters": "Will clear {{tokenMappings}} token mappings, {{externalMappings}} mapper bindings, and {{chatMessages}} chat messages across {{affectedPads}} pads.",
"confirm-irreversible": "This cannot be undone.",
"cancel": "Cancel",
"continue": "Continue",
"loading-preview": "Loading preview…",
"erasing": "Erasing…",
"erase-success-toast": "Author {{authorID}} erased.",
"erase-error-toast": "Erase failed: {{error}}",
"no-mappers": "—",
"never-seen": "—",
"prev-page": "Previous Page",
"next-page": "Next Page",
"page-counter": "{{current}} out of {{total}}"
}

View File

@ -0,0 +1,68 @@
import {expect, test} from "@playwright/test";
import {loginToAdmin, saveSettings} from "../helper/adminhelper";
// /admin tests run serially because they mutate global server state.
test.describe.configure({mode: 'serial'});
const ADMIN_URL = 'http://localhost:9001/admin';
const setErasureFlag = async (page: any, enabled: boolean) => {
await page.goto(`${ADMIN_URL}/settings`);
await page.waitForSelector('.settings');
const settings = page.locator('.settings');
await expect(settings).not.toHaveValue('', {timeout: 30000});
const raw = await settings.inputValue();
const obj = JSON.parse(raw.replace(/\/\*[\s\S]*?\*\//g, ''));
obj.gdprAuthorErasure = {enabled};
await settings.fill(JSON.stringify(obj));
await saveSettings(page);
};
test.describe('admin authors page', () => {
test.beforeEach(async ({page}) => {
await loginToAdmin(page, 'admin', 'changeme1');
});
test('renders the localized page title', async ({page}) => {
await page.goto(`${ADMIN_URL}/authors`);
await expect(page.getByRole('heading', {name: 'Authors'}))
.toBeVisible({timeout: 30000});
});
test('search filters the table to a matching author', async ({page}) => {
const tag = `pw-${Date.now()}`;
await page.goto(`${ADMIN_URL}/authors`);
await page.waitForSelector('table');
const search = page.getByPlaceholder('Search by name or mapper');
await search.fill(tag);
await expect(page.getByText('No authors match this search.'))
.toBeVisible({timeout: 5000});
});
test('disabled banner shows when gdprAuthorErasure.enabled = false',
async ({page}) => {
await setErasureFlag(page, false);
await page.goto(`${ADMIN_URL}/authors`);
await expect(page.getByRole('alert'))
.toContainText('Author erasure is disabled.', {timeout: 30000});
});
test('disabled banner is hidden when gdprAuthorErasure.enabled = true',
async ({page}) => {
await setErasureFlag(page, true);
await page.goto(`${ADMIN_URL}/authors`);
await page.waitForSelector('table');
await expect(page.getByRole('alert')).toHaveCount(0);
});
test.afterAll(async ({browser}) => {
const ctx = await browser.newContext();
const page = await ctx.newPage();
try {
await loginToAdmin(page, 'admin', 'changeme1');
await setErasureFlag(page, false);
} finally {
await ctx.close();
}
});
});