Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
99 lines
5.5 KiB
JavaScript
99 lines
5.5 KiB
JavaScript
// A deck's look is a theme, not a colour the model writes into a field. The
|
|
// slide vocabulary is structural — bullets, compare, table, callout — and none
|
|
// of it carries a colour, so this is the whole of a deck's styling.
|
|
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 deckSchema = require('../src/utils/deckSchema');
|
|
const catalogue = JSON.parse(read('assets/deck-themes.json')).themes;
|
|
|
|
test('every theme is complete, and every colour is a real colour', () => {
|
|
assert.ok(catalogue.length >= 4, 'a picker of one is not a choice');
|
|
for (const theme of catalogue) {
|
|
for (const key of ['id', 'name', 'accent', 'ink', 'muted', 'rule', 'paper', 'font']) {
|
|
assert.ok(theme[key], theme.id + ' is missing ' + key);
|
|
}
|
|
for (const key of ['accent', 'ink', 'muted', 'rule', 'paper']) {
|
|
assert.match(theme[key], /^[0-9A-Fa-f]{6}$/, theme.id + '.' + key + ' is not six hex digits');
|
|
}
|
|
}
|
|
const ids = catalogue.map(t => t.id);
|
|
assert.equal(ids.length, new Set(ids).size, 'theme ids must be unique');
|
|
});
|
|
|
|
test('the app and the renderer read one catalogue, so they cannot disagree', () => {
|
|
const py = read('scripts/render_pptx.py');
|
|
assert.match(py, /assets", "deck-themes\.json/);
|
|
const js = read('src/utils/deckSchema.js');
|
|
assert.match(js, /deck-themes\.json/);
|
|
assert.deepEqual(deckSchema.themes().map(t => t.id), catalogue.map(t => t.id));
|
|
});
|
|
|
|
test('an unknown theme is dropped rather than stored', () => {
|
|
// A stored deck must never claim a theme the renderer will silently ignore.
|
|
const slides = [{ type: 'bullets', heading: 'x', bullets: ['a'] }];
|
|
assert.equal(deckSchema.normalise({ theme: 'teaching-amber', slides }).theme, 'teaching-amber');
|
|
assert.equal(deckSchema.normalise({ theme: 'not-a-theme', slides }).theme, '');
|
|
assert.equal(deckSchema.normalise({ theme: '../../etc/passwd', slides }).theme, '');
|
|
assert.equal(deckSchema.normalise({ slides }).theme, '');
|
|
});
|
|
|
|
test('the renderer rebinds its palette instead of hardcoding one', () => {
|
|
// Every builder reads these names, which is what makes a theme a rebinding
|
|
// rather than a change to ten slide builders.
|
|
const py = read('scripts/render_pptx.py');
|
|
assert.match(py, /def apply_theme\(theme_id\)/);
|
|
assert.match(py, /global INK, MUTED, ACCENT, RULE, PAPER, FONT/);
|
|
assert.match(py, /apply_theme\(spec\.get\("theme"\)\)/);
|
|
// An unusable theme must not cost the deck its render.
|
|
assert.match(py, /a deck rendering in the wrong colours beats a deck not rendering/);
|
|
});
|
|
|
|
test('re-skinning is a column write, never a regeneration', () => {
|
|
const route = read('src/routes/myResources.js');
|
|
const handler = route.slice(route.indexOf("router.put('/my-resources/:id/theme'"));
|
|
assert.match(handler.slice(0, 1600), /UPDATE user_resources SET theme = \?, deck = COALESCE\(\?::jsonb, deck\)/);
|
|
assert.doesNotMatch(handler.slice(0, 1600), /callAI/, 'no model call: nothing can reword a slide');
|
|
assert.match(handler.slice(0, 1600), /AND user_id = \?/, 'scoped to the owner');
|
|
// Markdown slides take a theme too — the renderer applies it to both — so
|
|
// the only thing refused is an article, which has no slides at all.
|
|
assert.match(handler.slice(0, 1600), /An article has no slides/, 'articles have no theme');
|
|
});
|
|
|
|
test('the theme is shown by a deck the renderer built, not a mocked-up swatch', () => {
|
|
// A hand-drawn swatch drifts the moment a palette or a layout changes. This
|
|
// used to be a server-rendered PNG of one slide; it is now a downloadable
|
|
// sample deck of every layout — same principle, no Gotenberg.
|
|
// See test/deck-theme-sample.test.js.
|
|
const route = read('src/routes/myResources.js');
|
|
const sample = route.slice(route.indexOf("router.get('/my-resources/theme-sample/:id'"));
|
|
assert.match(sample.slice(0, 1800), /documentExport\.renderDeck\(deck, \[deckSample\.FIGURE\]/);
|
|
assert.match(sample.slice(0, 1800), /deckSchema\.themeId\(req\.params\.id\)/, 'the id is validated, not used as a path');
|
|
// Scoped to this route: the deck reviewer legitimately rasterises the deck
|
|
// it is about to judge. Showing a theme does not.
|
|
const body = sample.slice(0, sample.indexOf('\n});'));
|
|
assert.doesNotMatch(body, /slideImages|GOTENBERG|pdftoppm/, 'no rasterising to show a theme');
|
|
assert.doesNotMatch(route, /THEME_PREVIEW_DIR|theme-preview/, 'the cached-PNG preview is gone');
|
|
});
|
|
|
|
test('the theme is chosen in the editor, not on the list row', () => {
|
|
const ui = read('public/js/myResources.js');
|
|
// The list row looks and downloads; it carries no theme dropdown.
|
|
assert.doesNotMatch(ui, /theme\.dataset\.theme = String\(row\.id\)/);
|
|
assert.doesNotMatch(ui, /closest\('\[data-theme\]'\)/);
|
|
// The editor still offers the catalogue and a sample of each theme.
|
|
assert.match(ui, /getElementById\('mr-theme'\)/);
|
|
assert.match(ui, /theme-sample/);
|
|
});
|
|
|
|
test('a modification keeps the deck\'s theme', () => {
|
|
// The model returns a new deck; the look was never its to choose. The
|
|
// column keeps the theme regardless, but the deck field must agree with it.
|
|
const route = read('src/routes/myResources.js');
|
|
const refine = route.slice(route.indexOf("router.post('/my-resources/:id/refine'"));
|
|
assert.match(refine, /revisedDeck\.theme = existingDeck\.theme \|\| existing\.theme \|\| revisedDeck\.theme/);
|
|
assert.match(refine.slice(0, 2500), /SELECT id, kind, topic, markdown, deck, image_ids, theme FROM user_resources/);
|
|
});
|