Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
148 lines
8 KiB
JavaScript
148 lines
8 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', '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');
|
|
});
|