Release v2.15.5
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-17 16:55:37 +01:00
1 parent 0e77998e92
commit 2fd059d35a
18 files changed
+385 -30

No files matched your search

+111
View File
@@ -1,6 +1,7 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
require('../src/common');
@@ -332,6 +333,116 @@ test('template designer warns when invalid region names block save', () => {
assert.ok(script.includes('regionNameValidationToastShown = false;'));
});
function createRegionCardHarness(lockRatio = '') {
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
const fields = {};
const cardListeners = {};
const initialValues = { name: 'Region', type: 'text', key: 'Region', label: 'Region', x: '80', y: '80', z: '1', width: '300', height: '120', lock_ratio: lockRatio, animation_json: '{}' };
for (const [field, value] of Object.entries(initialValues)) {
fields[`[name="region_${field}[]"]`] = {
value,
listeners: {},
addEventListener(type, listener) { this.listeners[type] = listener; }
};
}
fields['[data-region-remove-button]'] = { addEventListener() {} };
const card = {
dataset: {},
querySelector(selector) { return fields[selector] || null; },
querySelectorAll() { return []; },
addEventListener(type, listener) { cardListeners[type] = listener; }
};
const previews = [];
let dirtyCount = 0;
const context = vm.createContext({
window: {},
regionCardTemplate: { content: { firstElementChild: { cloneNode: () => card } } },
populateRegionCard() {},
setRegionRemovalState() {},
getRegionName: () => 'Region',
updateRegionLockBadge() {},
markTemplateFormDirty() { dirtyCount += 1; },
requestOverlayRender() { previews.push(context.readCard(card)); },
getCanvasSize: () => ({ width: 1920, height: 1080 }),
normalizeLockRatio: (value) => value
});
vm.runInContext(fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer-utils.js'), 'utf8'), context);
Object.assign(context, {
readCard: context.window.templateDesignerUtils.readCard,
writeCard: context.window.templateDesignerUtils.writeCard,
parseLockRatio: context.window.templateDesignerUtils.parseLockRatio,
clamp: context.window.templateDesignerUtils.clamp
});
for (const name of ['fitRegionToCanvas', 'normalizeRegionCard', 'syncLockedDimensions', 'makeRegionCard']) {
const match = script.match(new RegExp(' function ' + name + '\\([^]*?\\n }'));
assert.ok(match, `Missing designer function: ${name}`);
vm.runInContext(match[0], context);
}
context.makeRegionCard({ region_key: 'Region' });
return {
previews,
get dirtyCount() { return dirtyCount; },
edit(field, value, type = 'input') {
const input = fields[`[name="region_${field}[]"]`];
input.value = value;
input.listeners[type]();
},
value(field) { return Number(fields[`[name="region_${field}[]"]`].value); },
rawValue(field) { return fields[`[name="region_${field}[]"]`].value; },
keydown: (event) => cardListeners.keydown(event)
};
}
test('region coordinate and stacking fields immediately refresh the preview and mark it dirty', () => {
const harness = createRegionCardHarness();
harness.edit('x', '140');
harness.edit('y', '220');
harness.edit('z', '3');
assert.equal(harness.previews.length, 3);
assert.equal(harness.previews.at(-1).x, 140);
assert.equal(harness.previews.at(-1).y, 220);
assert.equal(harness.previews.at(-1).z_index, 3);
assert.equal(harness.dirtyCount, 3);
});
test('region dimensions allow clearing and typing below the minimum until committed', () => {
for (const field of ['width', 'height']) {
const harness = createRegionCardHarness();
for (const value of ['', '1', '10', '100']) {
harness.edit(field, value);
assert.equal(harness.rawValue(field), value);
}
harness.edit(field, '1', 'change');
assert.equal(harness.value(field), 12);
harness.edit(field, '9999', 'change');
assert.equal(harness.value(field), field === 'width' ? 1920 : 1080);
}
});
test('locked region dimensions update their counterpart without replacing partial input', () => {
const harness = createRegionCardHarness('2:1');
harness.edit('width', '');
assert.equal(harness.rawValue('width'), '');
harness.edit('width', '1');
assert.equal(harness.rawValue('width'), '1');
harness.edit('width', '400');
assert.equal(harness.value('height'), 200);
harness.edit('height', '150');
assert.equal(harness.value('width'), 300);
harness.edit('height', '150', 'change');
assert.equal(harness.value('height'), 150);
assert.equal(harness.value('width'), 300);
});
test('Enter in region inputs prevents implicit saves without blocking buttons or textarea newlines', () => {
const harness = createRegionCardHarness();
for (const [key, tagName, expected] of [['Enter', 'INPUT', true], ['Tab', 'INPUT', false], ['Enter', 'BUTTON', false], ['Enter', 'TEXTAREA', false]]) {
let prevented = false;
harness.keydown({ key, target: { tagName }, preventDefault() { prevented = true; } });
assert.equal(prevented, expected);
}
});
test('normalizeBackgroundGradient accepts linear colors and clamps the angle', () => {
assert.equal(normalizeBackgroundGradient(JSON.stringify({
type: 'radial',