DeepSeek flash wrote 'A. Start IVIG' into option text the renderer already letters, so the slide read 'A A. Start IVIG'; and an answer of just 'A' sat alone on the answer card, sending the room back a slide to see what A was. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
82 lines
4.7 KiB
JavaScript
82 lines
4.7 KiB
JavaScript
// 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');
|
|
});
|