173 lines
7.6 KiB
JavaScript
173 lines
7.6 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { buildSlidePayload } = require('../src/data/slides');
|
|
const { normalizeDisplayMode } = require('../src/data/timetables');
|
|
|
|
test('thumbnail timetable regions resolve entries instead of displaying placeholders', () => {
|
|
const { buildThumbnailPreviewPayload } = require('../src/web/lib/media/slide-thumbnails');
|
|
const slide = {
|
|
template: { regions: [{ region_key: 'agenda', region_type: 'timetable', width: 600, height: 300 }] },
|
|
content: { agenda: { value: '<p>{{index}}: {{title}} at {{start.time(HH:mm,"Europe/Amsterdam")}}</p>', timetable_group_id: 7, display_mode: 'all', max_items: 1 } }
|
|
};
|
|
const options = { timetableGroups: [{ id: 7, timezone: 'Europe/Amsterdam', entries: [
|
|
{ id: 2, title: 'Later', start_datetime: '2099-01-01T12:00:00Z' },
|
|
{ id: 1, title: 'Opening & welcome', start_datetime: '2099-01-01T10:00:00Z' }
|
|
] }] };
|
|
const payload = buildThumbnailPreviewPayload(slide, options);
|
|
assert.match(payload.html, /1: Opening & welcome at 11:00/);
|
|
assert.doesNotMatch(payload.html, /\{\{|Later/);
|
|
slide.content.agenda.date_filter = 'after';
|
|
slide.content.agenda.date_from = '2099-01-01T11:00:00Z';
|
|
const filtered = buildThumbnailPreviewPayload(slide, options);
|
|
assert.match(filtered.html, /1: Later at 13:00/);
|
|
assert.doesNotMatch(filtered.html, /Opening|\{\{/);
|
|
assert.doesNotMatch(buildThumbnailPreviewPayload(slide, {}).html, /\{\{|Opening|Later/);
|
|
});
|
|
|
|
test('timetable display mode normalization accepts all and preserves existing modes', () => {
|
|
for (const mode of ['upcoming', 'current', 'both', 'all']) {
|
|
assert.equal(normalizeDisplayMode(mode.toUpperCase()), mode);
|
|
}
|
|
assert.equal(normalizeDisplayMode('invalid'), 'upcoming');
|
|
});
|
|
|
|
test('buildSlidePayload normalizes timetable region fields', async () => {
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM c_templates st')) {
|
|
return [[{ id: 9, name: 'Template 9', canvas_size_id: 1, canvas_size_width: 1920, canvas_size_height: 1080 }]];
|
|
}
|
|
|
|
if (sql.includes('FROM c_template_regions')) {
|
|
return [[{ id: 47, template_id: 9, region_key: 'timetable', region_type: 'timetable', label: 'Timetable' }]];
|
|
}
|
|
|
|
return [[]];
|
|
}
|
|
};
|
|
|
|
const payload = await buildSlidePayload(pool, {
|
|
body: {
|
|
title: 'Timetable slide',
|
|
template_id: '9',
|
|
region_text_47: '<p>{{title}}</p>',
|
|
region_timetable_group_id_47: '1',
|
|
region_timetable_display_mode_47: 'current',
|
|
region_timetable_max_items_47: '7',
|
|
region_date_filter_47: 'between',
|
|
region_date_from_47: '2099-08-20T12:00:00+02:00',
|
|
region_date_to_47: ''
|
|
},
|
|
files: []
|
|
}, null);
|
|
|
|
const content = JSON.parse(payload.contentJson);
|
|
assert.equal(content.timetable.type, 'timetable');
|
|
assert.equal(content.timetable.text, '<p>{{title}}</p>');
|
|
assert.equal(content.timetable.value, '<p>{{title}}</p>');
|
|
assert.equal(content.timetable.timetable_group_id, '1');
|
|
assert.equal(content.timetable.display_mode, 'current');
|
|
assert.equal(content.timetable.max_items, '7');
|
|
assert.equal(content.timetable.date_filter, 'between');
|
|
assert.equal(content.timetable.date_from, '2099-08-20T10:00:00.000Z');
|
|
assert.equal(content.timetable.date_to, '');
|
|
assert.equal(Object.prototype.hasOwnProperty.call(content.timetable, 'timetable_display_mode'), false);
|
|
assert.equal(Object.prototype.hasOwnProperty.call(content.timetable, 'timetable_max_items'), false);
|
|
});
|
|
|
|
test('timetable date settings persist, clear, default to all, and reject invalid ranges', async () => {
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM c_templates st')) {
|
|
return [[{ id: 9, name: 'Template 9' }]];
|
|
}
|
|
if (sql.includes('FROM c_template_regions')) {
|
|
return [[{ id: 47, region_key: 'timetable', region_type: 'timetable' }]];
|
|
}
|
|
return [[]];
|
|
}
|
|
};
|
|
const existing = { content_json: JSON.stringify({ timetable: { type: 'timetable', date_filter: 'between', date_from: '2099-08-20T10:00:00.000Z', date_to: '2099-08-20T11:00:00.000Z' } }) };
|
|
async function save(fields, slide) {
|
|
const payload = await buildSlidePayload(pool, { body: { title: 'Timetable', template_id: '9', ...fields }, files: [] }, slide);
|
|
return JSON.parse(payload.contentJson).timetable;
|
|
}
|
|
assert.equal((await save({})).date_filter, 'all');
|
|
assert.equal((await save({ region_timetable_display_mode_47: 'all' })).display_mode, 'all');
|
|
assert.equal((await save({}, existing)).date_from, '2099-08-20T10:00:00.000Z');
|
|
assert.equal((await save({ region_date_from_47: '' }, existing)).date_from, '');
|
|
const all = await save({ region_date_filter_47: 'all' }, existing);
|
|
assert.equal(all.date_filter, 'all');
|
|
assert.equal(all.date_to, '2099-08-20T11:00:00.000Z');
|
|
for (const mode of ['before', 'after']) {
|
|
const saved = await save({ region_date_filter_47: mode }, existing);
|
|
assert.equal(saved.date_filter, mode);
|
|
assert.equal(saved.date_from, '2099-08-20T10:00:00.000Z');
|
|
assert.equal(saved.date_to, '2099-08-20T11:00:00.000Z');
|
|
}
|
|
assert.equal((await save({ region_date_filter_47: 'after', region_date_to_47: '2099-08-19T00:00:00Z' }, existing)).date_filter, 'after');
|
|
assert.equal((await save({ region_date_filter_47: 'before', region_date_from_47: '2099-08-21T00:00:00Z' }, existing)).date_filter, 'before');
|
|
await assert.rejects(save({ region_date_filter_47: 'before', region_date_to_47: 'invalid' }), { statusCode: 400 });
|
|
await assert.rejects(save({ region_date_filter_47: 'after', region_date_from_47: 'invalid' }), { statusCode: 400 });
|
|
await assert.rejects(save({ region_date_filter_47: 'between', region_date_from_47: 'invalid' }), { statusCode: 400 });
|
|
await assert.rejects(save({ region_date_from_47: '2099-08-21T00:00:00Z' }, existing), { statusCode: 400 });
|
|
});
|
|
|
|
test('buildSlidePayload preserves safe image markup in text regions', async () => {
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM c_templates st')) {
|
|
return [[{ id: 9, name: 'Template 9', canvas_size_id: 1, canvas_size_width: 1920, canvas_size_height: 1080 }]];
|
|
}
|
|
|
|
if (sql.includes('FROM c_template_regions')) {
|
|
return [[{ id: 47, template_id: 9, region_key: 'body', region_type: 'text', label: 'Body' }]];
|
|
}
|
|
|
|
return [[]];
|
|
}
|
|
};
|
|
|
|
const payload = await buildSlidePayload(pool, {
|
|
body: {
|
|
title: 'Text slide',
|
|
template_id: '9',
|
|
region_text_47: '<p><img src="/media/uploads/photo.png" alt="Photo" onerror="alert(1)"></p>'
|
|
},
|
|
files: []
|
|
}, null);
|
|
|
|
const content = JSON.parse(payload.contentJson);
|
|
assert.equal(content.body.type, 'text');
|
|
assert.equal(content.body.value, '<p><img src="/media/uploads/photo.png" alt="Photo"></p>');
|
|
});
|
|
|
|
test('buildSlidePayload preserves safe color spans in text regions', async () => {
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM c_templates st')) {
|
|
return [[{ id: 9, name: 'Template 9', canvas_size_id: 1, canvas_size_width: 1920, canvas_size_height: 1080 }]];
|
|
}
|
|
|
|
if (sql.includes('FROM c_template_regions')) {
|
|
return [[{ id: 47, template_id: 9, region_key: 'body', region_type: 'text', label: 'Body' }]];
|
|
}
|
|
|
|
return [[]];
|
|
}
|
|
};
|
|
|
|
const payload = await buildSlidePayload(pool, {
|
|
body: {
|
|
title: 'Text slide',
|
|
template_id: '9',
|
|
region_text_47: '<p><span style="color:#ff0000" class="text-emphasis">Hello</span></p>'
|
|
},
|
|
files: []
|
|
}, null);
|
|
|
|
const content = JSON.parse(payload.contentJson);
|
|
assert.equal(content.body.type, 'text');
|
|
assert.equal(content.body.value, '<p><span style="color:#ff0000" class="text-emphasis">Hello</span></p>');
|
|
}); |