// 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', 'question', 'figure', 'image', 'custom', 'flow']; 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/); }); test('a flowchart is steps in the schema, boxes and arrows on the slide, a numbered list in Word', () => { const deckSchema = require('../src/utils/deckSchema'); const docSpec = require('../src/utils/docSpec'); const deck = deckSchema.normalise({ title: 't', slides: [ { type: 'flow', heading: 'Croup', steps: [{ text: 'Assess', note: 'stridor at rest?' }, { text: 'Dexamethasone' }, { text: 'Reassess' }, { text: 'Admit or home' }] }, { type: 'flow', heading: 'Long', steps: [1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => 'Step ' + i) }, { type: 'flow', heading: 'One', steps: ['only one'] } ] }, []); assert.deepEqual(deck.slides.map(s => [s.type, (s.steps || s.bullets).length]), [['flow', 4], ['flow', 8], ['bullets', 1]], 'eight steps at most; a single step is a bullet, not a flow'); // Across for four: four boxes, three arrows, all inside the slide. const across = deckSchema.flowShapes(deck.slides[0]); assert.deepEqual(across.map(s => s.kind), ['roundRect', 'arrow', 'roundRect', 'arrow', 'roundRect', 'arrow', 'roundRect']); assert.ok(across.every(s => s.x >= 0 && s.y >= 0 && s.x + s.w <= 100 && s.y + s.h <= 100)); assert.equal(across[0].runs[1].text, 'stridor at rest?', 'the note is the second line of its box'); // Down for eight: arrows point down, nothing runs off the bottom. const down = deckSchema.flowShapes(deck.slides[1]); assert.equal(down.filter(s => s.kind === 'arrowDown').length, 7); assert.ok(Math.max(...down.map(s => s.y + s.h)) <= 100); // Word gets the steps as a numbered list, in order. const doc = docSpec.build({ deck: { title: 't', slides: [deck.slides[0]] }, markdown: '' }); const list = doc.blocks.find(b => b.type === 'bullets'); assert.equal(list.items[0].text, '1. Assess — stridor at rest?'); assert.equal(list.items[3].text, '4. Admit or home'); // The renderer never sees "flow": it is handed the shapes. const src = fs.readFileSync(path.join(__dirname, '..', 'src/utils/documentExport.js'), 'utf8'); assert.match(src, /if \(copy\.type === 'flow'\) \{\s*copy\.shapes = deckSchema\.flowShapes\(copy\);\s*copy\.type = 'custom';/); assert.match(deckSchema.instructions(6, 0), /"type":"flow"/, 'the model is told it exists'); });