Release v2.5.6
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
require('../src/common');
|
||||
|
||||
const {
|
||||
buildTemplatePayload,
|
||||
extractTemplateRegions
|
||||
} = 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');
|
||||
|
||||
test('extractTemplateRegions normalizes JSON regions and filters invalid rows', () => {
|
||||
const regions = extractTemplateRegions({
|
||||
@@ -106,4 +112,121 @@ test('buildTemplatePayload resolves canvas size and rejects duplicate region nam
|
||||
}, null),
|
||||
(error) => error && error.statusCode === 400 && error.message === 'Region names must be unique on this template.'
|
||||
);
|
||||
});
|
||||
|
||||
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, /placeholder="type_1"/);
|
||||
assert.match(html, /Save and Close/);
|
||||
assert.match(html, /Save and New/);
|
||||
assert.match(html, /data-confirm-unsaved="You've made changes\. Are you sure you want to leave this page\?"/);
|
||||
});
|
||||
|
||||
test('template edit page confirms deletion', () => {
|
||||
const html = renderTemplateEditPage({
|
||||
id: 9,
|
||||
name: 'Main Template',
|
||||
canvas_size_id: 1,
|
||||
canvas_size_width: 1280,
|
||||
canvas_size_height: 720,
|
||||
background_image_path: '',
|
||||
background_color: '#111111',
|
||||
inUse: false,
|
||||
regions: [],
|
||||
region_usage: []
|
||||
}, {
|
||||
canvasSizes: [{ id: 1, name: 'HD', width: 1280, height: 720 }]
|
||||
}, '', { permissionKeys: ['templates.delete'] });
|
||||
|
||||
assert.match(html, /data-delete-action-url="\/templates\/9\/delete"/);
|
||||
assert.match(html, /data-confirm-message="Delete this template\?"/);
|
||||
assert.match(html, /data-confirm-unsaved="You've made changes\. Are you sure you want to leave this page\?"/);
|
||||
assert.doesNotMatch(html, /id="delete-template-form"/);
|
||||
});
|
||||
|
||||
test('template list template includes duplicate action', () => {
|
||||
const template = fs.readFileSync(require.resolve('../src/web/views/signage/templates/list.hbs'), 'utf8');
|
||||
|
||||
assert.match(template, /\/templates\/\{\{id\}\}\/duplicate/);
|
||||
assert.match(template, />Dupe<\/a>/);
|
||||
});
|
||||
|
||||
test('template animation controls mark the form dirty', () => {
|
||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||
|
||||
assert.match(script, /animationPresetInputs[\s\S]*markTemplateFormDirty\(\);/);
|
||||
assert.ok(script.includes("animationAdvancedModal.addEventListener('input'"));
|
||||
assert.ok(script.includes('clampAnimationModalField(event.target);'));
|
||||
assert.ok(script.includes("animationAdvancedModal.addEventListener('change'"));
|
||||
assert.ok(script.includes("animationModalApplyButton.addEventListener('click', function () {"));
|
||||
assert.ok(script.includes("saveAnimationModal();"));
|
||||
assert.ok(script.includes("animationModalResetButton.addEventListener('click', function () {"));
|
||||
assert.ok(script.includes("writeAnimationConfig(activeAnimationCard, normalizeAnimationConfig('{}'));"));
|
||||
assert.ok(script.includes("markTemplateFormDirty();"));
|
||||
});
|
||||
|
||||
test('template animation durations use percent in the advanced modal', () => {
|
||||
const modal = fs.readFileSync(require.resolve('../src/web/views/signage/templates/animation-advanced-modal.hbs'), 'utf8');
|
||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||
const utilsScript = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer-utils.js'), 'utf8');
|
||||
|
||||
assert.match(modal, /Duration %/);
|
||||
assert.match(modal, /data-animation-modal-field="duration_ms"/);
|
||||
assert.match(modal, /max="200"/);
|
||||
assert.match(modal, /data-animation-modal-field="iterations"/);
|
||||
assert.match(modal, /max="999"/);
|
||||
assert.ok(script.includes('function getAnimationDurationPercent(durationMs)'));
|
||||
assert.ok(script.includes('function getAnimationDurationMs(durationPercent)'));
|
||||
assert.ok(script.includes('durationInput.value = String(getAnimationDurationPercent(step.duration_ms));'));
|
||||
assert.ok(script.includes("nextStep.duration_ms = getAnimationDurationMs(durationInput ? durationInput.value : '');"));
|
||||
assert.ok(script.includes('MAX_ANIMATION_REPEAT_COUNT = 999'));
|
||||
assert.match(script, /Math\.min\(MAX_ANIMATION_REPEAT_COUNT, Math\.max\(1, readNumberField\(iterationsInput \? iterationsInput\.value : ''\) \|\| 1\)\)/);
|
||||
assert.match(utilsScript, /MAX_ANIMATION_REPEAT_COUNT = 999/);
|
||||
assert.match(utilsScript, /clamp\(Math\.round\(Number\(value\.iterations\)\), 1, MAX_ANIMATION_REPEAT_COUNT\)/);
|
||||
});
|
||||
|
||||
test('template designer generates default region names per type', () => {
|
||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||
|
||||
assert.ok(script.includes('function getNextDefaultRegionName(regionType)'));
|
||||
assert.ok(script.includes('var name = getNextDefaultRegionName(type);'));
|
||||
assert.ok(script.includes("var name = getNextDefaultRegionName('text');"));
|
||||
assert.ok(script.includes('Math.max(count, suffix)'));
|
||||
});
|
||||
|
||||
test('template designer warns when invalid region names block save', () => {
|
||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||
|
||||
assert.ok(script.includes("templateForm.addEventListener('invalid'"));
|
||||
assert.ok(script.includes('function notifyRegionNameValidationError(message)'));
|
||||
assert.ok(script.includes('regionNameValidationToastShown = false;'));
|
||||
});
|
||||
Reference in New Issue
Block a user