const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const { render } = require('../src/web/public/js/shared/slide-preview-renderer'); const { buildThumbnailPreviewPayload } = require('../src/web/lib/media/slide-thumbnails'); const placeholderUtils = require('../src/web/public/js/shared/placeholder-utils'); const { resolveRegionContent } = require('../src/web/public/js/shared/region-content'); const { getRegionEditorScripts } = require('../src/web/lib/region-scripts'); function loadEditorTimeDateModule() { const source = fs.readFileSync(require.resolve('../src/web/public/js/regions/type/time-date.js'), 'utf8'); const modules = new Map(); const sandbox = { window: { pulseRegionTypes: { register(type, definition) { modules.set(type, definition); } }, pulseRegionUtils: { escapeHtml(value) { return String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }, sanitizePreviewHtml(value) { return String(value); } }, placeholderUtils: placeholderUtils } }; vm.runInNewContext(source, sandbox, { filename: 'time-date.js' }); return modules.get('time-date'); } test('thumbnail payload passes saved region geometry and content to the popup renderer', () => { const payload = buildThumbnailPreviewPayload({ template: { canvas_size_width: 1280, canvas_size_height: 720, regions: [{ id: 14, region_key: 'clock', region_type: 'time-date', x: 20, y: 30, width: 400, height: 90, z_index: 3 }] }, content: { clock: { value: '{{hh}}:{{mm}}', timezone: 'Europe/London' } } }); assert.equal(payload.regions[0].x, 20); assert.equal(payload.regions[0].width, 400); assert.equal(payload.regions[0].content.value, '{{hh}}:{{mm}}'); assert.equal(payload.existingContent.clock.timezone, 'Europe/London'); }); test('thumbnail popup respects template defaults, read-only regions and explicit slide content', () => { const payload = buildThumbnailPreviewPayload({ template: { canvas_size_width: 1280, canvas_size_height: 720, regions: [ { region_key: 'logo', region_type: 'image', read_only: 1, default_content: { value: '/media/uploads/template-logo.png' }, x: 0, y: 0, width: 200, height: 100 }, { region_key: 'heading', region_type: 'text', default_content: JSON.stringify({ value: 'Template heading' }), x: 0, y: 100, width: 400, height: 100 }, { region_key: 'custom', region_type: 'text', default_content: { value: 'Default custom' }, x: 0, y: 200, width: 400, height: 100 }, { region_key: 'blank', region_type: 'text', default_content: { value: 'Do not restore' }, x: 0, y: 300, width: 400, height: 100 } ] }, content: { logo: { value: '/media/uploads/stale-logo.png' }, custom: { value: 'Slide heading' }, blank: { value: '' } } }); assert.equal(payload.regions[0].content.value, '/media/uploads/template-logo.png'); assert.equal(payload.regions[0].content.type, 'image'); assert.equal(payload.regions[1].content.value, 'Template heading'); assert.equal(payload.regions[2].content.value, 'Slide heading'); assert.equal(payload.regions[3].content.value, ''); assert.equal(payload.existingContent.logo.value, '/media/uploads/template-logo.png'); assert.equal(payload.existingContent.heading.value, 'Template heading'); assert.match(payload.html, /template-logo\.png/); assert.match(payload.html, /Template heading/); assert.match(payload.html, /Slide heading/); assert.doesNotMatch(payload.html, /stale-logo|Default custom|Do not restore/); }); test('editor and popup load the same browser-compatible content resolver', () => { const sandbox = { window: {} }; vm.runInNewContext(fs.readFileSync(require.resolve('../src/web/public/js/shared/region-content'), 'utf8'), sandbox); const region = { region_key: 'brand', region_type: 'text', read_only: true, default_content: { value: 'Shared brand' } }; const saved = { brand: { value: 'Stale brand' } }; assert.equal(JSON.stringify(sandbox.window.pulseRegionContent.resolveRegionContent(region, saved)), JSON.stringify(resolveRegionContent(region, saved))); assert.ok(getRegionEditorScripts().includes('js/shared/region-content.js')); const editorSource = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-form.js'), 'utf8'); assert.match(editorSource, /window\.pulseRegionContent\.resolveRegionContent\(region, existingContent\)/); }); test('thumbnail regions use shared region renderers and their source context', () => { const calls = []; const registry = { get(type) { return { renderPreview(...args) { calls.push({ type, args }); return '' + type + ''; } }; } }; const apiSources = [{ id: 8, responseJson: [{ name: 'Ada' }] }]; const payload = { regions: [ { id: 1, region_key: 'clock', region_type: 'time-date', x: 20, y: 12, width: 200, height: 80, z_index: 4, content: { value: '{{hh}}:{{mm}}', timezone: 'UTC' } }, { id: 2, region_key: 'donations', region_type: 'api', x: 220, y: 12, width: 300, height: 80, z_index: 2, content: { source_id: 8, value: '{{name}}' } } ], apiSources: apiSources, rssFeeds: [], weatherLocations: [], timetableGroups: [] }; const html = render(payload, registry); assert.match(html, /data-region-key="clock"[^>]*>time-date<\/span>/); assert.match(html, /data-region-key="donations"[^>]*>api<\/span>/); assert.equal(calls[0].args[3], 1); assert.equal(calls[1].args[2].sources, apiSources); }); test('thumbnail time/date uses the editor renderer to expand current-time tokens', () => { const registry = { get(type) { return type === 'time-date' ? loadEditorTimeDateModule() : null; } }; const html = render({ regions: [{ id: 4, region_key: 'clock', region_type: 'time-date', x: 0, y: 0, width: 320, height: 80, content: { value: '{{hh}}:{{mm}}', timezone: 'UTC' } }] }, registry); assert.match(html, /
]*>\d{2}:\d{2}<\/div>/); });