import { expect, type APIRequestContext, type Page, test } from '@playwright/test'; const BACKEND_API_URL = process.env.VITE_BACKEND_API_URL || 'http://localhost:8080/api'; // Use a campus-scoped tenant user (director) that has both organizationId and campusId. // Org-scoped content (regulation-zones, classroom-strategies, sign-language-items) is // filtered by organizationId, while per-tenant content (dashboard-* items) is filtered // by campusId. Super admin (admin@flatlogic.com) has globalAccess but no org/campus. const TEST_USER = { email: 'director@flatlogic.com', password: 'flatlogicUser123!', }; const CLASS_SCOPE_TEST_USER = { email: 'teacher@flatlogic.com', password: 'flatlogicUser123!', }; async function authenticateViaPage(page: Page, user = TEST_USER): Promise { await page.goto('/'); await page.getByPlaceholder('you@school.edu').fill(user.email); await page.getByPlaceholder('Enter your password').fill(user.password); await page.getByRole('button', { name: 'Sign In', exact: true }).click(); await page.waitForURL('/', { timeout: 10000 }); } interface ClassroomStrategyPayload { readonly title: string; } interface SignLanguagePayload { readonly word: string; } interface ZonePayload { readonly name: string; } interface SeededContentTypeExpectation { readonly contentType: string; readonly payloadShape: 'array' | 'object'; } const MINIMUM_SEEDED_CONTENT_TYPES: readonly SeededContentTypeExpectation[] = [ { contentType: 'classroom-strategies', payloadShape: 'array' }, { contentType: 'sign-language-items', payloadShape: 'array' }, { contentType: 'sign-language-page-content', payloadShape: 'object' }, { contentType: 'regulation-zones', payloadShape: 'array' }, { contentType: 'zones-of-regulation-page-content', payloadShape: 'object' }, { contentType: 'dashboard-encouraging-quotes', payloadShape: 'array' }, { contentType: 'dashboard-compliance-items', payloadShape: 'array' }, { contentType: 'dashboard-sign-of-week', payloadShape: 'object' }, ]; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function getStringField(value: unknown, fieldName: string): string { if (!isRecord(value)) { throw new Error(`Payload item must be an object with ${fieldName}`); } const fieldValue = value[fieldName]; if (typeof fieldValue !== 'string') { throw new Error(`Payload item ${fieldName} must be a string`); } return fieldValue; } function parseNamedItems(payload: unknown, fieldName: string): readonly { readonly value: string }[] { if (!Array.isArray(payload)) { throw new Error('Content catalog payload must be an array'); } return payload.map((item) => ({ value: getStringField(item, fieldName), })); } async function getSeededPayload( request: APIRequestContext, contentType: string, ): Promise { const response = await request.get( `${BACKEND_API_URL}/content-catalog/read/${encodeURIComponent(contentType)}`, ); expect( response.ok(), `Seeded backend content catalog request failed for ${contentType}. Run backend migrations, seeders, and backend server before this suite.`, ).toBe(true); const catalog: unknown = await response.json(); if (!isRecord(catalog)) { throw new Error(`Content catalog response for ${contentType} must be an object`); } return catalog.payload; } async function authenticateApiContext(request: APIRequestContext): Promise { const response = await request.post(`${BACKEND_API_URL}/auth/signin/local`, { data: { email: TEST_USER.email, password: TEST_USER.password }, }); expect(response.ok(), 'API authentication failed').toBe(true); } function getFirstPayloadItem(payload: readonly TItem[], contentType: string): TItem { expect(payload.length, `${contentType} seed payload must contain at least one item`).toBeGreaterThan(0); return payload[0]; } function expectSeededPayloadShape(payload: unknown, expectation: SeededContentTypeExpectation) { if (expectation.payloadShape === 'array') { if (!Array.isArray(payload)) { throw new Error(`${expectation.contentType} payload must be an array`); } expect(payload.length, `${expectation.contentType} payload must contain at least one item`).toBeGreaterThan(0); return; } if (!isRecord(payload)) { throw new Error(`${expectation.contentType} payload must be an object`); } } test.describe('seeded content catalog integration', () => { test('minimum backend content seed set is available through authenticated API', async ({ request }) => { await authenticateApiContext(request); for (const expectation of MINIMUM_SEEDED_CONTENT_TYPES) { const payload = await getSeededPayload(request, expectation.contentType); expectSeededPayloadShape(payload, expectation); } }); test('renders static classroom timer content', async ({ page }) => { await authenticateViaPage(page, CLASS_SCOPE_TEST_USER); await page.goto('/classroom-timer'); await expect(page.getByText('Loading classroom timer content...')).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Classroom Timer' })).toBeVisible(); await expect(page.getByText('Gentle Chime', { exact: true })).toBeVisible(); }); test('renders seeded classroom support strategies', async ({ page, request }) => { await authenticateApiContext(request); const payload = await getSeededPayload(request, 'classroom-strategies'); const strategies: readonly ClassroomStrategyPayload[] = parseNamedItems(payload, 'title').map((item) => ({ title: item.value, })); const firstStrategy = getFirstPayloadItem(strategies, 'classroom-strategies'); await authenticateViaPage(page); await page.goto('/classroom-support'); await expect(page.getByText('Loading classroom strategies...')).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Classroom Support' })).toBeVisible(); await expect(page.getByRole('heading', { name: firstStrategy.title })).toBeVisible(); }); test('renders seeded sign language content', async ({ page, request }) => { await authenticateApiContext(request); const payload = await getSeededPayload(request, 'sign-language-items'); const signs: readonly SignLanguagePayload[] = parseNamedItems(payload, 'word').map((item) => ({ word: item.value, })); const firstSign = getFirstPayloadItem(signs, 'sign-language-items'); await authenticateViaPage(page); await page.goto('/sign-language'); await expect(page.getByText('Loading sign language content and saved progress.')).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Sign Language' })).toBeVisible(); await expect(page.getByText(firstSign.word, { exact: true })).toBeVisible(); }); test('renders seeded zones of regulation content', async ({ page, request }) => { await authenticateApiContext(request); const payload = await getSeededPayload(request, 'regulation-zones'); const zones: readonly ZonePayload[] = parseNamedItems(payload, 'name').map((item) => ({ name: item.value, })); const firstZone = getFirstPayloadItem(zones, 'regulation-zones'); await authenticateViaPage(page); await page.goto('/zones-of-regulation'); await expect(page.getByText('Loading regulation zone content.')).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Zones of Regulation' })).toBeVisible(); await expect(page.getByText(firstZone.name, { exact: true })).toBeVisible(); }); });