pediatric-ai-scribe-v3/test/resource-previews.test.js
Daniel e92b72d406
Some checks failed
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Docker Build / Build Docker image (push) Successful in 7s
Forgejo Docker Build / End-to-end (browser) (push) Failing after 8s
feat: preview a resource, or a theme's sample deck, as pages — without downloading
A PowerPoint or Word file cannot be shown in a browser and a phone has
nowhere to open one. Preview renders the resource the way its download
would be built, turns it into one PNG per page (Gotenberg to PDF, pdftoppm
to pages), and shows the pages in an overlay that scrolls and pinch-zooms
like anything else. Rendered once per version — updated_at and theme are in
the key — and served from disk afterwards. The theme picker's sample deck
has the same Preview beside its download.

Every presentation now shows the theme picker in the library, since
markdown slides take a theme too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 14:48:45 +02:00

48 lines
3.3 KiB
JavaScript

// A resource, or a theme's sample deck, shown as pages in the page itself —
// on a phone as much as anywhere — rendered once per version.
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');
test('the cache key follows the version, so a modification or a re-skin renders afresh', () => {
const previews = require('../src/utils/previewPages');
const a = previews.cacheKey(['resource', 7, '2026-09-13T10:00:00Z', 'ward-teal', 'presentation']);
const same = previews.cacheKey(['resource', 7, '2026-09-13T10:00:00Z', 'ward-teal', 'presentation']);
const edited = previews.cacheKey(['resource', 7, '2026-09-13T11:00:00Z', 'ward-teal', 'presentation']);
const reskinned = previews.cacheKey(['resource', 7, '2026-09-13T10:00:00Z', 'slate', 'presentation']);
assert.equal(a, same);
assert.notEqual(a, edited);
assert.notEqual(a, reskinned);
assert.match(a, /^[0-9a-f]{40}$/, 'a directory name, nothing a caller typed');
});
test('previews are served for a resource and for a theme sample, and only to the owner', () => {
const route = read('src/routes/myResources.js');
assert.match(route, /router\.get\('\/my-resources\/:id\/preview', async/);
assert.match(route, /router\.get\('\/my-resources\/:id\/preview\/:page', async/);
assert.match(route, /router\.get\('\/my-resources\/theme-sample\/:id\/preview', async/);
assert.match(route, /router\.get\('\/my-resources\/theme-sample\/:id\/preview\/:page', async/);
// The resource routes read with the owner in the WHERE, like every other route here.
const preview = route.slice(route.indexOf("router.get('/my-resources/:id/preview'"), route.indexOf("// The sample deck of a theme"));
assert.equal((preview.match(/AND user_id = \?/g) || []).length, 2);
// Rendered the way the download is: the same exporter, the same theme.
assert.match(route, /documentExport\.render\(row\.markdown, row\.kind, format,\s*\{ images: figures, deck: row\.deck, theme: row\.theme, figureIds: figureIdList\(row\.image_ids\) \}\)/);
assert.match(route, /previewPages\.cacheKey\(\['resource', row\.id, row\.updated_at, row\.theme \|\| '', row\.kind\]\)/);
assert.match(route, /previewPages\.cacheKey\(\['theme-sample', id, JSON\.stringify\(theme \|\| \{\}\)\]\)/);
});
test('the page offers Preview beside the downloads and beside the theme sample, and the gallery scrolls', () => {
const js = read('public/js/myResources.js');
assert.match(js, /look\.dataset\.preview = String\(row\.id\)/);
assert.match(js, /openPreview\('\/api\/my-resources\/theme-sample\/' \+ encodeURIComponent\(select\.value\) \+ '\/preview'/);
// Fetched with the auth header — an <img src> cannot carry one.
assert.match(js, /fetch\(base \+ '\/' \+ n, \{ headers: getAuthHeaders\(\) \}\)/);
assert.match(js, /URL\.revokeObjectURL\(u\)/, 'blob URLs are released when the preview closes');
// Every presentation takes a theme now, markdown slides included.
assert.doesNotMatch(js, /row\.has_deck !== false && themeCatalogue\.length > 1/);
const css = read('public/css/styles.css');
assert.match(css, /\.mr-preview-pages \{[^}]*overflow:auto/);
assert.match(css, /\.mr-preview-page \{[^}]*width:100%/);
});