Three separate reasons tests were failing, none of them a defect in the app. The calculators. e2e-harness.html loaded calculators.js and drugs-loader.js with `defer` after they were split into ES modules; index.html was updated at the time and this page was not. A module parsed as a classic script throws "Cannot use import statement outside a module" before a line runs, so no click handler was ever attached: the pills rendered from static HTML and did nothing. Only the first calculator appeared to pass, because it carries `active` in the markup and needs no click. That was 52 failures. Settings and FAQ. Both moved from the tab rail into the account-card menu; the helper still clicked button.tab-btn[data-tab=…] and timed out. Ten more. The AI mocks, which had stopped intercepting for two independent reasons and so were calling the real model on every run — spending credits and comparing genuine output against strings like "MOCK HPI from dictation". A '**/api/x' glob matches no URL on Playwright 1.50, and page.route fails silently when nothing matches; measured against a real URL, that glob and '*/**/api/x' both matched zero times where a regex matched. Fixing that alone was not enough: the app registers a service worker that answers every /api/ request with its own fetch(), and a request made inside a service worker never reaches page.route. Blocking registration in the config puts them back in the page. The mocked dictation test now finishes in 1.6s rather than 7.5s, which is what a real model call costs. Whole suite: 204 passed / 96 failed in 15.8 minutes, now 289 passed / 11 failed in 6.8. The remaining eleven are spread across nine specs with no shared cause and are not touched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
106 lines
5.9 KiB
JavaScript
106 lines
5.9 KiB
JavaScript
// ============================================================
|
|
// 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('<script type="module" src="/js/' + file.replace('.', '\\.') + '"');
|
|
assert.match(app, inApp, file + ' is a module in the app');
|
|
assert.match(harness, inApp, 'so the harness must load it the same way');
|
|
}
|
|
assert.doesNotMatch(harness, /<script defer src="\/js\/(calculators|drugs-loader)\.js"/);
|
|
});
|
|
|
|
test('the AI mocks actually intercept, rather than calling the real model', () => {
|
|
const fixtures = read('e2e/fixtures.js');
|
|
// Two separate reasons the mocks were silently doing nothing, both measured.
|
|
//
|
|
// One: a '**/api/x' glob matches no URL on Playwright 1.50, and page.route
|
|
// fails silently when nothing matches.
|
|
assert.match(fixtures, /function asMatcher\(pattern\)/);
|
|
assert.match(fixtures, /await page\.route\(asMatcher\(pattern\)/);
|
|
|
|
// Two: the app registers a service worker that answers every /api/ request
|
|
// with its own fetch(), and a request made inside a service worker never
|
|
// reaches page.route. Blocking registration puts them back in the page.
|
|
assert.match(read('public/sw.js'), /url\.pathname\.startsWith\('\/api\/'\)/,
|
|
'if the worker stops handling /api this guard can be revisited');
|
|
assert.match(read('e2e/playwright.config.js'), /serviceWorkers: 'block'/);
|
|
});
|
|
|
|
test('Settings and FAQ are opened the way the app actually offers them', () => {
|
|
// Neither is on the tab rail; both live in the account-card menu with Admin.
|
|
const index = read('public/index.html');
|
|
for (const name of ['settings', 'faq']) {
|
|
assert.doesNotMatch(index, new RegExp('data-tab="' + name + '"'), name + ' is not a rail tab');
|
|
assert.match(index, new RegExp('data-account-tab="' + name + '"'));
|
|
}
|
|
const spec = read('e2e/tests/settings-faq-dictation.spec.js');
|
|
assert.match(spec, /const ACCOUNT_MENU = \['settings', 'faq'\];/);
|
|
assert.match(spec, /\[data-account-tab="\$\{name\}"\]/);
|
|
});
|