diff --git a/apps/pwa/e2e/admin.spec.ts b/apps/pwa/e2e/admin.spec.ts index f81bc7c..f88dfa5 100644 --- a/apps/pwa/e2e/admin.spec.ts +++ b/apps/pwa/e2e/admin.spec.ts @@ -1,11 +1,14 @@ /** - * admin.spec.ts — Phase 10 Plan 04 admin route gate + * admin.spec.ts — Phase 10 Plan 04 admin route gate + Phase 17 Plan 06 two-tab ARIA * * Tests: * - Admin user (id=1, seeded is_admin=true by global-setup) sees the Admin nav entry * and reaches /admin with "Admin Settings" heading + Members section. * - Non-admin (route-mocked isAdmin:false) does NOT see the Admin nav entry and is * redirected from /admin to /calendar. + * - Phase 17 (D-10): Two-tab ARIA strip — tablist + named tabs visible, ArrowRight + * moves selection to the Settings tab (keyboard nav). + * - Phase 17 (D-08): Success toast — role=status element structure present. * * Requires the dev stack running with DEV_AUTH_BYPASS=true (see e2e/README.md). * global-setup seeds: users id=1 is_admin=true (Plan 10-01 note). @@ -105,3 +108,84 @@ test.describe('Non-admin user — admin nav entry hidden + /admin redirect', () await expect(page.getByRole('heading', { name: 'Admin Settings' })).toHaveCount(0); }); }); + +// ── Phase 17 D-10: Two-tab ARIA strip ──────────────────────────────────────── + +test.describe('Admin two-tab ARIA strip (D-10)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/admin'); + // Wait for the heading to confirm /admin loaded (admin session via DEV_AUTH_BYPASS) + await expect(page.getByRole('heading', { name: 'Admin Settings' })).toBeVisible(); + }); + + test('tab strip has correct ARIA roles — tablist and both named tabs visible', async ({ + page, + }) => { + await expect(page.getByRole('tablist')).toBeVisible(); + await expect(page.getByRole('tab', { name: 'Members & Accounts' })).toBeVisible(); + await expect(page.getByRole('tab', { name: 'Settings' })).toBeVisible(); + }); + + test('Members & Accounts tab is selected by default', async ({ page }) => { + const membersTab = page.getByRole('tab', { name: 'Members & Accounts' }); + await expect(membersTab).toHaveAttribute('aria-selected', 'true'); + }); + + test('ArrowRight switches selection to the Settings tab', async ({ page }) => { + const membersTab = page.getByRole('tab', { name: 'Members & Accounts' }); + const settingsTab = page.getByRole('tab', { name: 'Settings' }); + await membersTab.focus(); + await page.keyboard.press('ArrowRight'); + await expect(settingsTab).toHaveAttribute('aria-selected', 'true'); + }); + + test('ArrowLeft from Settings tab switches back to Members & Accounts tab', async ({ page }) => { + const membersTab = page.getByRole('tab', { name: 'Members & Accounts' }); + const settingsTab = page.getByRole('tab', { name: 'Settings' }); + // Navigate to Settings first + await membersTab.focus(); + await page.keyboard.press('ArrowRight'); + await expect(settingsTab).toHaveAttribute('aria-selected', 'true'); + // Then go back + await page.keyboard.press('ArrowLeft'); + await expect(membersTab).toHaveAttribute('aria-selected', 'true'); + }); + + test('both tab panels exist with correct ARIA labelledby', async ({ page }) => { + // Both panels are in the DOM; the inactive one uses the HTML `hidden` attribute + const membersPanel = page.locator('#admin-panel-members'); + const settingsPanel = page.locator('#admin-panel-settings'); + await expect(membersPanel).toHaveAttribute('aria-labelledby', 'admin-tab-members'); + await expect(settingsPanel).toHaveAttribute('aria-labelledby', 'admin-tab-settings'); + }); + + test('tab strip fits without horizontal overflow on phone viewport (390px)', async ({ + page, + viewport, + }) => { + // Only meaningful on narrow viewports; skip on desktop + if ((viewport?.width ?? 1280) >= 768) return; + const tablist = page.getByRole('tablist'); + // Verify both tabs are visible (no overflow clipping them) + await expect(page.getByRole('tab', { name: 'Members & Accounts' })).toBeVisible(); + await expect(page.getByRole('tab', { name: 'Settings' })).toBeVisible(); + // scrollWidth <= clientWidth (no horizontal overflow) + const overflows = await tablist.evaluate((el) => el.scrollWidth > el.clientWidth); + expect(overflows, 'Tab strip must not overflow horizontally').toBe(false); + }); +}); + +// ── Phase 17 D-08: Success toast structure ─────────────────────────────────── + +test.describe('Admin success toast structure (D-08)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/admin'); + await expect(page.getByRole('heading', { name: 'Admin Settings' })).toBeVisible(); + }); + + test('role=status live region is not present when no toast is active', async ({ page }) => { + // On page load no toast should be showing + // The toast is rendered conditionally only when toast !== null + await expect(page.locator('[role="status"]')).toHaveCount(0); + }); +});