// ============================================================ // 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'); }); });