pediatric-ai-scribe-v3/e2e/tests/my-resources.spec.js
Daniel 28877ba0c4
All checks were successful
Forgejo Docker Build / Root app tests (push) Successful in 50s
Forgejo Docker Build / Build Docker image (push) Successful in 17s
Forgejo Docker Build / End-to-end (browser) (push) Successful in 6s
feat: My Resources takes Details — what to cover, at length, folded away until needed
Topic is one line and Instructions say how; neither is the place for a list
of twelve topics or a case to build around. Details is: optional,
collapsed by default, up to 12,000 characters, quoted into the prompt as the
author's own account of what the resource must cover — material, not
commands, in both the deck and the markdown shapes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 14:30:09 +02:00

313 lines
15 KiB
JavaScript

// ============================================================
// MY RESOURCES — the UI, and the requests it actually sends
// ============================================================
// The unit tests for this feature read source files and assert patterns: they
// prove the code says the right thing, not that the screen does it. Nothing
// exercised the browser, so a mismatch between what the form sends and what the
// route reads would have passed all of them.
//
// Three real bugs shipped through that gap in one session — a modification that
// updated the markdown but not the deck, generation that failed whenever the
// slide reviewer was off, and a figure generated for a slide that never
// referenced it. Every one was found by driving the running server by hand.
//
// So these tests assert the request bodies, not only the rendering. The API is
// stubbed: this is about the contract between the screen and the route, and
// stubbing keeps it fast, free and deterministic.
const { test, expect, E2E_BASE } = require('../fixtures');
const OPTIONS = {
success: true,
models: ['model-a', 'model-b'],
defaultModel: 'model-a',
imagesAvailable: true,
webSearchAvailable: true,
pubmedAvailable: true,
};
const LIBRARY = {
success: true,
resources: [
{ id: 1, title: 'Croup in children', kind: 'presentation', topic: 'croup',
grounded_count: 12, created_at: '2026-09-01T10:00:00Z' },
{ id: 2, title: 'Neonatal jaundice', kind: 'article', topic: 'jaundice',
grounded_count: 0, created_at: '2026-09-02T10:00:00Z' },
{ id: 3, title: 'Bronchiolitis basics', kind: 'presentation', topic: 'bronchiolitis',
grounded_count: 30, created_at: '2026-09-03T10:00:00Z' },
],
};
/** Stub the feature's endpoints and record every request body sent to them. */
async function stub(page, overrides = {}) {
const sent = [];
const json = (route, body, status = 200) =>
route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) });
await page.route(/\/api\/my-resources\/options/, r => json(r, overrides.options || OPTIONS));
await page.route(/\/api\/my-resources\/\d+\/refine/, async route => {
sent.push({ url: route.request().url(), body: route.request().postDataJSON() });
if (overrides.refineStatus) return json(route, overrides.refineBody || { error: 'refused' }, overrides.refineStatus);
return json(route, { success: true, resource: { id: 1, title: 'Croup in children' },
markdown: '# Croup\n\n- revised', grounding: { used: true, count: 4 },
searches: [], imageJobs: [], imageFailures: [] });
});
await page.route(/\/api\/my-resources\/generate/, async route => {
sent.push({ url: route.request().url(), body: route.request().postDataJSON() });
if (overrides.generateStatus) return json(route, overrides.generateBody || { error: 'Generation failed' }, overrides.generateStatus);
return json(route, { success: true, resource: { id: 9, title: 'New resource', kind: 'presentation' },
markdown: '# New\n\n- one', grounding: { used: true, count: 7 },
searches: overrides.searches || [], imageJobs: overrides.imageJobs || [], imageFailures: [] });
});
// The bare listing, and nothing longer — /options and /generate are matched above.
await page.route(/\/api\/my-resources$/, r => json(r, overrides.library || LIBRARY));
return sent;
}
async function openTab(page) {
await page.goto(E2E_BASE + '/');
await page.waitForSelector('button.tab-btn', { timeout: 20000 });
const vp = page.viewportSize();
if (vp && vp.width <= 768) await page.click('#btn-menu-toggle').catch(() => {});
await page.click('button.tab-btn[data-tab="myresources"]');
await page.waitForSelector('#mr-topic', { timeout: 20000 });
// Wait for the answers, not for a guess at how long they take. A fixed sleep
// here made two tests fail only in a full run: the library had not landed, so
// Modify reported "nothing to modify yet" instead of the refusal under test.
await page.waitForFunction(() => {
const list = document.getElementById('mr-list');
const picker = document.getElementById('mr-modify-target');
const listed = list && (list.querySelector('.saved-enc-item') || /Nothing yet/.test(list.textContent));
return Boolean(listed && picker && picker.options.length);
}, { timeout: 20000 });
}
const visible = (page, id) => page.evaluate(i => {
const el = document.getElementById(i);
return el ? el.offsetParent !== null : 'absent';
}, id);
test.describe('My Resources', () => {
test('the screen says what it is for, and offers every enabled source', async ({ authedPage: _, page }) => {
await stub(page);
await openTab(page);
// "Private to you" said who could see it, not what it did.
await expect(page.locator('#myresources-tab')).toContainText('Build a teaching deck or handout');
await expect(page.locator('#myresources-tab')).toContainText('Only you can see these');
for (const id of ['mr-use-corpus', 'mr-pubmed', 'mr-web-search', 'mr-with-images']) {
expect(await visible(page, id), id).toBe(true);
}
// The library is the default, because most resources should be grounded.
await expect(page.locator('#mr-use-corpus')).toBeChecked();
for (const id of ['mr-pubmed', 'mr-web-search', 'mr-with-images']) {
await expect(page.locator('#' + id), id + ' is opt-in').not.toBeChecked();
}
// Two approved models means a choice worth offering; one would not be.
expect(await visible(page, 'mr-model-row')).toBe(true);
});
test('an option an administrator has not enabled is hidden, not shown and refused', async ({ authedPage: _, page }) => {
await stub(page, { options: { ...OPTIONS, webSearchAvailable: false, pubmedAvailable: false,
imagesAvailable: false, models: ['only-one'] } });
await openTab(page);
for (const id of ['mr-web-row', 'mr-pubmed-row', 'mr-images-row']) {
expect(await visible(page, id), id).toBe(false);
}
// And the same rule on the Modify card, from the same answer.
for (const id of ['mr-modify-web-row', 'mr-modify-pubmed-row', 'mr-modify-images-row']) {
expect(await visible(page, id), id).toBe(false);
}
// One model is not a decision anyone should be asked to take.
expect(await visible(page, 'mr-model-row')).toBe(false);
});
test('Generate sends exactly what the route reads', async ({ authedPage: _, page }) => {
const sent = await stub(page);
await openTab(page);
await page.fill('#mr-topic', 'croup in children');
await page.selectOption('#mr-kind', 'presentation');
await page.fill('#mr-slide-count', '9');
await page.check('#mr-pubmed');
await page.check('#mr-web-search');
await page.fill('#mr-refinement', 'for FY1s');
// Details is folded away until somebody has something long to paste.
await page.click('#mr-details-wrap summary');
await page.fill('#mr-details', 'Cover: febrile seizure definition; red flags; when to LP; discharge advice.');
await page.click('#btn-mr-generate');
await expect.poll(() => sent.length, { timeout: 15000 }).toBeGreaterThan(0);
const body = sent[0].body;
expect(body.topic).toBe('croup in children');
expect(body.kind).toBe('presentation');
expect(body.slideCount).toBe('9');
expect(body.refinement).toBe('for FY1s');
expect(body.details).toContain('when to LP');
// Strings, because the route compares against 'true' / 'false'.
expect(body.useCorpus).toBe('true');
expect(body.withPubmed).toBe('true');
expect(body.withWebSearch).toBe('true');
expect(body.withImages).toBe('false');
expect(body.model).toBe('model-a');
// What it was written from is said plainly; ungrounded material presented as
// grounded is the failure worth preventing.
await expect(page.locator('#mr-status')).toContainText('7 library excerpts');
});
test('unticking the library is sent as false, not omitted', async ({ authedPage: _, page }) => {
const sent = await stub(page);
await openTab(page);
await page.fill('#mr-topic', 'anything');
await page.uncheck('#mr-use-corpus');
await page.click('#btn-mr-generate');
await expect.poll(() => sent.length, { timeout: 15000 }).toBeGreaterThan(0);
// The route reads `!== 'false'`, so an omitted field would silently mean on.
expect(sent[0].body.useCorpus).toBe('false');
});
test('asking for a figure in the instructions switches illustrations on', async ({ authedPage: _, page }) => {
await stub(page);
await openTab(page);
await expect(page.locator('#mr-with-images')).not.toBeChecked();
await page.fill('#mr-refinement', 'case-based, and include a diagram of the airway');
await expect(page.locator('#mr-with-images')).toBeChecked();
await expect(page.locator('#mr-image-hint')).toContainText('Illustration switched on');
// Switching it off by hand sticks: it must not fight the person using it.
await page.uncheck('#mr-with-images');
await page.fill('#mr-refinement', 'case-based, and include a diagram of the airway please');
await expect(page.locator('#mr-with-images')).not.toBeChecked();
await expect(page.locator('#mr-image-hint')).toContainText('Tick the illustration option');
});
test('with no image model, an instruction asking for one says so', async ({ authedPage: _, page }) => {
await stub(page, { options: { ...OPTIONS, imagesAvailable: false } });
await openTab(page);
await page.fill('#mr-refinement', 'include a diagram');
await expect(page.locator('#mr-image-hint')).toContainText('no image model is configured');
});
test('a failed generation says what went wrong and keeps the form', async ({ authedPage: _, page }) => {
await stub(page, { generateStatus: 500, generateBody: { error: 'Generation failed' } });
await openTab(page);
await page.fill('#mr-topic', 'anything');
await page.click('#btn-mr-generate');
await expect(page.locator('#mr-status')).toContainText('Generation failed');
// The topic is still there to try again with.
await expect(page.locator('#mr-topic')).toHaveValue('anything');
await expect(page.locator('#btn-mr-generate')).toBeEnabled();
});
test('the library is bounded, searchable, and says which empty it is', async ({ authedPage: _, page }) => {
await stub(page);
await openTab(page);
await expect(page.locator('#mr-list .saved-enc-item')).toHaveCount(3);
const box = await page.evaluate(() => {
const el = document.getElementById('mr-list');
const cs = getComputedStyle(el);
return { maxHeight: cs.maxHeight, overflowY: cs.overflowY };
});
expect(box.maxHeight).toBe('360px');
expect(box.overflowY).toBe('auto');
await page.fill('#mr-search', 'croup');
await expect(page.locator('#mr-list .saved-enc-item')).toHaveCount(1);
// Topic is searched as well as title.
await page.fill('#mr-search', 'jaundice');
await expect(page.locator('#mr-list .saved-enc-item')).toHaveCount(1);
// Telling someone whose search missed that they have never generated
// anything would be wrong.
await page.fill('#mr-search', 'zzzz-nothing');
await expect(page.locator('#mr-list')).toContainText('Nothing matches');
await page.fill('#mr-search', '');
await expect(page.locator('#mr-list .saved-enc-item')).toHaveCount(3);
});
test('an empty library says so differently', async ({ authedPage: _, page }) => {
await stub(page, { library: { success: true, resources: [] } });
await openTab(page);
await expect(page.locator('#mr-list')).toContainText('Nothing yet');
// Nothing to modify, and the picker says that rather than sitting empty.
await expect(page.locator('#mr-modify-target')).toBeDisabled();
await expect(page.locator('#mr-modify-target')).toContainText('Nothing to modify yet');
});
test('an article is never offered as slides', async ({ authedPage: _, page }) => {
await stub(page);
await openTab(page);
const row = id => page.locator('#mr-list .saved-enc-item').filter({ hasText: id });
// A deck of paragraphs is not a presentation.
await expect(row('Neonatal jaundice').locator('[data-format="pptx"]')).toHaveCount(0);
await expect(row('Neonatal jaundice').locator('[data-format="docx"]')).toHaveCount(1);
await expect(row('Neonatal jaundice').locator('[data-format="pdf"]')).toHaveCount(1);
// A presentation as Word is fine — prose absorbs slides without overflowing.
await expect(row('Croup in children').locator('[data-format="pptx"]')).toHaveCount(1);
});
test('Modify sends the instruction and every source option', async ({ authedPage: _, page }) => {
const sent = await stub(page);
await openTab(page);
// The picker is the library, so it cannot drift from it.
await expect(page.locator('#mr-modify-target option')).toHaveCount(3);
await expect(page.locator('#mr-modify-target')).toContainText('Croup in children — presentation');
await page.selectOption('#mr-modify-target', '3');
await page.fill('#mr-modify-instructions', 'split slide four');
await page.check('#mr-modify-pubmed');
await page.check('#mr-modify-images');
await page.click('#btn-mr-modify');
await expect.poll(() => sent.length, { timeout: 15000 }).toBeGreaterThan(0);
const call = sent[0];
expect(call.url).toContain('/my-resources/3/refine');
expect(call.body.instructions).toBe('split slide four');
expect(call.body.useCorpus).toBe('true');
expect(call.body.withPubmed).toBe('true');
expect(call.body.withWebSearch).toBe('false');
expect(call.body.withImages).toBe('true');
await expect(page.locator('#mr-modify-status')).toContainText('Applied');
});
test('Modify refuses locally rather than spending a round trip', async ({ authedPage: _, page }) => {
const sent = await stub(page);
await openTab(page);
await page.fill('#mr-modify-instructions', '');
await page.click('#btn-mr-modify');
await expect(page.locator('#mr-modify-status')).toContainText('Say what to change');
expect(sent.length, 'nothing was sent').toBe(0);
});
test('a refused modification surfaces the reason', async ({ authedPage: _, page }) => {
await stub(page, { refineStatus: 502,
refineBody: { error: 'That change could not be applied. Try wording it differently.' } });
await openTab(page);
await page.selectOption('#mr-modify-target', '1');
await page.fill('#mr-modify-instructions', 'do something impossible');
await page.click('#btn-mr-modify');
// Saying "applied" here is how a modification that changed nothing hides.
await expect(page.locator('#mr-modify-status')).toContainText('could not be applied');
});
test('a search that ran is reported, including one that found nothing', async ({ authedPage: _, page }) => {
await stub(page, { searches: [
{ tool: 'pubmed_search', query: 'croup', count: 6, reason: null },
{ tool: 'web_search', query: 'croup', count: 0, reason: 'no results' },
]});
await openTab(page);
await page.fill('#mr-topic', 'croup');
await page.click('#btn-mr-generate');
// A query that left the network is worth showing plainly.
await expect(page.locator('body')).toContainText('Searched PubMed');
await expect(page.locator('body')).toContainText('Nothing found on the web');
});
});