const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); require('../src/common'); const { buildTemplatePayload, extractTemplateRegions, normalizeBackgroundGradient } = require('../src/data/templates'); const renderTemplateAddPage = require('../src/web/routes/signage/templates/add'); const renderTemplateEditPage = require('../src/web/routes/signage/templates/edit'); const { buildDuplicateTemplateName, buildDuplicateTemplate } = require('../src/web/routes/signage/templates/duplicate'); const { buildThumbnailPreviewPayload } = require('../src/web/lib/media/slide-thumbnails'); function readCssDeclarations(css, selector) { const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const match = css.match(new RegExp(escapedSelector + '\\s*\\{([^}]*)\\}')); assert.ok(match, `CSS rule not found: ${selector}`); return Object.fromEntries(match[1].split(';').map((declaration) => declaration.trim().split(':')).filter(([property, value]) => property && value).map(([property, ...value]) => [property.trim(), value.join(':').trim()])); } test('extractTemplateRegions normalizes JSON regions and filters invalid rows', () => { const regions = extractTemplateRegions({ regions_json: JSON.stringify([ { region_name: 'Hero', region_type: 'TEXT', lock_ratio: '16 : 9', default_content: '{"type":"text","value":"Template headline"}', read_only: 'true', animation_json: '{"intro":{"preset":"fadeIn"},"out":{"preset":"bounceOut"}}', x: '10', y: '20', width: '320', height: '180', z_index: '2' }, { region_name: ' ', region_type: 'image' } ]) }); assert.deepEqual(regions, [{ region_key: 'Hero', region_type: 'TEXT', label: 'Hero', lock_ratio: '16:9', default_content: { type: 'text', value: 'Template headline' }, read_only: true, animation_json: { intro: { preset: 'fadeIn' }, outro: { preset: 'bounceOut' }, loop: { preset: 'none' } }, x: 10, y: 20, width: 320, height: 180, z_index: 2 }]); }); test('extractTemplateRegions keeps JSON and form z-index values at positive integers', () => { const values = [-5, 0, '', undefined, 'invalid', 'Infinity', 1, '4', 2.6]; const expected = [1, 1, 1, 1, 1, 1, 1, 4, 3]; const jsonRegions = extractTemplateRegions({ regions_json: JSON.stringify(values.map((value, index) => ({ region_name: `Region ${index}`, z_index: value }))) }); const formRegions = extractTemplateRegions({ 'region_key[]': values.map((value, index) => `Region ${index}`), 'region_z[]': values }); assert.deepEqual(jsonRegions.map((region) => region.z_index), expected); assert.deepEqual(formRegions.map((region) => region.z_index), expected); }); test('buildTemplatePayload resolves canvas size and rejects duplicate region names', async () => { const pool = { async query(sql, params) { if (sql.includes('FROM c_canvas_sizes WHERE id = ?')) { assert.deepEqual(params, [4]); return [[{ id: 4, name: 'HD', width: 1280, height: 720 }]]; } throw new Error(`unexpected query: ${sql}`); } }; const payload = await buildTemplatePayload(pool, { body: { name: ' Main Template ', canvas_size_id: '4', background_color: 'not-a-color', regions_json: JSON.stringify([ { region_name: 'Header', region_type: 'text', lock_ratio: '4:3' } ]) }, files: [] }, null); assert.deepEqual(payload, { name: 'Main Template', canvasSizeId: 4, canvasSizeWidth: 1280, canvasSizeHeight: 720, backgroundImagePath: null, backgroundColor: '#111111', backgroundGradient: null, regions: [{ region_key: 'Header', region_type: 'text', label: 'Header', lock_ratio: '4:3', default_content: {}, read_only: false, animation_json: { intro: { preset: 'none' }, outro: { preset: 'none' }, loop: { preset: 'none' } }, x: 0, y: 0, width: 100, height: 100, z_index: 1 }] }); await assert.rejects( () => buildTemplatePayload(pool, { body: { name: 'Dupes', regions_json: JSON.stringify([ { region_name: 'One' }, { region_name: 'one' } ]) }, files: [] }, null), (error) => error && error.statusCode === 400 && error.message === 'Region names must be unique on this template.' ); }); test('disabled canvas sizes cannot be assigned to new templates but existing assignments can be retained', async () => { const pool = { async query() { return [[{ id: 4, name: 'Legacy', width: 1280, height: 720, enabled: 0 }]]; } }; const req = { body: { name: 'Legacy template', canvas_size_id: '4', regions_json: JSON.stringify([{ region_name: 'Body', region_type: 'text' }]) }, files: [] }; await assert.rejects( () => buildTemplatePayload(pool, req, null), (error) => error && error.statusCode === 400 && error.message === 'Canvas size is disabled.' ); const payload = await buildTemplatePayload(pool, req, { canvas_size_id: 4 }); assert.equal(payload.canvasSizeId, 4); }); test('buildDuplicateTemplate copies template fields and strips row identity', () => { const duplicate = buildDuplicateTemplate({ id: 9, name: 'Main Template', canvas_size_id: 4, canvas_size_width: 1280, canvas_size_height: 720, background_color: '#112233', background_image_path: '/media/uploads/bg.png', regions: [{ id: 22, template_id: 9, region_key: 'Hero', label: 'Hero', region_type: 'text' }] }, buildDuplicateTemplateName('Main Template')); assert.equal(duplicate.id, null); assert.equal(duplicate.name, 'Copy of Main Template'); assert.equal(duplicate.canvas_size_id, 4); assert.equal(duplicate.background_color, '#112233'); assert.equal(duplicate.background_image_path, '/media/uploads/bg.png'); assert.deepEqual(duplicate.region_usage, []); assert.equal(duplicate.regions.length, 1); assert.equal(duplicate.regions[0].id, undefined); assert.equal(duplicate.regions[0].template_id, undefined); assert.equal(duplicate.regions[0].region_key, 'Hero'); }); test('template create page renders the background remove control', () => { const html = renderTemplateAddPage(null, '', [ { id: 1, name: 'HD', width: 1280, height: 720 } ], { permissionKeys: ['templates.create'] }); assert.match(html, /id="remove-background-image"/); assert.match(html, /name="remove_background_image"/); assert.match(html, /data-media-picker-hidden="existing-background-image-path"/); assert.match(html, /data-media-picker-file-input="background-image"/); assert.match(html, /id="background-image"/); assert.match(html, /data-region-default-content/); assert.match(html, /data-region-default-content-button/); assert.match(html, /data-region-read-only/); assert.match(html, /form-check form-switch d-inline-flex align-items-center gap-2 mb-0 ps-0 text-nowrap flex-shrink-0/); assert.match(html, /form-check-input m-0 flex-shrink-0/); assert.match(html, /Template-only<\/span>/); assert.match(html, /title="Hidden in the slide editor\. Edit this content on the template\."/); assert.match(html, /title="Edit template default content">