// A deck has a shape as well as a look: a journal club runs PICO → methods → // appraisal → bottom line, a board review runs in case → question → answer // cycles. The format is a recipe the model follows and may repeat, never a // slide type of its own. 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 deckFormats = require('../src/utils/deckFormats'); const deckSchema = require('../src/utils/deckSchema'); const catalogue = JSON.parse(read('assets/deck-formats.json')).formats; const LAYOUTS = ['title', 'section', 'bullets', 'two', 'compare', 'table', 'callout', 'question', 'figure', 'image', 'flow', 'custom']; test('every format is complete and speaks only the slide vocabulary', () => { assert.ok(catalogue.length >= 5, 'a picker of a few is not a choice'); for (const format of catalogue) { for (const key of ['id', 'name', 'audience', 'description', 'sequence']) { assert.ok(format[key], format.id + ' is missing ' + key); } assert.ok(format.sequence.length >= 3, format.id + ' has too few parts'); for (const part of format.sequence) { assert.ok(part.part && part.purpose, format.id + ' has a part without a name or purpose'); for (const layout of part.layouts || []) { assert.ok(LAYOUTS.includes(layout), format.id + ' names a layout the renderer cannot draw: ' + layout); } } assert.ok(format.sequence.some(p => p.repeat), format.id + ' has no part the model may reuse'); } const ids = catalogue.map(f => f.id); assert.equal(ids.length, new Set(ids).size, 'format ids must be unique'); }); test('the brief lists the parts in order, says which repeat, and never invents a slide type', () => { const format = catalogue[0]; const brief = deckFormats.instructions(format.id); assert.match(brief, new RegExp('FORMAT: this is a ' + format.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); format.sequence.forEach((part, i) => { assert.ok(brief.indexOf((i + 1) + '. ' + part.part) !== -1, 'part ' + part.part + ' is listed in order'); }); assert.match(brief, /\(repeatable\)/); assert.match(brief, /may be used more than once/); assert.match(brief, /Every slide is still one of the types above/); assert.equal(deckFormats.instructions('not-a-format'), ''); assert.equal(deckFormats.instructions(''), ''); }); test('an unknown format is dropped rather than stored, and a known one survives normalisation', () => { const slides = [{ type: 'bullets', heading: 'x', bullets: ['a'] }]; assert.equal(deckSchema.normalise({ format: catalogue[0].id, slides }).format, catalogue[0].id); assert.equal(deckSchema.normalise({ format: 'not-a-format', slides }).format, ''); assert.equal(deckSchema.normalise({ slides }).format, ''); }); test('the format reaches the model after the layouts, on generation and on revision', () => { const route = read('src/routes/myResources.js'); assert.match(route, /deckSchema\.instructions\(opts\.slideCount, opts\.figureCount\) \+[\s\S]{0,400}deckFormats\.instructions\(opts\.format\)/); assert.match(route, /deckFormats\.instructions\(existingDeck\.format\)/); assert.match(route, /revisedDeck\.format = existingDeck\.format/); assert.match(route, /formats: deckFormats\.catalogue\(\)/); const ui = read('public/js/myResources.js'); assert.match(ui, /format: \(document\.getElementById\('mr-format'\) \|\| \{\}\)\.value/); }); test('the picker is told what a format is for, never the recipe itself', () => { for (const entry of deckFormats.catalogue()) { assert.deepEqual(Object.keys(entry).sort(), ['audience', 'description', 'id', 'length', 'name']); } }); test('a question slide never letters its options twice, and a bare-letter answer names its option', () => { const deck = deckSchema.normalise({ slides: [{ type: 'question', heading: 'Case 1', question: 'Next step?', options: ['A. Start IVIG plus aspirin', 'B) Antibiotics', '(C) Observe', 'Give NSAIDs'], answer: 'A', explanation: 'x' }] }); const q = deck.slides[0]; assert.deepEqual(q.options, ['Start IVIG plus aspirin', 'Antibiotics', 'Observe', 'Give NSAIDs']); assert.equal(q.answer, 'A — Start IVIG plus aspirin'); const prose = deckSchema.normalise({ slides: [{ type: 'question', question: 'q', options: ['x', 'y'], answer: 'Start IVIG' }] }); assert.equal(prose.slides[0].answer, 'Start IVIG', 'a written-out answer is left alone'); const stray = deckSchema.normalise({ slides: [{ type: 'question', question: 'q', options: ['x'], answer: 'D.' }] }); assert.equal(stray.slides[0].answer, 'D', 'a letter with no such option is shown as the letter'); });