// ============================================================ // E2E HARNESS // ============================================================ // The browser suite is the only thing that exercises the app the way a person // uses it, so the two ways it silently stops doing that are worth pinning. // // Both were found the same day: every browser-driving spec was failing, and had // been, because the harness could not sign in at all. // ============================================================ const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const read = p => fs.readFileSync(path.join(__dirname, '..', p), 'utf8'); test('the browser suite runs against a loopback origin, because the app needs a secure context', () => { // AccountBoundary mints a session generation with crypto.randomUUID() on // every sign-in. On a non-loopback http origin the browser provides no // crypto.randomUUID at all, so that call throws, the boot handler's catch // swallows it, and every test lands on the login screen with a valid session // in hand. Measured: isSecureContext false and randomUUID undefined on // http://pediatric-ai-scribe-e2e:3000, both true on http://127.0.0.1:3553. assert.match(read('public/js/accountBoundary.js'), /crypto\.randomUUID\(\)/, 'if this call is gone, the loopback requirement may have gone with it'); const config = read('e2e/playwright.config.js'); const fixtures = read('e2e/fixtures.js'); const runner = read('scripts/e2e.sh'); // host.docker.internal is a hostname, not loopback, and was the old default // in all three places. for (const [name, src] of [['config', config], ['fixtures', fixtures], ['runner', runner]]) { assert.ok(!/host\.docker\.internal/.test(src), name + ' must not default to a non-loopback origin'); } assert.match(config, /127\.0\.0\.1:3553/, 'playwright baseURL is loopback'); assert.match(fixtures, /127\.0\.0\.1:3553/, 'the fixtures authenticate against loopback'); assert.match(runner, /--network=host/, 'which needs the host network to reach the published port'); assert.match(read('docker-compose.e2e.yml'), /http:\/\/127\.0\.0\.1:3553/, 'and CORS has to allow it'); }); test('the e2e accounts are seeded, and the seed cannot touch a real one', () => { const seed = read('e2e/seed.js'); // Both roles. Without an admin account nothing under /api/admin could be // tested through a real request, which is how the Search Sources screen came // to be checked by reading its markup instead. assert.match(seed, /role: 'user'/); assert.match(seed, /role: 'admin'/); // It resets passwords and grants the admin role, so the domain guard is the // part that matters: a mistyped environment variable must not be able to // reach a real account. assert.match(seed, /refusing to seed/); assert.match(seed, /TEST_DOMAIN = '@ped-ai\.test'/); assert.match(seed, /email\.slice\(-TEST_DOMAIN\.length\) !== TEST_DOMAIN/); // Reconciles rather than only creating, so a leftover account with the wrong // role cannot fail the suite for a reason unrelated to the code. assert.match(seed, /UPDATE users SET password = \?, name = \?, role = \?, email_verified = true, disabled = false/); // And the runner seeds before it tests, so nobody has to remember to. assert.match(read('scripts/e2e.sh'), /node e2e\/seed\.js/); }); test('the harness loads the calculators the way the app does', () => { // calculators.js and drugs-loader.js are ES modules. Loaded with `defer` they // are parsed as classic scripts and throw "Cannot use import statement outside // a module" before a line runs, so no click handler is ever attached: the // pills render from static HTML and do nothing. Measured in the browser, and // it was 52 of the 96 e2e failures. Only the first calculator "passed", // because it carries `active` in the markup and needs no click. const harness = read('public/e2e-harness.html'); const app = read('public/index.html'); for (const file of ['calculators.js', 'drugs-loader.js']) { const inApp = new RegExp('