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
|
|
@ -67,3 +67,128 @@ test.describe('Events list page', () => {
|
|||
await expect(page.locator('h1')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
async function navigateToFirstEventDetail(page) {
|
||||
await page.goto('/events')
|
||||
await page.locator('.past-toggle').click()
|
||||
await page.waitForLoadState('networkidle')
|
||||
const eventLinks = page.locator('.event-row a')
|
||||
const count = await eventLinks.count()
|
||||
if (count === 0) return null
|
||||
const href = await eventLinks.first().getAttribute('href')
|
||||
return href
|
||||
}
|
||||
|
||||
test.describe('Event detail — ticket gating', () => {
|
||||
test('series-pass-required shows pass-required notice instead of buy button', async ({ page }) => {
|
||||
const href = await navigateToFirstEventDetail(page)
|
||||
test.skip(!href, 'No events in dev DB to navigate against')
|
||||
|
||||
await page.route('**/api/events/*/tickets/available**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
available: false,
|
||||
reason: 'series_pass_required',
|
||||
requiresSeriesPass: true,
|
||||
series: { id: 'series-stub', slug: 'series-stub', title: 'Stub Series' }
|
||||
})
|
||||
})
|
||||
})
|
||||
await page.route('**/api/events/*/check-series-access**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ requiresSeriesPass: false })
|
||||
})
|
||||
})
|
||||
|
||||
await page.locator(`.event-row a[href="${href}"]`).first().click()
|
||||
await page.waitForURL(`**${href}`)
|
||||
|
||||
const ticketPanel = page.locator('.event-ticket-purchase')
|
||||
await expect(ticketPanel.locator('.ticket-status', { hasText: 'Series Pass Required' })).toBeVisible()
|
||||
await expect(ticketPanel.locator('button', { hasText: /Pay |Register for this event|Complete Registration/ })).toHaveCount(0)
|
||||
await expect(ticketPanel.locator('a[href="/series/series-stub"] button')).toBeVisible()
|
||||
})
|
||||
|
||||
test('memberSavings line is hidden for anonymous viewers', async ({ page }) => {
|
||||
const href = await navigateToFirstEventDetail(page)
|
||||
test.skip(!href, 'No events in dev DB to navigate against')
|
||||
|
||||
await page.route('**/api/events/*/tickets/available**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
available: true,
|
||||
alreadyRegistered: false,
|
||||
isFree: false,
|
||||
isMember: false,
|
||||
name: 'General Admission',
|
||||
formattedPrice: '$25.00',
|
||||
remaining: 10,
|
||||
memberSavings: 0,
|
||||
publicTicket: null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await page.locator(`.event-row a[href="${href}"]`).first().click()
|
||||
await page.waitForURL(`**${href}`)
|
||||
|
||||
const ticketCard = page.locator('.ticket-card')
|
||||
await expect(ticketCard).toBeVisible()
|
||||
await expect(page.locator('.ticket-savings')).toHaveCount(0)
|
||||
await expect(page.locator('text=/save .* as a member/i')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('memberSavings line is shown when API reports savings', async ({ page }) => {
|
||||
const href = await navigateToFirstEventDetail(page)
|
||||
test.skip(!href, 'No events in dev DB to navigate against')
|
||||
|
||||
await page.route('**/api/events/*/tickets/available**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
available: true,
|
||||
alreadyRegistered: false,
|
||||
isFree: false,
|
||||
isMember: true,
|
||||
name: 'Member Ticket',
|
||||
formattedPrice: '$10.00',
|
||||
remaining: 10,
|
||||
memberSavings: 15,
|
||||
publicTicket: { formattedPrice: '$25.00' }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await page.locator(`.event-row a[href="${href}"]`).first().click()
|
||||
await page.waitForURL(`**${href}`)
|
||||
|
||||
const savings = page.locator('.ticket-savings')
|
||||
await expect(savings).toBeVisible()
|
||||
await expect(savings).toContainText(/save/i)
|
||||
})
|
||||
|
||||
test.skip('hidden event returns 404', async () => {
|
||||
// Skipped: hidden-event gating happens during SSR useFetch in [slug].vue,
|
||||
// which page.route cannot intercept. Verifying this gate requires either
|
||||
// seeding a hidden event in the dev DB or a server-side mock layer.
|
||||
})
|
||||
|
||||
test.skip('past-deadline event shows registration-closed copy', async () => {
|
||||
// Skipped: when the available endpoint returns reason
|
||||
// "Registration deadline has passed", the current UI surfaces it as the
|
||||
// generic "Event Sold Out" panel — there is no distinct "Registration
|
||||
// closed" string to assert against without changing the component.
|
||||
})
|
||||
|
||||
test.skip('member with paid registration cannot self-cancel', async () => {
|
||||
// Skipped: requires seeding an authed member with a paid registration in
|
||||
// the DB, which is out of scope for API-level mocking.
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue