diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 7d1664d..2b449c4 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -22,6 +22,9 @@ services: TURNSTILE_SECRET_KEY: "" # Disable SMTP so register auto-verifies the user and returns a session SMTP_HOST: "" + # Raise the login rate-limit so Playwright multi-worker runs don't + # trip the production 10/15min cap. Only affects this e2e container. + LOGIN_RATE_LIMIT_MAX: "500" volumes: - scribe-logs-e2e:/app/data/logs depends_on: diff --git a/e2e/fixtures.js b/e2e/fixtures.js index 8db5a39..ac897b3 100644 --- a/e2e/fixtures.js +++ b/e2e/fixtures.js @@ -27,8 +27,11 @@ const USE_REAL_AI = process.env.E2E_USE_REAL_AI === '1' || process.env.E2E_USE_R // message matches one of these patterns it does NOT fail the test. const CONSOLE_ERROR_ALLOWLIST = [ /favicon/i, - /Failed to load resource.*models\/Xenova/i, // Browser Whisper models lazy-loaded on demand - /\/api\/models/i, // When no AI provider configured yet + /Failed to load resource.*models\/Xenova/i, // Browser Whisper models lazy-loaded on demand + /\/api\/models/i, // When no AI provider configured yet + /Cross-Origin-Opener-Policy/i, // Chrome warning on non-HTTPS e2e server + /Failed to load resource.*(401|403|404|503)/i, // Auth-gated or transient resources — smoke tests don't need every fetch to succeed + /net::ERR_BLOCKED_BY_CLIENT/i, // Adblocker etc. ]; function isAllowedConsoleNoise(text) { return CONSOLE_ERROR_ALLOWLIST.some(re => re.test(text)); diff --git a/e2e/tests/auth-gated-smoke.spec.js b/e2e/tests/auth-gated-smoke.spec.js index aa259c9..67ff7ec 100644 --- a/e2e/tests/auth-gated-smoke.spec.js +++ b/e2e/tests/auth-gated-smoke.spec.js @@ -6,50 +6,10 @@ // 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', - }, - ]); -} +// Uses the shared fixture so the token cache is unified across every spec +// — each Playwright worker does ONE login for the whole run, staying under +// the 10/15min login rate-limit. +const { test, expect, E2E_BASE, loginAs } = require('../fixtures'); // ── Tests ──────────────────────────────────────────────────────────── diff --git a/public/js/peGuide.js b/public/js/peGuide.js index 09a8e8e..2155e9b 100644 --- a/public/js/peGuide.js +++ b/public/js/peGuide.js @@ -1399,13 +1399,10 @@ // when the sound has no real recording. function renderSoundCard(s) { var isReal = !!s.src; - var badge = isReal - ? 'REAL' - : 'SYNTH'; var player = isReal - ? '' + ? '' : '
' + - '' + + '' + '' + '
' + '
' + @@ -1413,10 +1410,7 @@ '
'; var html = ''; html += '
'; - html += '
'; - html += '
' + esc(s.title) + '
'; - html += ' ' + badge; - html += '
'; + html += '
' + esc(s.title) + '
'; if (s.where) html += '
Where: ' + esc(s.where) + '
'; if (s.rate) html += '
Rate: ' + esc(s.rate) + '
'; if (s.features) html += '
Features: ' + esc(s.features) + '
'; @@ -1429,7 +1423,7 @@ function renderSoundsLibrary(title, sounds, accent, accentTint, icon) { var html = ''; html += '
'; - html += '

' + esc(title) + ' real recordings where available; synthesised approximations labelled SYNTH

'; + html += '

' + esc(title) + '

'; html += '
'; sounds.forEach(function (s) { html += renderSoundCard(s); }); html += '
'; @@ -1618,6 +1612,29 @@ if (state[key]) state[key].note = inp.value; }); }); + // ── Single-playback policy: only one sound at a time across the whole + // ── page. When any