pediatric-ai-scribe-v3/test/deck-theme-sample.test.js
Daniel 5f738fbe30 feat: a theme is shown by a sample deck you download, not a picture
A picture of one slide answers a narrower question than the one a theme
picker is asked. What a person wants to know is what a deck will look
like in this theme — all of it, at the size it will be shown, with the
fonts substituted the way they will be. One rendered slide showed one
layout, in content someone then read instead of looking at.

So: a sample deck per theme. Every layout the renderer can draw — title,
bullets with a sub-point, both two-column forms, table, callout, figure,
full-slide figure, section divider, a custom slide with shapes, an arrow
and a chart, and a references slide — with filler text throughout.
Download it, open it, see the theme.

The text is deliberately meaningless. Clinical content in a specimen
invites you to read it, and then you are judging the teaching rather
than the type; that is what the old croup slide got wrong.

No engine. The previous preview needed a Gotenberg round trip and a
pdftoppm to produce a PNG, cached on disk because of what it cost, and
could fail in ways a missing picture cannot explain. python-pptx builds
the file in ~330ms and PowerPoint draws it. The link is a plain anchor,
so it is there whether or not anything on the server is well.

The figure placeholder rides the same path a generated figure does —
attachFigures downgrades a figure slide with no picture to bullets and
an image slide to a section, so without it the sample would silently
stop showing those two layouts.

Verified: all five themes build, 11 slides, the chart is a real chart
and the placeholder embeds as real media.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 22:24:05 +02:00

119 lines
5.9 KiB
JavaScript

// A theme picker owes you a look at the theme. A server-rendered PNG of one
// slide answered a narrower question than the one being asked, needed a
// Gotenberg round trip and a pdftoppm to do it, and could fail in ways a
// missing picture cannot explain. A sample deck answers it properly: every
// layout, filler text, opened in PowerPoint at the size it will be shown.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const read = f => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
const deckSample = require('../src/utils/deckSample');
const deckSchema = require('../src/utils/deckSchema');
// The vocabulary the renderer can draw, from the schema itself, so a layout
// added later fails this until the sample shows it too.
const SCHEMA_TYPES = ['title', 'section', 'bullets', 'two', 'compare', 'table',
'callout', 'figure', 'image', 'custom'];
test('the sample shows every layout the renderer can draw', () => {
const deck = deckSample.build({ id: 'clinical-blue', name: 'Clinical Blue' });
const present = new Set(deck.slides.map(s => s.type));
for (const type of SCHEMA_TYPES) {
assert.ok(present.has(type), 'the sample never shows a "' + type + '" slide');
}
});
test('that list is the schema\'s own, not a copy that can drift', () => {
const schema = read('src/utils/deckSchema.js');
const declared = schema.match(/var VALID = \[([^\]]*)\]/)[1]
.split(',').map(s => s.trim().replace(/'/g, '')).filter(Boolean);
assert.deepEqual(declared.slice().sort(), SCHEMA_TYPES.slice().sort(),
'deckSchema.VALID changed — the sample must show the new layout too');
});
test('the custom slide exercises the shape vocabulary, not just one box', () => {
const deck = deckSample.build({ id: 'slate', name: 'Slate' });
const custom = deck.slides.find(s => s.type === 'custom');
const kinds = new Set(custom.shapes.map(s => s.kind));
for (const kind of ['roundRect', 'arrow', 'line', 'text', 'chart']) {
assert.ok(kinds.has(kind), 'no ' + kind + ' shape in the custom sample');
}
});
test('the figure layouts carry a placeholder, or they render as plain text', () => {
// attachFigures downgrades a figure slide with no picture to bullets and an
// image slide to a section, so without this the sample would silently stop
// showing those two layouts at all.
const deck = deckSample.build({ id: 'ward-teal', name: 'Ward Teal' });
for (const type of ['figure', 'image']) {
const slide = deck.slides.find(s => s.type === type);
assert.equal(slide.image_job, deckSample.FIGURE_JOB, type + ' has no figure attached');
}
assert.ok(fs.existsSync(deckSample.FIGURE), 'the placeholder image is missing from the repo');
});
test('the sample carries no clinical content', () => {
// Real content invites you to read it, and then you are judging the teaching
// rather than the type — which is what the previous sample slide got wrong.
const text = JSON.stringify(deckSample.build({ id: 'slate', name: 'Slate' })).toLowerCase();
for (const word of ['stridor', 'croup', 'cough', 'dose', 'mg/kg', 'patient', 'fever']) {
assert.ok(!text.includes(word), 'the sample mentions "' + word + '"');
}
});
test('every theme builds, and the deck is stamped with the one asked for', () => {
for (const theme of deckSchema.themes()) {
const deck = deckSample.build(theme);
assert.ok(deck.slides.length >= SCHEMA_TYPES.length);
assert.equal(deck.title, theme.name);
}
});
// ---- how it is served and offered ------------------------------------------
test('the route renders with python-pptx alone — no Gotenberg, no pdftoppm', () => {
const route = read('src/routes/myResources.js');
const fn = route.slice(route.indexOf("router.get('/my-resources/theme-sample/:id'"));
const body = fn.slice(0, fn.indexOf('\n});'));
assert.match(body, /documentExport\.renderDeck\(deck, \[deckSample\.FIGURE\], \[deckSample\.FIGURE_JOB\]\)/);
assert.doesNotMatch(body, /slideImages|GOTENBERG|pdftoppm/);
assert.doesNotMatch(route, /theme-preview/, 'the rendered-PNG preview should be gone');
});
test('it downloads as a named file rather than rendering in the tab', () => {
const route = read('src/routes/myResources.js');
const fn = route.slice(route.indexOf("router.get('/my-resources/theme-sample/:id'"));
assert.match(fn.slice(0, 1400), /attachment; filename="' \+ id \+ '-sample\.pptx"/);
assert.match(fn.slice(0, 1400), /documentExport\.FORMATS\.pptx\.mime/);
});
test('an unknown theme is refused rather than rendered as a default', () => {
const route = read('src/routes/myResources.js');
const fn = route.slice(route.indexOf("router.get('/my-resources/theme-sample/:id'"));
assert.match(fn.slice(0, 600), /if \(!id\) return res\.status\(404\)/);
});
test('the route is registered before the :id catch-all', () => {
const route = read('src/routes/myResources.js');
assert.ok(route.indexOf("'/my-resources/theme-sample/:id'") < route.indexOf("router.get('/my-resources/:id'"));
});
test('the link is a plain anchor, present whatever the server does', () => {
// The previous preview hid itself when it failed, so a broken preview and an
// absent one looked the same. A link cannot fail to appear.
const ui = read('public/js/myResources.js');
const fn = ui.slice(ui.indexOf('function showThemeSample'), ui.indexOf('function describeTheme'));
assert.match(fn, /createElement\('a'\)/);
assert.match(fn, /link\.href = '\/api\/my-resources\/theme-sample\/'/);
assert.match(fn, /box\.hidden = false/);
assert.doesNotMatch(fn, /onerror/);
assert.doesNotMatch(ui, /showThemePreview/);
});
test('the link names the theme it will download', () => {
const ui = read('public/js/myResources.js');
const fn = ui.slice(ui.indexOf('function showThemeSample'), ui.indexOf('function describeTheme'));
assert.match(fn, /'Download a sample deck in ' \+ name/);
});