diff --git a/assets/sample-figure.png b/assets/sample-figure.png
new file mode 100644
index 00000000..b84d87de
Binary files /dev/null and b/assets/sample-figure.png differ
diff --git a/public/components/my-resources.html b/public/components/my-resources.html
index 088f4826..3715b541 100644
--- a/public/components/my-resources.html
+++ b/public/components/my-resources.html
@@ -49,8 +49,9 @@
diff --git a/public/js/myResources.js b/public/js/myResources.js
index 904bf234..ff1689f1 100644
--- a/public/js/myResources.js
+++ b/public/js/myResources.js
@@ -210,41 +210,25 @@
// when the tab was reopened, looking like output for a topic nobody had
// typed. It cleared on a full page refresh and only then, which is why it
// read as a leak.
- // A rendered slide in the chosen palette. The renderer draws it, so it cannot
- // promise a look the export does not deliver.
+ // A sample deck to download: every layout, filler text, this theme.
//
- // The first view of a theme costs a pptx render, a Gotenberg round trip and a
- // pdftoppm — a second or so — and after that it is served from a disk cache.
- // So it says it is working, rather than leaving an empty gap that reads as a
- // feature that is not there.
+ // Not a picture of one slide, and not a picture of their deck. The question a
+ // theme picker answers is what the thing will look like, and the file itself
+ // answers it better than any screenshot — opened in PowerPoint, at the size it
+ // will be shown, with the fonts actually substituted the way they will be.
//
- // A failure says so too. Hiding the box on error, which is what this did,
- // makes a broken preview and an absent one look identical, from both sides of
- // the screen: nothing to see, and nothing to report.
- function showThemePreview() {
+ // An ordinary link, so it is there whether or not anything renders.
+ function showThemeSample() {
var select = document.getElementById('mr-theme');
var box = document.getElementById('mr-theme-preview');
if (!select || !select.value || !box) return;
- var url = '/api/my-resources/theme-preview/' + encodeURIComponent(select.value);
-
- var note = box.querySelector('p') || document.createElement('p');
- note.style.cssText = 'margin:0;font-size:12px;color:var(--g500);';
- note.textContent = 'Drawing a sample slide...';
- if (!note.parentNode) box.appendChild(note);
-
- var img = box.querySelector('img') || document.createElement('img');
- img.alt = 'A slide rendered in the ' + ((select.options[select.selectedIndex] || {}).text || 'chosen') + ' theme';
- img.style.cssText = 'max-width:min(100%,340px);border:1px solid var(--g200);border-radius:8px;display:none;';
- img.onload = function () { img.style.display = 'block'; note.hidden = true; };
- img.onerror = function () {
- img.style.display = 'none';
- note.hidden = false;
- note.textContent = 'No preview just now — the look itself is unaffected.';
- };
- // Assigned last: in a cache hit, onload can fire before the handlers below
- // the assignment would have been attached.
- img.src = url;
- if (!img.parentNode) box.appendChild(img);
+ var name = (select.options[select.selectedIndex] || {}).text || 'this theme';
+ var link = box.querySelector('a') || document.createElement('a');
+ link.href = '/api/my-resources/theme-sample/' + encodeURIComponent(select.value);
+ link.textContent = 'Download a sample deck in ' + name;
+ link.title = 'Every slide layout, with placeholder text, as a PowerPoint file';
+ link.style.cssText = 'font-size:12px;color:var(--blue);text-decoration:none;';
+ if (!link.parentNode) box.appendChild(link);
box.hidden = false;
}
@@ -255,7 +239,7 @@
if (!select || !hint) return;
var chosen = themes.filter(function (t) { return t.id === select.value; })[0];
hint.textContent = chosen ? (chosen.description || '') : '';
- showThemePreview();
+ showThemeSample();
}
function clearResults() {
diff --git a/src/routes/myResources.js b/src/routes/myResources.js
index cca5b1f2..7e3a9ac9 100644
--- a/src/routes/myResources.js
+++ b/src/routes/myResources.js
@@ -24,6 +24,7 @@ var { callAI } = require('../utils/ai');
var learningRetrieval = require('../utils/learningRetrieval');
var resourceImages = require('../utils/resourceImages');
var deckSchema = require('../utils/deckSchema');
+var deckSample = require('../utils/deckSample');
var deckBuild = require('../utils/deckBuild');
var deckReview = require('../utils/deckReview');
var nextcloudFiles = require('../utils/nextcloudFiles');
@@ -542,66 +543,37 @@ router.post('/my-resources/:id/to-nextcloud', async function (req, res) {
});
// ── Theme previews ──────────────────────────────────────────
-// One representative slide per theme, rendered by the renderer itself and
-// cached. Drawn rather than mocked up: a hand-made swatch drifts the moment a
-// palette or a layout changes, and the point of a preview is to be true.
+// A sample deck per theme: every layout the renderer can draw, filler text
+// throughout, downloaded and opened in PowerPoint.
//
-// Cached on disk because it costs a pptx render, a Gotenberg round trip and a
-// pdftoppm — far too much to repeat per page view, and the output only changes
-// when the catalogue or the renderer does.
-var THEME_PREVIEW_DIR = require('path').join(require('os').tmpdir(), 'deck-theme-previews');
-
-function previewDeck(theme) {
- return {
- theme: theme.id,
- slides: [{
- type: 'compare',
- heading: theme.name,
- columns: [
- { label: 'MILD', bullets: [{ text: 'Barking cough', level: 0 }, { text: 'No stridor at rest', level: 0 }] },
- { label: 'SEVERE', bullets: [{ text: 'Stridor at rest', level: 0 }, { text: 'Marked recession', level: 0 }] }
- ]
- }]
- };
-}
-
-router.get('/my-resources/theme-preview/:id', async function (req, res) {
- var fsp = require('fs/promises');
- var pathMod = require('path');
+// This replaced a server-rendered PNG of one slide. That needed a Gotenberg
+// round trip and a pdftoppm to show a single layout, could fail in ways a
+// picture cannot explain, and answered a narrower question than the one being
+// asked — what does a deck look like in this theme, not what does this one
+// slide look like. python-pptx alone builds the file, and PowerPoint draws it.
+router.get('/my-resources/theme-sample/:id', async function (req, res) {
try {
var id = deckSchema.themeId(req.params.id);
if (!id) return res.status(404).json({ error: 'No such theme' });
-
- var cached = pathMod.join(THEME_PREVIEW_DIR, id + '.png');
- try {
- var hit = await fsp.readFile(cached);
- res.setHeader('Content-Type', 'image/png');
- res.setHeader('Cache-Control', 'private, max-age=86400');
- return res.send(hit);
- } catch (e) { /* not rendered yet */ }
-
var theme = deckSchema.themes().filter(function (t) { return t.id === id; })[0];
- var pptx = await documentExport.renderDeck(previewDeck(theme), [], []);
- var images = await deckReview.slideImages(pptx, documentExport.GOTENBERG,
- documentExport.FORMATS.pptx.mime);
- if (!images.length) return res.status(503).json({ error: 'Preview rendering unavailable' });
- var png = Buffer.from(images[0].dataBase64, 'base64');
- // Best effort: a preview that cannot be cached is still a preview.
- try {
- await fsp.mkdir(THEME_PREVIEW_DIR, { recursive: true });
- await fsp.writeFile(cached, png);
- } catch (e) {}
- res.setHeader('Content-Type', 'image/png');
+ var deck = deckSample.build(theme);
+ deck.theme = id;
+ // The placeholder rides the same path a generated figure does, so the
+ // sample exercises the real figure layouts rather than a special case.
+ var pptx = await documentExport.renderDeck(deck, [deckSample.FIGURE], [deckSample.FIGURE_JOB]);
+
+ res.setHeader('Content-Type', documentExport.FORMATS.pptx.mime);
+ res.setHeader('Content-Disposition',
+ 'attachment; filename="' + id + '-sample.pptx"');
res.setHeader('Cache-Control', 'private, max-age=86400');
- res.send(png);
+ res.send(pptx);
} catch (err) {
- logger.warn('[my-resources] theme preview', { theme: req.params.id, error: err.message });
- res.status(503).json({ error: 'Preview rendering unavailable' });
+ logger.warn('[my-resources] theme sample', { theme: req.params.id, error: err.message });
+ res.status(503).json({ error: 'Could not build the sample deck.' });
}
});
-// ── The owner's library ─────────────────────────────────────
router.get('/my-resources', async function (req, res) {
try {
var rows = await db.all(
diff --git a/src/utils/deckSample.js b/src/utils/deckSample.js
new file mode 100644
index 00000000..4566d082
--- /dev/null
+++ b/src/utils/deckSample.js
@@ -0,0 +1,123 @@
+// ============================================================
+// DECK SAMPLE
+// ============================================================
+// A specimen deck: every layout the renderer can draw, in one theme, with
+// filler text.
+//
+// This is what a theme picker owes you. A picture of one slide shows one
+// layout, and a picture of *your* deck answers a different question — whether
+// the writing is any good — when the question here is only what the thing will
+// look like. So: one file, every layout, download it and look.
+//
+// The text is deliberately meaningless. Clinical content in a specimen invites
+// you to read it, and then you are judging the teaching rather than the type.
+//
+// Nothing renders here but python-pptx. No Gotenberg, no pdftoppm, no image
+// model — a theme is colours, type and rules, and the file itself shows those
+// better than a screenshot of it could.
+
+var pathMod = require('path');
+
+// Stands in for an illustration. Neutral in every palette, and plainly a
+// placeholder rather than art, so nothing in the sample looks like content.
+var FIGURE = pathMod.join(__dirname, '..', '..', 'assets', 'sample-figure.png');
+var FIGURE_JOB = 'sample-figure';
+
+function bullets(items) {
+ return items.map(function (item) {
+ return Array.isArray(item) ? { text: item[0], level: item[1] } : { text: item, level: 0 };
+ });
+}
+
+/**
+ * The specimen for one theme.
+ *
+ * Ordered the way a real deck runs — title, content, a divider, more content —
+ * so the sample also shows how the layouts sit next to each other, which is
+ * where a palette usually goes wrong.
+ */
+function build(theme) {
+ var name = (theme && theme.name) || 'Sample';
+ return {
+ title: name,
+ subtitle: 'A sample deck — every slide layout, in this theme',
+ date: 'Placeholder text throughout',
+ slides: [
+ { type: 'title', heading: name,
+ subtitle: 'A sample deck — every slide layout, in this theme' },
+
+ { type: 'bullets', heading: 'Bullets, with a sub-point',
+ bullets: bullets([
+ 'A first point, about the length one usually runs to',
+ 'A second point that carries a little more detail with it',
+ ['An indented sub-point beneath it', 1],
+ ['A second sub-point, to show the spacing', 1],
+ 'A closing point to fill the slide out'
+ ]),
+ notes: 'Speaker notes look like this. They do not appear on the slide.' },
+
+ { type: 'compare', heading: 'Two labelled columns',
+ columns: [
+ { label: 'LEFT', bullets: bullets(['First item', 'Second item', 'Third item']) },
+ { label: 'RIGHT', bullets: bullets(['First item', 'Second item', 'Third item']) }
+ ] },
+
+ { type: 'two', heading: 'Two plain columns',
+ left: bullets(['One', 'Two', 'Three']),
+ right: bullets(['Four', 'Five', 'Six']) },
+
+ { type: 'table', heading: 'A table',
+ header: ['Column', 'Second', 'Third'],
+ rows: [
+ ['First row', 'Value', 'Value'],
+ ['Second row', 'Value', 'Value'],
+ ['Third row', 'Value', 'Value'],
+ ['Fourth row', 'Value', 'Value']
+ ] },
+
+ { type: 'callout', heading: 'A callout',
+ text: 'One sentence on a tinted card, for something worth stopping on.' },
+
+ { type: 'figure', heading: 'A figure beside its text',
+ image_job: FIGURE_JOB,
+ bullets: bullets([
+ 'Text takes the left, the picture the right',
+ 'Three or four points sit comfortably here',
+ 'The grey panel stands in for an illustration'
+ ]) },
+
+ { type: 'section', heading: 'A section divider' },
+
+ { type: 'image', heading: 'A full-slide figure',
+ image_job: FIGURE_JOB,
+ caption: 'A caption sits under the picture.' },
+
+ { type: 'custom', heading: 'A custom slide: shapes, an arrow, a chart',
+ shapes: [
+ { kind: 'roundRect', x: 5, y: 30, w: 20, h: 14, fill: 'EFF6FF', line: '2563EB',
+ runs: [{ text: 'First', bold: true, align: 'center' }] },
+ { kind: 'arrow', x: 26, y: 34, w: 7, h: 6, fill: '94A3B8' },
+ { kind: 'roundRect', x: 34, y: 30, w: 20, h: 14, fill: 'EFF6FF', line: '2563EB',
+ runs: [{ text: 'Second', bold: true, align: 'center' }] },
+ { kind: 'line', x: 5, y: 50, w: 49, h: 0, line: 'E5E7EB' },
+ { kind: 'text', x: 5, y: 53, w: 49, h: 20,
+ runs: [
+ { text: 'Free text under a rule', bold: true },
+ { text: 'A bulleted run inside a text shape', bullet: true },
+ { text: 'And a second one', bullet: true }
+ ] },
+ { kind: 'chart', chart: 'column', x: 58, y: 28, w: 37, h: 45,
+ categories: ['One', 'Two', 'Three'],
+ series: [{ name: 'Series', values: [8, 14, 11] }] }
+ ] },
+
+ { type: 'bullets', heading: 'References',
+ bullets: bullets([
+ 'A reference, set the way the last slide of a real deck sets them',
+ 'A second reference, longer, to show how a wrapped line falls'
+ ]) }
+ ]
+ };
+}
+
+module.exports = { build: build, FIGURE: FIGURE, FIGURE_JOB: FIGURE_JOB };
diff --git a/test/deck-theme-sample.test.js b/test/deck-theme-sample.test.js
new file mode 100644
index 00000000..aa7b20e4
--- /dev/null
+++ b/test/deck-theme-sample.test.js
@@ -0,0 +1,119 @@
+// 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', 'figure', 'image', 'custom'];
+
+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/);
+});
diff --git a/test/deck-themes.test.js b/test/deck-themes.test.js
index 3e998c04..cbb8830a 100644
--- a/test/deck-themes.test.js
+++ b/test/deck-themes.test.js
@@ -61,14 +61,20 @@ test('re-skinning is a column write, never a regeneration', () => {
assert.match(handler.slice(0, 1400), /has no slide layout/, 'flat markdown has no theme');
});
-test('the preview is rendered by the renderer, not mocked up', () => {
- // A hand-drawn swatch drifts the moment a palette or a layout changes.
+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 preview = route.slice(route.indexOf("router.get('/my-resources/theme-preview/:id'"));
- assert.match(preview.slice(0, 1800), /documentExport\.renderDeck\(previewDeck\(theme\)/);
- assert.match(preview.slice(0, 1800), /deckReview\.slideImages/);
- assert.match(preview.slice(0, 1800), /deckSchema\.themeId\(req\.params\.id\)/, 'the id is validated, not used as a path');
- assert.match(route, /THEME_PREVIEW_DIR/, 'cached: it costs a render, a Gotenberg trip and a rasterise');
+ 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 library offers a theme only where there is a deck to re-skin', () => {
diff --git a/test/my-resources-refine.test.js b/test/my-resources-refine.test.js
index 713dde32..ef4dcb51 100644
--- a/test/my-resources-refine.test.js
+++ b/test/my-resources-refine.test.js
@@ -69,6 +69,7 @@ function router(t, overrides = {}) {
},
'../utils/deckBuild': deckBuild,
'../utils/deckSchema': require('../src/utils/deckSchema'),
+ '../utils/deckSample': require('../src/utils/deckSample'),
'../utils/deckReview': {
slideImages: async () => (overrides.slideImages || []),
review: async (deck) => { reviewCalls.push(deck); return { deck: deck, reviewed: true, reason: 'ok' }; },
diff --git a/test/theme-preview-visible.test.js b/test/theme-preview-visible.test.js
deleted file mode 100644
index aa6d7997..00000000
--- a/test/theme-preview-visible.test.js
+++ /dev/null
@@ -1,60 +0,0 @@
-// The theme preview box was hidden on error and hidden until an image loaded,
-// so a preview that failed and a preview that did not exist looked exactly the
-// same — an empty gap. That cost a diagnosis: the endpoint was working the
-// whole time and there was no way to tell from the page.
-//
-// The first view of a theme is a pptx render, a Gotenberg round trip and a
-// pdftoppm, so it is a second or so before anything appears. Silence for that
-// second reads as a missing feature.
-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 ui = read('public/js/myResources.js');
-const fn = ui.slice(ui.indexOf('function showThemePreview'), ui.indexOf('function describeTheme'));
-
-test('a failed preview says so instead of vanishing', () => {
- assert.doesNotMatch(fn, /onerror = function \(\) \{ box\.hidden = true; \}/);
- assert.match(fn, /img\.onerror = function \(\)[\s\S]{0,200}note\.textContent = 'No preview just now/);
-});
-
-test('it says it is working while the render happens', () => {
- assert.match(fn, /note\.textContent = 'Drawing a sample slide\.\.\.'/);
- assert.match(fn, /box\.hidden = false/);
-});
-
-test('the note is replaced by the image, not left stacked above it', () => {
- assert.match(fn, /img\.onload = function \(\) \{ img\.style\.display = 'block'; note\.hidden = true; \}/);
-});
-
-test('src is assigned after the handlers, so a cache hit is not missed', () => {
- // A cached preview can complete before a handler attached after the
- // assignment would exist, leaving the box stuck on "Drawing...".
- const onload = fn.indexOf('img.onload');
- const onerror = fn.indexOf('img.onerror');
- const src = fn.indexOf('img.src =');
- assert.ok(onload > -1 && onerror > -1 && src > -1);
- assert.ok(src > onload && src > onerror, 'img.src must be set after both handlers');
-});
-
-test('no request for a theme that has not been chosen', () => {
- assert.match(fn, /if \(!select \|\| !select\.value \|\| !box\) return;/);
-});
-
-test('the preview is requested once per change, not twice', () => {
- // describeTheme ends by calling it; a second call beside it fired an
- // identical request on every load.
- assert.equal((ui.match(/\n\s*showThemePreview\(\);/g) || []).length, 1);
-});
-
-test('the route it calls exists and is the one that renders', () => {
- const route = read('src/routes/myResources.js');
- assert.match(fn, /'\/api\/my-resources\/theme-preview\/' \+ encodeURIComponent\(select\.value\)/);
- assert.match(route, /router\.get\('\/my-resources\/theme-preview\/:id'/);
- // It must be registered before the '/my-resources/:id' catch-all, or that
- // would answer instead.
- assert.ok(route.indexOf("'/my-resources/theme-preview/:id'") < route.indexOf("router.get('/my-resources/:id'"),
- 'theme-preview must be registered before the :id route');
-});