test(17-06): add admin two-tab ARIA and keyboard assertions to admin.spec.ts
- Add 'Admin two-tab ARIA strip (D-10)' describe block: tablist visible, both named tabs visible, ArrowRight/ArrowLeft keyboard switching, panel aria-labelledby, phone overflow check - Add 'Admin success toast structure (D-08)' describe: role=status absent on load - Satisfies Wave 0 admin-ARIA CI requirement — 12/12 tests pass on pixel profile
This commit is contained in:
@@ -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:
|
* Tests:
|
||||||
* - Admin user (id=1, seeded is_admin=true by global-setup) sees the Admin nav entry
|
* - 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.
|
* and reaches /admin with "Admin Settings" heading + Members section.
|
||||||
* - Non-admin (route-mocked isAdmin:false) does NOT see the Admin nav entry and is
|
* - Non-admin (route-mocked isAdmin:false) does NOT see the Admin nav entry and is
|
||||||
* redirected from /admin to /calendar.
|
* 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).
|
* 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).
|
* 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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user