- Run prettier on all new/modified PWA files (CredentialSheet, SetupBanner, AdminPage, admin.spec.ts) - Remove unnecessary 'as React.RefObject<HTMLElement | null>' casts flagged by @typescript-eslint/no-unnecessary-type-assertion - Format pre-existing API files from Plans 02/03 (me.ts, user.test.ts, requireAdmin.test.ts, me.test.ts) - All 270 API tests + 191 PWA vitest tests pass; lint/typecheck/build clean
108 lines
4.6 KiB
TypeScript
108 lines
4.6 KiB
TypeScript
/**
|
||
* 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 <section>)
|
||
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);
|
||
});
|
||
});
|