feat(admin): rename series route and add tags review page

Rename /admin/series-management to /admin/series so it follows the
/admin/<section> convention; the breadcrumb's auto-derived parent link
is now a real route (was a dead /admin/series link).

Add an /admin/tags page to review pending TagSuggestions — list,
approve (creates the Tag), reject — backed by new admin endpoints and a
tagSuggestionReviewSchema. Resolves the dead /admin/tags alert link.
This commit is contained in:
Jennie Robinson Faber 2026-05-24 00:44:14 +01:00
parent 7beb86b430
commit 151481f1ec
9 changed files with 358 additions and 9 deletions

View file

@ -2,7 +2,7 @@ import { test, expect } from './helpers/fixtures.js'
test.describe('Admin series management page', () => {
test('series list loads for admin', async ({ adminPage }) => {
await adminPage.goto('/admin/series-management')
await adminPage.goto('/admin/series')
await expect(adminPage.getByRole('heading', { name: 'Series', level: 1 })).toBeVisible({
timeout: 15000,
})
@ -12,9 +12,9 @@ test.describe('Admin series management page', () => {
test.describe('Admin series access control', () => {
test('non-admin redirect', async ({ page }) => {
await page.goto('/admin/series-management')
await page.goto('/admin/series')
await page.waitForURL((url) => !url.pathname.startsWith('/admin'))
expect(page.url()).not.toContain('/admin/series-management')
expect(page.url()).not.toContain('/admin/series')
})
})
@ -39,7 +39,7 @@ test.describe('Admin series CRUD', () => {
await adminPage.getByRole('button', { name: 'Create Series' }).click()
await adminPage.waitForURL('**/admin/series-management', { timeout: 15000 })
await adminPage.waitForURL('**/admin/series', { timeout: 15000 })
const card = adminPage.locator('.series-card', { hasText: title })
await expect(card).toBeVisible({ timeout: 10000 })

57
e2e/admin-tags.spec.js Normal file
View file

@ -0,0 +1,57 @@
import { test, expect } from './helpers/fixtures.js'
test.describe('Admin tags page', () => {
test('page loads for admin', async ({ adminPage }) => {
await adminPage.goto('/admin/tags')
await expect(adminPage.getByRole('heading', { name: 'Tags', level: 1 })).toBeVisible({
timeout: 15000,
})
})
test('approve and reject pending suggestions', async ({ adminPage }) => {
const suffix = Date.now().toString().slice(-6)
const approveLabel = `e2e-tag-approve-${suffix}`
const rejectLabel = `e2e-tag-reject-${suffix}`
// Load the page first so the csrf-token cookie is set, then seed two
// pending suggestions via the authed member endpoint (state-changing
// requests require the double-submit CSRF header — see middleware/01.csrf.js).
await adminPage.goto('/admin/tags')
const cookies = await adminPage.context().cookies()
const csrf = cookies.find((c) => c.name === 'csrf-token')?.value
await adminPage.request.post('/api/tags/suggest', {
headers: { 'x-csrf-token': csrf },
data: { label: approveLabel, pool: 'craft' },
})
await adminPage.request.post('/api/tags/suggest', {
headers: { 'x-csrf-token': csrf },
data: { label: rejectLabel, pool: 'cooperative' },
})
await adminPage.reload()
await adminPage.waitForLoadState('networkidle')
const approveRow = adminPage.locator('tr', { hasText: approveLabel })
await expect(approveRow).toBeVisible({ timeout: 10000 })
await approveRow.getByRole('button', { name: 'Approve' }).click()
await expect(adminPage.locator('tr', { hasText: approveLabel })).toHaveCount(0, {
timeout: 10000,
})
const rejectRow = adminPage.locator('tr', { hasText: rejectLabel })
await expect(rejectRow).toBeVisible()
await rejectRow.getByRole('button', { name: 'Reject' }).click()
await expect(adminPage.locator('tr', { hasText: rejectLabel })).toHaveCount(0, {
timeout: 10000,
})
})
})
test.describe('Admin tags access control', () => {
test('non-admin redirect', async ({ page }) => {
await page.goto('/admin/tags')
await page.waitForURL((url) => !url.pathname.startsWith('/admin'))
expect(page.url()).not.toContain('/admin/tags')
})
})