// Smoke tests for pages behind the auth wall. Runs against the separate // `pediatric-ai-scribe-e2e` container (port 3553 on host, 3000 internal) which // has TURNSTILE_SECRET_KEY="" + SMTP_HOST="" so tests can log in without a // bot challenge and register auto-verifies. // // Each test logs in via the API (no UI interaction needed) and injects the // session cookie into the browser context. const { test, expect } = require('@playwright/test'); const E2E_BASE_INTERNAL = 'http://pediatric-ai-scribe-e2e:3000'; const E2E_BASE_EXTERNAL = 'http://host.docker.internal:3553'; // only used when running Playwright on host const E2E_BASE = process.env.E2E_AUTH_BASE_URL || E2E_BASE_INTERNAL; const TEST_EMAIL = 'e2e-user@ped-ai.test'; const TEST_PASSWORD = 'E2E-testPassword123!'; // Module-scoped token cache — one login per Playwright worker (config uses // workers:1, so effectively one login for the whole run). Without this we // hit the 10/15min login rate-limit on the first pass. let _tokenCache = null; async function getAuthToken(request) { if (_tokenCache) return _tokenCache; const r = await request.post(E2E_BASE + '/api/auth/login', { data: { email: TEST_EMAIL, password: TEST_PASSWORD }, }); if (!r.ok()) { const text = await r.text(); throw new Error(`Login failed (status ${r.status()}): ${text}`); } const body = await r.json(); if (!body.token) throw new Error('Login response missing token: ' + JSON.stringify(body)); _tokenCache = body.token; return _tokenCache; } async function loginAs(context, request) { const token = await getAuthToken(request); const url = new URL(E2E_BASE); await context.addCookies([ { name: 'ped_auth', value: token, domain: url.hostname, path: '/', httpOnly: true, secure: false, sameSite: 'Lax', }, ]); } // ── Tests ──────────────────────────────────────────────────────────── test.describe('Auth-gated pages — main tabs', () => { test.beforeEach(async ({ context, request }) => { await loginAs(context, request); }); test('Landing page shows tab navigation after login', async ({ page }) => { await page.goto(E2E_BASE + '/'); await expect(page.locator('button.tab-btn').first()).toBeVisible({ timeout: 15000 }); }); // Each auth-gated tab test: activate the tab, assert its container becomes // visible + contains the expected anchor string. We use getTabPanel helpers // because components load lazily from /components/.html. const tabs = [ { name: 'encounter', anchor: /New Encounter|encounter|SOAP/i }, { name: 'wellvisit', anchor: /Well Visit|well visit/i }, { name: 'chart', anchor: /Chart|visits|patients/i }, { name: 'vaxschedule', anchor: /Vaccine|schedule|dose/i }, { name: 'catchup', anchor: /Catch-up|catch up|schedule/i }, { name: 'learning', anchor: /Learning|quiz|topic/i }, { name: 'dictation', anchor: /Dictation|record|transcrib/i }, { name: 'settings', anchor: /Setting|profile|preferences|account/i }, { name: 'calculators', anchor: /Pediatric Calculator|BP Percentile|BMI/i }, { name: 'faq', anchor: /FAQ|question|answer/i }, ]; for (const { name, anchor } of tabs) { test(`${name} tab loads content`, async ({ page, viewport }) => { await page.goto(E2E_BASE + '/'); // On mobile the sidebar is hidden behind a hamburger. Open it so the // tab buttons become interactable. if (viewport && viewport.width <= 768) { await page.click('#btn-menu-toggle').catch(() => {}); } await page.waitForSelector('button.tab-btn', { timeout: 15000 }); const tabBtn = page.locator(`button.tab-btn[data-tab="${name}"]`); const isHidden = await tabBtn.evaluate((el) => el.classList.contains('hidden')).catch(() => true); test.skip(isHidden, `Tab "${name}" is hidden for this user role`); await tabBtn.click(); // Wait for lazy component load to complete await page.waitForFunction( (t) => { const el = document.getElementById(t + '-tab'); return el && el.classList.contains('active') && el.innerHTML.trim().length > 100; }, name, { timeout: 15000 } ); await expect(page.locator(`#${name}-tab`)).toContainText(anchor); }); } });