test(e2e): expand coverage and harden cross-file isolation
New specs (4):
- accept-invite: pre-registrant flow happy path + cadence/preset UX
- admin-pre-registrants: list, filter, action gating, redirect
- admin-series: list, create, edit (delete skipped — button no-ops)
- admin-site-content: list whitelist, edit + roundtrip on /
Extended specs (6):
- join-flow: cadence ×12 math, guidance label, paid-tier success
- events: series-pass-required, member-savings gating
- admin-events: full CRUD via /admin/events/create?edit=<id>
- admin-members: add-member submit, status select, detail nav
- a11y: add /accept-invite, /member/account, /board, /admin/pre-registrants
- wave-slack-onboarding: 9 of 16 scaffold tests now passing
Cross-file isolation hardening:
- admin-events CRUD: refresh auth cookie (auth.spec.js logout test
bumps tokenVersion on the shared admin), wait for hydration
before form fill, search by unique title to dodge pagination.
- board: switch memberPage from shared admin to dedicated seeded
member to avoid the same tokenVersion race.
- wave-slack §6.4: create dedicated test member, filter by email
before clicking, removing the "first row" anchor.
Also fixed board heading drift ("Board" → "Bulletin Board").
This commit is contained in:
parent
03dfdab20e
commit
8dd55ccc09
11 changed files with 1077 additions and 89 deletions
|
|
@ -53,3 +53,116 @@ test.describe('Admin events access control', () => {
|
|||
expect(page.url()).not.toContain('/admin/events')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Admin events CRUD', () => {
|
||||
test('create, edit, and delete an event', async ({ adminPage }) => {
|
||||
const suffix = Date.now().toString().slice(-6)
|
||||
const title = `e2e-event-${suffix}`
|
||||
const editedTitle = `e2e-event-${suffix}-edited`
|
||||
|
||||
// Re-prime the auth cookie immediately before this multi-step flow.
|
||||
// The shared test-admin account's tokenVersion is bumped whenever
|
||||
// auth.spec.js's logout test runs in parallel, which would otherwise
|
||||
// surface mid-flow as "Session has been revoked" on the first POST.
|
||||
const loginRes = await adminPage.context().request.get('/api/dev/test-login', { maxRedirects: 0 })
|
||||
if (loginRes.status() !== 302) {
|
||||
throw new Error(`Failed to refresh admin session: ${loginRes.status()}`)
|
||||
}
|
||||
|
||||
// --- Create ---
|
||||
await adminPage.goto('/admin/events/create')
|
||||
await expect(adminPage.locator('h1')).toContainText('Create Event')
|
||||
// Ensure Vue has hydrated (initial $fetch for series/tags has resolved)
|
||||
// before interacting — under cross-file load, hydration can lag and a
|
||||
// pre-hydration submit will native-POST against an empty form.
|
||||
await adminPage.waitForLoadState('networkidle')
|
||||
|
||||
await adminPage
|
||||
.getByPlaceholder('Enter a clear, descriptive event title')
|
||||
.fill(title)
|
||||
|
||||
await adminPage
|
||||
.getByPlaceholder(
|
||||
'Provide a clear description of what attendees can expect from this event'
|
||||
)
|
||||
.fill('e2e test event description')
|
||||
|
||||
await adminPage
|
||||
.getByPlaceholder('e.g., https://zoom.us/j/123... or #channel-name')
|
||||
.fill('https://example.com/zoom')
|
||||
|
||||
const startInput = adminPage.getByPlaceholder(
|
||||
"e.g., 'tomorrow at 3pm', 'next Friday at 9am'"
|
||||
)
|
||||
await startInput.fill('next Tuesday at 3pm')
|
||||
await startInput.blur()
|
||||
|
||||
const endInput = adminPage.getByPlaceholder(
|
||||
"e.g., 'tomorrow at 5pm', 'next Friday at 11am'"
|
||||
)
|
||||
await endInput.fill('next Tuesday at 5pm')
|
||||
await endInput.blur()
|
||||
|
||||
await adminPage.getByRole('button', { name: 'Create Event' }).click()
|
||||
|
||||
// The form posts via $fetch and then auto-redirects after a 1.5s setTimeout.
|
||||
// Under cross-file load that auto-redirect can race against waitForURL.
|
||||
// Wait for the surfaced success/error state, fail fast on error, then
|
||||
// navigate explicitly so subsequent assertions are deterministic.
|
||||
await expect(
|
||||
adminPage.locator('.success-box').or(adminPage.locator('.error-box'))
|
||||
).toBeVisible({ timeout: 15000 })
|
||||
await expect(adminPage.locator('.success-box')).toBeVisible()
|
||||
await adminPage.goto('/admin/events')
|
||||
await adminPage.waitForLoadState('networkidle')
|
||||
|
||||
// Filter to just our event — orphan rows from prior failed runs can push
|
||||
// the new row off page 1 of the paginated list.
|
||||
await adminPage.getByPlaceholder('Search events...').fill(title)
|
||||
const row = adminPage.locator('tr', { hasText: title })
|
||||
await expect(row).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// --- Edit ---
|
||||
// Find the event ID from the row's "View" link (href is /events/<slug-or-id>),
|
||||
// and use the row's Edit button. Pair the click with waitForURL so we don't
|
||||
// miss the navigation event under load.
|
||||
await Promise.all([
|
||||
adminPage.waitForURL(/\/admin\/events\/create\?edit=/, { timeout: 15000 }),
|
||||
row.getByRole('button', { name: 'Edit' }).click(),
|
||||
])
|
||||
await expect(adminPage.locator('h1')).toContainText('Edit Event')
|
||||
|
||||
const titleInput = adminPage.getByPlaceholder(
|
||||
'Enter a clear, descriptive event title'
|
||||
)
|
||||
await titleInput.fill(editedTitle)
|
||||
|
||||
await adminPage.getByRole('button', { name: 'Update Event' }).click()
|
||||
|
||||
await expect(
|
||||
adminPage.locator('.success-box').or(adminPage.locator('.error-box'))
|
||||
).toBeVisible({ timeout: 15000 })
|
||||
await expect(adminPage.locator('.success-box')).toBeVisible()
|
||||
await adminPage.goto('/admin/events')
|
||||
await adminPage.waitForLoadState('networkidle')
|
||||
|
||||
// Filter to the edited event's unique title for the same pagination reason.
|
||||
await adminPage.getByPlaceholder('Search events...').fill(editedTitle)
|
||||
const editedRow = adminPage.locator('tr', { hasText: editedTitle })
|
||||
await expect(editedRow).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// --- Delete (custom modal, not browser dialog) ---
|
||||
await editedRow.getByRole('button', { name: 'Del' }).click()
|
||||
await expect(
|
||||
adminPage.getByRole('heading', { name: 'Delete Event' })
|
||||
).toBeVisible()
|
||||
await adminPage
|
||||
.locator('.modal')
|
||||
.getByRole('button', { name: 'Delete' })
|
||||
.click()
|
||||
|
||||
await expect(
|
||||
adminPage.locator('tr', { hasText: editedTitle })
|
||||
).toHaveCount(0, { timeout: 10000 })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue