Add template animation editor
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user