Release v2.18.0: template default content
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 2m2s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s

This commit is contained in:
lzstealth committed 2026-10-24 18:03:38 +01:00
1 parent 48e8f3eb4b
commit 24f1603b43
54 files changed
+1421 -151

No files matched your search

+72 -3
View File
@@ -330,7 +330,7 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
addEventListener(type, handler) { this.handlers[type] = handler; },
querySelectorAll() { return []; }, replaceChildren() {}, appendChild() {}, focus() {},
setAttribute() {}, removeAttribute() {},
style: { setProperty() {} }, classList: { add() {}, remove() {} } };
style: { setProperty() {}, removeProperty() {} }, classList: { add() {}, remove() {}, toggle() {} } };
}
const ids = Object.fromEntries([
'slide-animation-modal', 'adjust-slide-animations', 'slide-animation-region',
@@ -344,7 +344,17 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
const advanced = ids['animation-advanced-modal'];
const advancedApply = element();
const advancedReset = element();
const animationPreviewButtons = ['intro', 'outro', 'loop'].map(mode => Object.assign(element(), { dataset: { slideAnimationPreview: mode }, querySelector: () => element() }));
const readonlyPreviewRegion = element();
readonlyPreviewRegion.className = 'slide-preview-region';
readonlyPreviewRegion.dataset.regionKey = 'fixed_brand';
readonlyPreviewRegion.classList = {
add(...classNames) { readonlyPreviewRegion.className += ' ' + classNames.join(' '); },
remove(...classNames) { readonlyPreviewRegion.className = readonlyPreviewRegion.className.split(/\s+/).filter(name => !classNames.includes(name)).join(' '); }
};
advanced.querySelectorAll = () => fields;
ids['slide-animation-modal'].querySelectorAll = selector => selector === '[data-slide-animation-preview]' ? animationPreviewButtons : [];
ids['slide-animation-preview'].querySelectorAll = () => [readonlyPreviewRegion];
advanced.querySelector = selector => selector === '[data-animation-modal-apply]' ? advancedApply
: selector === '[data-animation-modal-reset]' ? advancedReset : element();
const hidden = { value: '{}' };
@@ -363,7 +373,7 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
};
const modalInstances = new Map();
const scrollBarCalls = [];
const context = { window: { innerWidth: 1200, pulseModal: {
const context = { window: { innerWidth: 1200, setTimeout() { return 1; }, clearTimeout() {}, pulseModal: {
show(modal) { this.getOrCreate(modal)._scrollBar.hide(); modal.shown = true; },
hide(modal) { this.getOrCreate(modal)._scrollBar.reset(); modal.shown = false; modal.handlers['hidden.bs.modal'](); },
getOrCreate(modal) {
@@ -387,12 +397,19 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
vm.runInNewContext(source.replace('export function', 'function'), context);
let applied;
context.createSlideAnimationEditor({ stage: { parentNode: { insertBefore() {} }, nextSibling: null },
getTemplate() { return { regions: [{ region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } }] }; },
getTemplate() { return { regions: [
{ region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } },
{ region_key: 'fixed_brand', read_only: true, animation_json: { loop: { preset: 'pulse', duration_ms: 200, iterations: 1 } } }
] }; },
getAnimationOverrides() { return {}; }, requestPreviewRender() {}, markEdited() {},
setAnimationOverrides(value) { applied = value; }
});
ids['slide-animation-region'].value = 'hero';
ids['adjust-slide-animations'].handlers.click();
assert.equal(animationPreviewButtons[2].disabled, false);
animationPreviewButtons[2].handlers.click();
assert.match(readonlyPreviewRegion.className, /animate__pulse/);
animationPreviewButtons[2].handlers.click();
const resetRegion = ids['slide-animation-reset-region'];
const resetAll = ids['slide-animation-reset-all'];
assert.equal(resetRegion.disabled, true);
@@ -438,6 +455,7 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
assert.equal(JSON.parse(applied.hero).intro.preset, 'none');
assert.equal(JSON.parse(applied.hero).outro.preset, 'fadeOut');
assert.equal(JSON.parse(applied.hero).outro.duration_ms, 1500);
assert.equal(applied.fixed_brand, undefined);
assert.deepEqual(scrollBarCalls, [
['hide', ids['slide-animation-modal']],
['reset', ids['slide-animation-modal']]
@@ -618,4 +636,55 @@ test('shared animation route is protected and updates template animations withou
assert.ok(calls.some(call => call[0] === 'commit'));
assert.ok(calls.some(call => call[0] === 'notify'));
assert.ok(calls.some(call => call[0] === 'thumbnail'));
});
test('editable regions inherit template defaults until a slide supplies an override', async function () {
const pool = {
async query(sql) {
if (sql.includes('FROM c_templates')) return [[{ id: 7, canvas_size_width: 1920, canvas_size_height: 1080 }]];
if (sql.includes('FROM c_template_regions')) return [[{
id: 11, template_id: 7, region_key: 'hero', region_type: 'text', default_content: { type: 'text', value: 'Template default' }, read_only: 0,
x: 10, y: 20, width: 300, height: 100
}]];
throw new Error('Unexpected query: ' + sql);
}
};
const initial = await buildSlidePayload(pool, { body: { title: 'Example', template_id: '7' } }, null);
assert.equal(JSON.parse(initial.contentJson).hero.value, 'Template default');
const overridden = await buildSlidePayload(pool, { body: { title: 'Example', template_id: '7', region_text_11: 'Slide override' } }, null);
assert.equal(JSON.parse(overridden.contentJson).hero.value, 'Slide override');
});
test('read-only regions are excluded from slide content and layout overrides', async function () {
const pool = {
async query(sql) {
if (sql.includes('FROM c_templates')) return [[{ id: 7, canvas_size_width: 1920, canvas_size_height: 1080 }]];
if (sql.includes('FROM c_template_regions')) return [[{
id: 11, template_id: 7, region_key: 'brand', region_type: 'text', default_content: { type: 'text', value: 'Fixed brand' }, read_only: 1,
x: 10, y: 20, width: 300, height: 100
}]];
throw new Error('Unexpected query: ' + sql);
}
};
const result = await buildSlidePayload(pool, { body: {
title: 'Example', template_id: '7', region_text_11: 'Client override',
region_layouts_json: JSON.stringify({ brand: {
x: 100, y: 100, width: 200, height: 80,
animation_json: { intro: { preset: 'bounceIn' } }
} })
} }, null);
assert.deepEqual(JSON.parse(result.contentJson), {});
});
test('thumbnail markup uses template defaults for read-only regions', function () {
const html = buildThumbnailPreviewMarkup({
template: {
regions: [{ region_key: 'brand', region_type: 'image', read_only: 1, default_content: { value: '/media/brand.png' }, x: 0, y: 0, width: 100, height: 100 }],
canvas_size_width: 100,
canvas_size_height: 100
},
content: { brand: { value: '/media/old-brand.png' } }
}, 'http://localhost');
assert.match(html, /\/media\/brand\.png/);
assert.doesNotMatch(html, /old-brand/);
});