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 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', 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('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); });