/** * admin.spec.ts — Phase 10 Plan 04 admin route gate * * 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. * * 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). * * Route-mock pattern for non-admin simulation: * page.route('/api/me', ...) → { user: { ..., isAdmin: false, needsProviderSetup: false } } * per [[dev-data-user1-no-calendars]] idiom + lists.spec page.route precedent. * * Runs on all three device profiles automatically (playwright.config.ts matrix): * iphone: iPhone 14 / WebKit / 390×844 * pixel: Pixel 7 / Chromium / 412×915 * desktop: Desktop Chrome / Chromium / 1280×720 * * Run: * pnpm --filter @familysync/pwa test:e2e * pnpm --filter @familysync/pwa exec playwright test admin.spec.ts * pnpm --filter @familysync/pwa test:e2e -- admin */ import { test, expect } from '@playwright/test'; // ── Admin user (seeded is_admin=true) ───────────────────────────────────────── test.describe('Admin user — admin nav entry + /admin route', () => { test.beforeEach(async ({ page }) => { await page.goto('/calendar'); // Wait for auth and nav to be visible before asserting await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); }); test('admin sees the Admin nav entry (ShieldCheck, aria-label="Admin settings")', async ({ page, }) => { // The admin nav link is rendered with aria-label="Admin settings" in both // AppNav (desktop) and BottomTabBar (mobile). const adminEntry = page.getByRole('link', { name: 'Admin settings' }); await expect(adminEntry).toBeVisible(); }); test('admin reaches /admin and sees "Admin Settings" heading', async ({ page }) => { // Navigate directly — also verifies the route guard does NOT redirect admins await page.goto('/admin'); await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); // The page heading is "Admin Settings" await expect(page.getByRole('heading', { name: 'Admin Settings' })).toBeVisible(); }); test('admin /admin page renders the MEMBERS section', async ({ page }) => { await page.goto('/admin'); await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); // The section is labeled "Members" (aria-label on
) await expect(page.getByRole('region', { name: 'Members' })).toBeVisible(); }); }); // ── Non-admin user (route-mocked isAdmin:false) ─────────────────────────────── test.describe('Non-admin user — admin nav entry hidden + /admin redirect', () => { // Route-mock /api/me to return isAdmin:false BEFORE navigation so the PWA // never sees isAdmin:true in this test context. const mockNonAdminMe = async (page: import('@playwright/test').Page) => { await page.route('/api/me', (route) => { void route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ user: { id: 1, displayName: 'Dev User', color: '#4A90D9', isAdmin: false, needsProviderSetup: false, }, }), }); }); }; test('non-admin does NOT see the Admin nav entry', async ({ page }) => { await mockNonAdminMe(page); await page.goto('/calendar'); await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); // Admin entry must be absent const adminEntry = page.getByRole('link', { name: 'Admin settings' }); await expect(adminEntry).toHaveCount(0); }); test('non-admin navigating to /admin is redirected to /calendar', async ({ page }) => { await mockNonAdminMe(page); await page.goto('/admin'); // Wait for meQuery to resolve and redirect to fire — the Navigate component // replaces the URL once meQuery.isLoading = false + isAdmin = false. await page.waitForURL(/\/calendar/, { timeout: 10_000 }); // Should have landed on /calendar const url = new URL(page.url()); expect(url.pathname, `Expected /calendar but got ${url.pathname}`).toMatch( /^\/(calendar)?$/, ); // "Admin Settings" heading must NOT be present await expect(page.getByRole('heading', { name: 'Admin Settings' })).toHaveCount(0); }); });