import { expect, type APIRequestContext, test } from '@playwright/test'; const BACKEND_API_URL = process.env.VITE_BACKEND_API_URL || 'http://localhost:8080/api'; interface ClassroomStrategyPayload { readonly title: string; } interface SignLanguagePayload { readonly word: string; } interface ZonePayload { readonly name: string; } interface TimerSoundPayload { readonly name: string; } interface SeededContentTypeExpectation { readonly contentType: string; readonly payloadShape: 'array' | 'object'; } const MINIMUM_SEEDED_CONTENT_TYPES: readonly SeededContentTypeExpectation[] = [ { contentType: 'classroom-timer-backgrounds', payloadShape: 'array' }, { contentType: 'classroom-timer-sounds', payloadShape: 'array' }, { contentType: 'classroom-timer-presets', payloadShape: 'array' }, { contentType: 'classroom-timer-tips', payloadShape: 'array' }, { 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}/public/content-catalog/${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; } 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 public API', async ({ request }) => { for (const expectation of MINIMUM_SEEDED_CONTENT_TYPES) { const payload = await getSeededPayload(request, expectation.contentType); expectSeededPayloadShape(payload, expectation); } }); test('renders seeded classroom timer catalog content', async ({ page, request }) => { const payload = await getSeededPayload(request, 'classroom-timer-sounds'); const sounds: readonly TimerSoundPayload[] = parseNamedItems(payload, 'name').map((item) => ({ name: item.value, })); const firstSound = getFirstPayloadItem(sounds, 'classroom-timer-sounds'); 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(firstSound.name, { exact: true })).toBeVisible(); }); test('renders seeded classroom support strategies', async ({ page, 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 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.getByText(firstStrategy.title, { exact: true })).toBeVisible(); }); test('renders seeded sign language content', async ({ page, 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 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 }) => { 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 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(); }); });