Files
pulse-signage/test/player-playlist.test.js
T

148 lines
5.8 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const { createPlayerPlaylistService } = require('../src/player/playlist');
function createFsStub() {
const original = {
readFile: fs.promises.readFile,
writeFile: fs.promises.writeFile,
mkdir: fs.promises.mkdir
};
const calls = [];
fs.promises.readFile = async function (filePath) {
calls.push({ method: 'readFile', filePath });
if (filePath.endsWith('fallback.json')) {
return JSON.stringify({ fromSnapshot: true, screen: { slug: 'test2' } });
}
const error = new Error('missing');
error.code = 'ENOENT';
throw error;
};
fs.promises.writeFile = async function (filePath, content) {
calls.push({ method: 'writeFile', filePath, content });
};
fs.promises.mkdir = async function (dirPath, options) {
calls.push({ method: 'mkdir', dirPath, options });
};
return {
calls,
restore() {
fs.promises.readFile = original.readFile;
fs.promises.writeFile = original.writeFile;
fs.promises.mkdir = original.mkdir;
}
};
}
test('buildScreenPlaylist assembles slides, templates, and derived values', async () => {
const pool = {
async query(sql, params) {
if (sql.includes('FROM d_screens')) {
assert.deepEqual(params, ['test2']);
return [[{ id: 7, name: 'Screen 7', slug: 'test2', playlist_id: 22, modified_at: '2026-08-03T00:00:00.000Z' }]];
}
if (sql.includes('FROM c_playlists')) {
assert.deepEqual(params, [22]);
return [[{ id: 22, name: 'Playlist 22', fade_between_slides: 1, skip_unavailable_rtmp: 0, modified_at: '2026-08-02T00:00:00.000Z' }]];
}
if (sql.includes('FROM c_playlist_slides ps')) {
return [[{
id: 101,
title: 'Intro',
template_id: 33,
content_json: '{"videoRegion":{"type":"video","duration_seconds":12.3456},"textRegion":{"type":"text","value":"Hello"}}',
modified_at: '2026-08-03T00:00:01.000Z',
position: 1,
duration_seconds: 9,
use_video_duration: 1,
disable_audio: null,
template_name: 'Template 33',
canvas_size_name: 'HD',
canvas_size_width: 1920,
canvas_size_height: 1080,
canvas_width: 1920,
canvas_height: 1080
}]];
}
if (sql.includes('FROM c_playlist_slide_schedule_rules')) {
return [[
{ id: 500, playlist_slide_id: 101, position: 1, start_datetime: null, end_datetime: null, start_time: '08:00', end_time: '12:00', schedule_days_json: '[1,2,3]' },
{ id: 501, playlist_slide_id: 101, position: 2, start_datetime: null, end_datetime: null, start_time: '13:00', end_time: '17:00', schedule_days_json: '[4,5]' }
]];
}
if (sql.includes('FROM c_templates st')) {
assert.deepEqual(params, [[33]]);
return [[{ id: 33, name: 'Template 33', canvas_size_id: 4, canvas_size_width: 1920, canvas_size_height: 1080, background_image_path: '/media/bg.png', background_color: '#111111', modified_at: '2026-08-03T00:00:02.000Z' }]];
}
if (sql.includes('FROM c_template_regions')) {
assert.deepEqual(params, [[33]]);
return [[{ id: 900, template_id: 33, region_key: 'textRegion', region_type: 'text', label: 'Text Region', font_family: 'Arial', x: 10, y: 20, width: 300, height: 200, z_index: 1, modified_at: '2026-08-03T00:00:03.000Z' }]];
}
throw new Error(`unexpected query: ${sql}`);
}
};
const common = {
parseJsonSafe(value) {
return JSON.parse(value);
},
fetchRssFeedsData: async () => ({ rssFeeds: [{ id: 1, title: 'Feed 1' }] }),
fetchRssFeedItemsByFeedId: async () => ([{ title: 'Item 1', link: 'https://example.com' }]),
normalizeRssFeedItem(item) {
return Object.assign({}, item, { normalized: true });
},
fetchApiSourcesData: async () => ({ apiSources: [{ id: 2, name: 'Source 2', last_response_json: '{"ok":true}' }] }),
fetchTimetablesData: async () => ({ timetableGroups: [{ id: 3, name: 'Group 3' }] })
};
const fsStub = createFsStub();
try {
const service = createPlayerPlaylistService({ pool, common, snapshotDir: 'C:\\tmp\\snapshots' });
const payload = await service.buildScreenPlaylist('test2');
assert.equal(payload.screen.slug, 'test2');
assert.equal(payload.playlist.name, 'Playlist 22');
assert.equal(payload.slides[0].duration_seconds, 12.346);
assert.equal(payload.slides[0].disable_audio, true);
assert.equal(payload.slides[0].content.videoRegion.disable_audio, true);
assert.equal(payload.slides[0].content.videoRegion.cache_bust, '2026-08-03T00:00:01.000Z');
assert.equal(payload.slides[0].scheduleRules.length, 2);
assert.equal(payload.slides[0].template.regions[0].label, 'Text Region');
assert.equal(payload.rssFeeds[0].items[0].normalized, true);
assert.deepEqual(payload.apiSources[0].responseJson, { ok: true });
assert.deepEqual(payload.timetableGroups, [{ id: 3, name: 'Group 3' }]);
assert.match(payload.revision, /^[a-f0-9]{40}$/);
assert.ok(fsStub.calls.some((call) => call.method === 'writeFile'));
} finally {
fsStub.restore();
}
});
test('buildScreenPlaylist falls back to a snapshot when queries fail', async () => {
const pool = {
async query() {
throw new Error('database unavailable');
}
};
const common = {
parseJsonSafe() {
throw new Error('not expected');
}
};
const fsStub = createFsStub();
try {
const service = createPlayerPlaylistService({ pool, common, snapshotDir: 'C:\\tmp\\snapshots' });
const payload = await service.buildScreenPlaylist('fallback');
assert.deepEqual(payload, { fromSnapshot: true, screen: { slug: 'test2' } });
assert.ok(fsStub.calls.some((call) => call.method === 'readFile'));
} finally {
fsStub.restore();
}
});