220 lines
9.9 KiB
JavaScript
220 lines
9.9 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
require('../src/common');
|
|
|
|
const { buildPlaylistFormViewModel } = require('../src/web/routes/signage/playlists/form-view-model');
|
|
|
|
test('playlist add and edit forms share the saved-image preview and accessible thumbnail buttons', () => {
|
|
const { renderView } = require('../src/web/view');
|
|
for (const isEdit of [false, true]) {
|
|
const html = renderView('playlists/form', {
|
|
isEdit,
|
|
playlist: { id: 3, name: 'Preview test' },
|
|
selectableSlides: [],
|
|
playlistSlideTemplate: { scheduleRules: [] },
|
|
playlistSlides: [{ slide_id: 124, title: 'Landscape', thumbnail_path: '/media/thumbnails/slides/slide-124.png', canvas_width: 1920, canvas_height: 1080, scheduleRules: [] }]
|
|
});
|
|
assert.equal((html.match(/id="slide-preview-modal"/g) || []).length, 1);
|
|
assert.match(html, /<button[^>]*data-slide-preview-url="\/media\/thumbnails\/slides\/slide-124.png"[^>]*data-canvas-width="1920"[^>]*aria-label="Preview slide: Landscape"/);
|
|
assert.match(html, /data-slide-preview-image/);
|
|
assert.doesNotMatch(html, /data-slide-preview-frame/);
|
|
}
|
|
});
|
|
|
|
test('new playlist rows receive preview URLs, titles and canvas dimensions', () => {
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const source = fs.readFileSync(require.resolve('../src/web/public/js/playlists/playlist-schedule'), 'utf8');
|
|
const start = source.indexOf(' function populatePlaylistSlideRow(');
|
|
const end = source.indexOf(' function createRow(', start);
|
|
const sandbox = { escapeHtmlAttribute: value => value };
|
|
vm.runInNewContext(source.slice(start, end), sandbox);
|
|
const attrs = {};
|
|
const thumb = { setAttribute(name, value) { attrs[name] = value; }, style: { setProperty() {} } };
|
|
const row = { setAttribute() {}, querySelector(selector) { return selector === '[data-playlist-slide-thumb]' ? thumb : null; } };
|
|
sandbox.populatePlaylistSlideRow(row, { slide_id: 125, title: 'Portrait', thumbnail_path: '/media/thumbnails/slides/slide-125.png', canvas_width: 1080, canvas_height: 1920, summary: 'Always visible' }, 'new-1');
|
|
assert.equal(attrs['data-slide-preview-url'], '/media/thumbnails/slides/slide-125.png');
|
|
assert.equal(attrs['data-slide-preview-title'], 'Portrait');
|
|
assert.equal(attrs['data-canvas-width'], '1080');
|
|
assert.equal(attrs['data-canvas-height'], '1920');
|
|
assert.equal(attrs['aria-label'], 'Preview slide: Portrait');
|
|
});
|
|
|
|
test('playlist schedule summaries show up to two short rules on separate lines and count longer summaries', () => {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const { formatScheduleRulesSummary } = require('../src/web/lib/playlist-schedule');
|
|
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/playlists/playlist-schedule.js'), 'utf8');
|
|
const context = {};
|
|
vm.runInNewContext(source.slice(0, source.indexOf(' function parseScheduleBoundary('))
|
|
+ 'globalThis.formatSummary = formatScheduleRulesSummary; })();', context);
|
|
|
|
const cases = [
|
|
[[], 'Always visible'],
|
|
[[{ days: [1, 3], start_time: '09:00', end_time: '17:00' }], 'Rule 1: Mon, Wed 09:00-17:00'],
|
|
[[{ start_time: '09:00', end_time: '17:00' }], 'Rule 1: Any day 09:00-17:00'],
|
|
[[{ start_time: '09:00', end_time: '17:00' }, { days: [5] }],
|
|
'Rule 1: Any day 09:00-17:00\nRule 2: Fri'],
|
|
[[{ days: [1] }, { days: [5] }], 'Rule 1: Mon\nRule 2: Fri'],
|
|
[[{ days: [1] }, { days: [3] }, { days: [5] }], '3 rules'],
|
|
[[{ days: [1] }, { days: [2] }, { days: [3] }, { days: [4] }], '4 rules'],
|
|
[[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00', days: [1] }],
|
|
'Rule 1: Dates 2026-10-25 09:00 to 2026-10-26 17:00: Mon'],
|
|
[[{ start_datetime: '2026-10-25T09:00' }], 'Rule 1: From 2026-10-25 09:00'],
|
|
[[{ end_datetime: '2026-10-26T17:00' }], 'Rule 1: Until 2026-10-26 17:00'],
|
|
[[{ start_datetime: new Date(2026, 0, 1, 0, 0), end_datetime: new Date(2026, 9, 30, 20, 0) }],
|
|
'Rule 1: Dates 2026-01-01 00:00 to 2026-10-30 20:00'],
|
|
[[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00',
|
|
days: [1, 2], start_time: '09:00', end_time: '17:00' }],
|
|
'1 rule'],
|
|
[[{ days: [0, 1, 2, 3, 4, 5, 6], start_time: '09:00', end_time: '17:00' },
|
|
{ days: [1, 2, 3, 4, 5], start_time: '18:00', end_time: '19:00' }],
|
|
'Rule 1: Sun, Mon, Tue, Wed, Thu, Fri, Sat 09:00-17:00\nRule 2: Mon, Tue, Wed, Thu, Fri 18:00-19:00'],
|
|
[[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00',
|
|
days: [1, 2, 3] }], 'Rule 1: Dates 2026-10-25 09:00 to 2026-10-26 17:00: Mon, Tue, Wed'],
|
|
[[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00',
|
|
days: [1, 2, 3, 4, 5] }], '1 rule'],
|
|
[[{ days: [1] }, { start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00',
|
|
days: [1, 2, 3, 4, 5] }], '2 rules'],
|
|
[[{ start_datetime: '2026-10-25T09:00', end_datetime: '2026-10-26T17:00',
|
|
days: [1, 2], start_time: '09:00', end_time: '17:00' }, { days: [5] }], '2 rules']
|
|
];
|
|
|
|
for (const [rules, expected] of cases) {
|
|
assert.equal(formatScheduleRulesSummary(rules), expected);
|
|
assert.equal(context.formatSummary(rules), expected);
|
|
assert.equal(formatScheduleRulesSummary(JSON.stringify(rules)), expected);
|
|
assert.equal(context.formatSummary(JSON.stringify(rules)), expected);
|
|
}
|
|
|
|
const css = fs.readFileSync(path.join(__dirname, '../src/web/public/css/theme-custom.css'), 'utf8');
|
|
assert.match(css, /\.playlist-schedule-summary \.chip\s*\{[^}]*display:\s*inline-block;[^}]*white-space:\s*pre-line;[^}]*overflow-wrap:\s*anywhere;/);
|
|
});
|
|
|
|
test('live schedule validator accepts one datetime bound and still rejects reversed ranges', () => {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/playlists/playlist-schedule.js'), 'utf8');
|
|
const start = source.lastIndexOf(' function validateScheduleRuleCard(card)');
|
|
const end = source.indexOf(' function updateLiveRuleValidity(card)', start);
|
|
const fields = {
|
|
days: [],
|
|
startDatetime: { value: '2026-10-25T09:00' },
|
|
endDatetime: { value: '' },
|
|
startTime: { value: '' },
|
|
endTime: { value: '' }
|
|
};
|
|
const context = {
|
|
getRuleFields: () => fields,
|
|
getRuleDayLabels: () => [],
|
|
getDateRangeDayIndices: () => [0, 1, 2, 3, 4, 5, 6],
|
|
clearRuleValidity() {},
|
|
setRuleError() {},
|
|
setDayInputError() {},
|
|
hasScheduleRuleOverlap: () => true
|
|
};
|
|
vm.runInNewContext(source.slice(start, end) + '\nglobalThis.validateRule = validateScheduleRuleCard;', context);
|
|
|
|
assert.equal(context.validateRule({}), true);
|
|
fields.endDatetime.value = '2026-10-24T09:00';
|
|
assert.equal(context.validateRule({}), false);
|
|
});
|
|
|
|
test('playlist rows show mute controls for RTMP slides', () => {
|
|
const model = buildPlaylistFormViewModel(
|
|
{ id: 22, name: 'Playlist 22' },
|
|
{
|
|
playlistSlides: [
|
|
{
|
|
id: 1,
|
|
playlist_id: 22,
|
|
slide_id: 101,
|
|
position: 1,
|
|
title: 'Video slide',
|
|
content_json: '{"videoRegion":{"type":"video","value":"/media/video.mp4"}}',
|
|
duration_seconds: 10,
|
|
use_video_duration: 0,
|
|
disable_audio: 1
|
|
},
|
|
{
|
|
id: 2,
|
|
playlist_id: 22,
|
|
slide_id: 102,
|
|
position: 2,
|
|
title: 'RTMP slide',
|
|
content_json: '{"rtmpRegion":{"type":"rtmp","value":"rtmp://example/live"}}',
|
|
duration_seconds: 10,
|
|
use_video_duration: 0,
|
|
disable_audio: 1
|
|
},
|
|
{
|
|
id: 3,
|
|
playlist_id: 22,
|
|
slide_id: 103,
|
|
position: 3,
|
|
title: 'Image slide',
|
|
content_json: '{"imageRegion":{"type":"image","value":"/media/image.png"}}',
|
|
duration_seconds: 10,
|
|
use_video_duration: 0,
|
|
disable_audio: 1
|
|
}
|
|
],
|
|
slides: []
|
|
},
|
|
'',
|
|
null,
|
|
{ isEdit: true }
|
|
);
|
|
|
|
assert.equal(model.playlistSlides[0].showMuteButton, true);
|
|
assert.equal(model.playlistSlides[1].showMuteButton, true);
|
|
assert.equal(model.playlistSlides[2].showMuteButton, false);
|
|
});
|
|
|
|
test('playlist rows use the longest video region duration', () => {
|
|
const model = buildPlaylistFormViewModel(
|
|
{ id: 23, name: 'Playlist 23' },
|
|
{
|
|
playlistSlides: [
|
|
{
|
|
id: 4,
|
|
playlist_id: 23,
|
|
slide_id: 104,
|
|
position: 1,
|
|
title: 'Multi-video slide',
|
|
content_json: '{"firstVideo":{"type":"video","value":"/media/first.mp4","duration_seconds":12.5},"secondVideo":{"type":"video","value":"/media/second.mp4","duration_seconds":27.75},"textRegion":{"type":"text","value":"Hello"}}',
|
|
duration_seconds: 10,
|
|
use_video_duration: 1,
|
|
disable_audio: 1
|
|
}
|
|
],
|
|
slides: []
|
|
},
|
|
'',
|
|
null,
|
|
{ isEdit: true }
|
|
);
|
|
|
|
assert.equal(model.playlistSlides[0].showVideoDurationButton, true);
|
|
assert.equal(model.playlistSlides[0].videoDurationSeconds, 27.75);
|
|
assert.equal(model.playlistSlides[0].durationSeconds, 27.75);
|
|
});
|
|
|
|
test('disabled canvas sizes are hidden for new playlists but retained for existing assignments', () => {
|
|
const canvasSizes = [
|
|
{ id: 1, name: 'Full HD', width: 1920, height: 1080, enabled: 1 },
|
|
{ id: 2, name: 'Legacy', width: 1280, height: 720, enabled: 0 }
|
|
];
|
|
const data = { playlistSlides: [], slides: [], canvasSizes: canvasSizes };
|
|
|
|
const createModel = buildPlaylistFormViewModel({}, data, '', null, {});
|
|
assert.deepEqual(createModel.canvasSizes.map((size) => size.id), [1]);
|
|
|
|
const editModel = buildPlaylistFormViewModel({ id: 12, canvas_id: 2 }, data, '', null, { isEdit: true });
|
|
assert.deepEqual(editModel.canvasSizes.map((size) => size.id), [1, 2]);
|
|
assert.match(editModel.canvasSizes[1].label, /Disabled/);
|
|
assert.equal(editModel.canvasSizes[1].selected, true);
|
|
}); |