Files
familysync/apps/pwa/e2e/admin.spec.ts
T
Lucas Berger 79fe3e0e04 fix(10-04): prettier format + remove unnecessary type assertions
- 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
2026-06-13 15:22:50 -04:00

108 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
});