test: bring the e2e specs up to date with the app they test
Three specs were asserting screens that no longer exist. They passed for as long as they did only because the e2e stack shared production's database and its configuration; against a clean one they failed honestly. Signing in is a stepped flow now — email, then a choice between a password and an emailed code — so #login-password is in the DOM but hidden until that choice is made. The spec asserted it visible on the landing screen. Replaced with one test for the landing step and a new one that walks the transition, which nothing covered before. The register link is hidden only when registration is disabled. This install has it enabled and invite-gated, so the link shows and the invite field is required; the spec asserted display:none. Connecting Nextcloud by signing in to Nextcloud is now the offered path, with the username and app-password fields folded behind "Use an app password instead". The spec asserted all three visible at once; it now checks the primary path and then opens the fallback. learning-tab.spec.js is deleted and `learning` is out of the smoke tab list — that feature was removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
5a666f5ca5
commit
6b039c4d87
4 changed files with 126 additions and 10 deletions
|
|
@ -32,7 +32,6 @@ test.describe('Auth-gated pages — main tabs', () => {
|
|||
{ 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 },
|
||||
|
|
|
|||
|
|
@ -10,24 +10,53 @@ const { test, expect, E2E_BASE } = require('../fixtures');
|
|||
test.describe('Unauthenticated auth screen', () => {
|
||||
|
||||
// Use the base test that doesn't auto-login.
|
||||
test('landing shows login form with email + password fields', async ({ page }) => {
|
||||
//
|
||||
// Signing in is a stepped flow, not one form: email first, then a choice
|
||||
// between a password and an emailed code. The password field exists in the
|
||||
// DOM from the start but stays hidden until that choice is made, so asserting
|
||||
// it visible on the landing screen tests a page that no longer exists.
|
||||
test('landing asks for the email only, and hides the rest of the flow', async ({ page }) => {
|
||||
await page.goto(E2E_BASE + '/');
|
||||
await expect(page.locator('#auth-screen')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('#login-email')).toBeVisible();
|
||||
await expect(page.locator('#login-password')).toBeVisible();
|
||||
await expect(page.locator('#btn-local-login')).toBeVisible();
|
||||
await expect(page.locator('#btn-login-continue')).toBeVisible();
|
||||
// Later steps are present but not yet offered.
|
||||
await expect(page.locator('#login-password')).toBeHidden();
|
||||
await expect(page.locator('#login-code')).toBeHidden();
|
||||
await expect(page.locator('#btn-local-login')).toBeHidden();
|
||||
// main app body must be hidden while unauthenticated
|
||||
await expect(page.locator('#main-app')).toBeHidden();
|
||||
});
|
||||
|
||||
test('register link is present but currently disabled (display:none)', async ({ page }) => {
|
||||
// Invite-only registration hides the link while keeping the form in the DOM.
|
||||
test('an email leads to the choice, and choosing a password reveals it', async ({ page }) => {
|
||||
await page.goto(E2E_BASE + '/');
|
||||
await page.waitForSelector('#auth-screen', { timeout: 10000 });
|
||||
const display = await page.locator('#show-register').evaluate(el => el.style.display);
|
||||
expect(display).toBe('none');
|
||||
// The register form element still exists in the DOM for programmatic access
|
||||
await page.fill('#login-email', 'someone@ped-ai.test');
|
||||
await page.click('#btn-login-continue');
|
||||
|
||||
await expect(page.locator('#login-choice')).toBeVisible();
|
||||
// The address is fixed once the flow has moved past it; "use a different
|
||||
// email" is how you go back, and it only appears after the first step.
|
||||
await expect(page.locator('#login-email')).toHaveJSProperty('readOnly', true);
|
||||
await expect(page.locator('#login-change-email')).toBeVisible();
|
||||
|
||||
await page.click('#btn-login-use-password');
|
||||
await expect(page.locator('#login-password')).toBeVisible();
|
||||
await expect(page.locator('#btn-local-login')).toBeVisible();
|
||||
});
|
||||
|
||||
test('the register link follows the registration setting', async ({ page }) => {
|
||||
// Hidden by default and shown only when registration is enabled. The seed
|
||||
// sets registration_enabled true and invite_only true, which is what
|
||||
// production runs: registration is open, but a code is required — so the
|
||||
// link shows and the invite field is required.
|
||||
await page.goto(E2E_BASE + '/');
|
||||
await page.waitForSelector('#auth-screen', { timeout: 10000 });
|
||||
await expect(page.locator('#show-register')).toBeVisible();
|
||||
await expect(page.locator('#register-form')).toHaveCount(1);
|
||||
await page.click('#show-register');
|
||||
await expect(page.locator('#reg-invite')).toBeVisible();
|
||||
await expect(page.locator('#reg-invite')).toHaveJSProperty('required', true);
|
||||
});
|
||||
|
||||
test('register form DOM is wired correctly if manually unhidden', async ({ page }) => {
|
||||
|
|
|
|||
79
e2e/tests/openapi.spec.js
Normal file
79
e2e/tests/openapi.spec.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// The API contract, checked against the API.
|
||||
//
|
||||
// docs/api-reference.md was hand-written and drifted until it documented
|
||||
// twenty-three endpoints that answer 404. The document is now generated from
|
||||
// the router, which fixes the paths; this fixes the prose, by failing when a
|
||||
// route exists that nothing describes.
|
||||
//
|
||||
// It runs here rather than in the unit suite because it needs the whole app
|
||||
// mounted, and requiring server.js from node:test pulls in the database pool
|
||||
// and hangs the run — that has happened before in this repo.
|
||||
const { test, expect } = require('../fixtures');
|
||||
|
||||
async function spec(page) {
|
||||
const response = await page.request.get('/api/openapi.json');
|
||||
expect(response.status(), 'the document should be served to a signed-in user').toBe(200);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
test.describe('OpenAPI', () => {
|
||||
test('the document describes this deployment, not a remembered one', async ({ authedPage: page }) => {
|
||||
const doc = await spec(page);
|
||||
expect(doc.openapi).toBe('3.1.0');
|
||||
expect(Object.keys(doc.paths).length).toBeGreaterThan(100);
|
||||
// Both ways of holding a session are declared.
|
||||
expect(Object.keys(doc.components.securitySchemes).sort()).toEqual(['bearer', 'cookie']);
|
||||
});
|
||||
|
||||
test('endpoints that exist are in it', async ({ authedPage: page }) => {
|
||||
const doc = await spec(page);
|
||||
for (const path of ['/api/health', '/api/build', '/api/auth/me', '/api/my-resources']) {
|
||||
expect(doc.paths[path], path + ' is missing from the document').toBeTruthy();
|
||||
}
|
||||
// A path parameter is written the way OpenAPI writes one.
|
||||
expect(doc.paths['/api/my-resources/{id}']).toBeTruthy();
|
||||
expect(doc.paths['/api/my-resources/:id']).toBeFalsy();
|
||||
});
|
||||
|
||||
test('endpoints that were removed are not', async ({ authedPage: page }) => {
|
||||
// Learning Hub is gone. The generated document cannot claim otherwise,
|
||||
// which is exactly what the hand-written reference did for weeks.
|
||||
const doc = await spec(page);
|
||||
const stale = Object.keys(doc.paths).filter(p => p.includes('/learning'));
|
||||
expect(stale, 'removed endpoints are still described').toEqual([]);
|
||||
});
|
||||
|
||||
test('every documented operation is reachable, and none 404s', async ({ authedPage: page }) => {
|
||||
const doc = await spec(page);
|
||||
const missing = [];
|
||||
for (const [path, methods] of Object.entries(doc.paths)) {
|
||||
// Only GETs with no path parameter can be probed safely: a POST would
|
||||
// change something and a templated path has no real id to try.
|
||||
if (!methods.get || path.includes('{')) continue;
|
||||
const response = await page.request.get(path, { failOnStatusCode: false });
|
||||
if (response.status() === 404) missing.push(path);
|
||||
}
|
||||
expect(missing, 'documented but answering 404').toEqual([]);
|
||||
});
|
||||
|
||||
test('a description is required, so a new endpoint cannot ship unexplained', async ({ authedPage: page }) => {
|
||||
// The generator supplies paths and methods; a person supplies meaning. This
|
||||
// is the half that rots, so it is the half that is enforced — undescribed
|
||||
// operations are listed by name rather than counted, so the failure says
|
||||
// what to write.
|
||||
const doc = await spec(page);
|
||||
const undescribed = [];
|
||||
for (const [path, methods] of Object.entries(doc.paths)) {
|
||||
for (const [method, operation] of Object.entries(methods)) {
|
||||
if (!operation.summary) undescribed.push(method.toUpperCase() + ' ' + path);
|
||||
}
|
||||
}
|
||||
// Held at the current count while the backlog is written down, and lowered
|
||||
// as it is. It must never rise: a new endpoint adds to this list and fails
|
||||
// the build.
|
||||
const BUDGET = Number(process.env.OPENAPI_UNDESCRIBED_BUDGET || 0);
|
||||
expect(undescribed.length,
|
||||
'undescribed operations (add them to src/utils/openapiRoutes.js):\n ' +
|
||||
undescribed.slice(0, 40).join('\n ')).toBeLessThanOrEqual(BUDGET);
|
||||
});
|
||||
});
|
||||
|
|
@ -56,11 +56,20 @@ test.describe('Settings — voice, password, nextcloud sections render', () => {
|
|||
expect(setupCount + disableCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Nextcloud section: URL/user/pass fields render', async ({ authedPage: _, page }) => {
|
||||
test('Nextcloud section: signing in is the offered path, app password the fallback', async ({ authedPage: _, page }) => {
|
||||
// Connecting by signing in to Nextcloud itself is the ordinary way in, so
|
||||
// the address and that button are what the card shows. The username and
|
||||
// app-password fields still exist, folded away behind "Use an app password
|
||||
// instead" — they were visible when this test was written.
|
||||
await openTab(page, 'settings');
|
||||
await expect(page.locator('#nc-url')).toBeVisible();
|
||||
await expect(page.locator('#btn-nc-login-flow')).toBeVisible();
|
||||
await expect(page.locator('#nc-user')).toBeHidden();
|
||||
|
||||
await page.locator('#nc-manual summary').click();
|
||||
await expect(page.locator('#nc-user')).toBeVisible();
|
||||
await expect(page.locator('#nc-pass')).toBeVisible();
|
||||
await expect(page.locator('#btn-nc-connect')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue