Release 2.22.1: preserve playlist row identities
This commit is contained in:
1 parent
c40bc6844a
commit
9b87ad4721
10 files changed
+337
-75
No files matched your search
@@ -41,6 +41,32 @@ test('new playlist rows receive preview URLs, titles and canvas dimensions', ()
|
||||
assert.equal(attrs['aria-label'], 'Preview slide: Portrait');
|
||||
});
|
||||
|
||||
test('playlist group rule JSON is escaped in the hidden form field and remains intact', () => {
|
||||
const { renderView } = require('../src/web/view');
|
||||
const model = buildPlaylistFormViewModel({ id: 3, name: 'Group rules' }, {
|
||||
playlistSlides: [{
|
||||
id: 12,
|
||||
playlist_id: 3,
|
||||
slide_id: 22,
|
||||
position: 0,
|
||||
group_id: 5,
|
||||
group_key: 'events',
|
||||
group_name: 'Events',
|
||||
group_pick_count: 1,
|
||||
groupScheduleRules: [{ id: 71, playlist_group_id: 5, position: 0, start_time: '09:00:00', end_time: '17:00:00', schedule_days_json: '[1,3]' }],
|
||||
scheduleRules: []
|
||||
}],
|
||||
slides: [],
|
||||
canvasSizes: []
|
||||
}, '', null, { isEdit: true });
|
||||
const html = renderView('playlists/form', model);
|
||||
const hiddenRuleField = html.match(/<input[^>]+data-playlist-group-schedule-rules[^>]+>/);
|
||||
|
||||
assert.ok(hiddenRuleField);
|
||||
assert.match(hiddenRuleField[0], /value="\[\{"id":71,/);
|
||||
assert.doesNotMatch(hiddenRuleField[0], /value="\[\{"id/);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
+123
-22
@@ -54,14 +54,9 @@ test('playlist create and update redirect to the canonical edit path', async ()
|
||||
calls.push('update-playlist');
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('DELETE FROM c_playlist_slides')) {
|
||||
calls.push('delete-slides');
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('DELETE FROM c_playlist_groups')) {
|
||||
calls.push('delete-groups');
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('SELECT ps.id, ps.slide_id, ps.group_id')) return [[]];
|
||||
if (sql.startsWith('SELECT id, group_key FROM c_playlist_groups')) return [[]];
|
||||
if (sql.startsWith('SELECT r.id, r.playlist_slide_id')) return [[]];
|
||||
if (sql.startsWith('SELECT sl.id, st.canvas_size_id')) {
|
||||
return [[{ id: 11, canvas_size_id: 7 }]];
|
||||
}
|
||||
@@ -171,12 +166,8 @@ test('playlist create and update redirect to the canonical edit path', async ()
|
||||
[71, 1, null, '2026-10-31T18:00', null, null, null, 12, 12]
|
||||
]);
|
||||
assert.deepEqual(calls, [
|
||||
'delete-slides',
|
||||
'delete-groups',
|
||||
{ kind: 'redirectAfterSave', url: '/playlists/4/edit', path: '/playlists' },
|
||||
'update-playlist',
|
||||
'delete-slides',
|
||||
'delete-groups',
|
||||
'insert-group',
|
||||
'insert-group-rule',
|
||||
'insert-group-rule',
|
||||
@@ -220,24 +211,134 @@ test('playlist create and update redirect to the canonical edit path', async ()
|
||||
}
|
||||
});
|
||||
|
||||
test('playlist save updates retained slide, group, and schedule rows in place', async () => {
|
||||
const writes = [];
|
||||
let items = [
|
||||
{ id: 101, slide_id: 11, group_id: 201, duration_seconds: 10, position: 0, use_video_duration: 0, disable_audio: 1 },
|
||||
{ id: 102, slide_id: 12, group_id: null, duration_seconds: 15, position: 1, use_video_duration: 0, disable_audio: 1 }
|
||||
];
|
||||
let groups = [{ id: 201, group_key: 'news' }];
|
||||
let rules = [
|
||||
{ id: 301, playlist_slide_id: null, playlist_group_id: 201, position: 0, start_datetime: null, end_datetime: null, start_time: '08:00:00', end_time: '17:00:00', schedule_days_json: '[1,2]' },
|
||||
{ id: 302, playlist_slide_id: 101, playlist_group_id: null, position: 0, start_datetime: null, end_datetime: null, start_time: '09:00:00', end_time: '18:00:00', schedule_days_json: null },
|
||||
{ id: 303, playlist_slide_id: 102, playlist_group_id: null, position: 0, start_datetime: null, end_datetime: null, start_time: '10:00:00', end_time: '19:00:00', schedule_days_json: null }
|
||||
];
|
||||
const connection = {
|
||||
async beginTransaction() {}, async commit() {}, async rollback() {}, release() {},
|
||||
async query(sql, params) {
|
||||
if (sql.startsWith('SELECT ps.id, ps.slide_id, ps.group_id FROM c_playlist_slides')) return [structuredClone(items)];
|
||||
if (sql.startsWith('SELECT id, group_key FROM c_playlist_groups')) return [structuredClone(groups)];
|
||||
if (sql.startsWith('SELECT r.id, r.playlist_slide_id')) return [structuredClone(rules)];
|
||||
if (sql.startsWith('SELECT sl.id, st.canvas_size_id')) return [[{ id: 11, canvas_size_id: 7 }, { id: 13, canvas_size_id: 7 }]];
|
||||
if (sql.startsWith('UPDATE c_playlists SET')) return [{ affectedRows: 1 }];
|
||||
if (sql.startsWith('UPDATE c_playlist_groups SET')) {
|
||||
writes.push({ kind: 'update-group', id: params[6], name: params[0] });
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('UPDATE c_playlist_slides SET')) {
|
||||
writes.push({ kind: 'update-slide', id: params[7], duration: params[2] });
|
||||
items = items.map(item => item.id === params[7] ? Object.assign({}, item, {
|
||||
slide_id: params[0], position: params[1], duration_seconds: params[2],
|
||||
use_video_duration: params[3], disable_audio: params[4], group_id: params[5]
|
||||
}) : item);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('UPDATE c_playlist_schedule_rules SET')) {
|
||||
writes.push({ kind: 'update-rule', id: params[7] });
|
||||
rules = rules.map(rule => rule.id === params[7] ? Object.assign({}, rule, {
|
||||
position: params[0], start_datetime: params[1], end_datetime: params[2],
|
||||
start_time: params[3], end_time: params[4], schedule_days_json: params[5]
|
||||
}) : rule);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('INSERT INTO c_playlist_slides')) {
|
||||
writes.push({ kind: 'insert-slide', slideId: params[1] });
|
||||
items.push({ id: 103, slide_id: params[1], group_id: params[6], duration_seconds: params[3], position: params[2] });
|
||||
return [{ insertId: 103 }];
|
||||
}
|
||||
if (sql.startsWith('DELETE FROM c_playlist_slides')) {
|
||||
writes.push({ kind: 'delete-slide', id: params[0] });
|
||||
items = items.filter(item => item.id !== params[0]);
|
||||
rules = rules.filter(rule => rule.playlist_slide_id !== params[0]);
|
||||
}
|
||||
return [[]];
|
||||
}
|
||||
};
|
||||
const { app, handlers } = createAppAndHandlers();
|
||||
registerPlaylistRoutes(app, {
|
||||
pool: { async getConnection() { return connection; } },
|
||||
common: {
|
||||
async fetchDuplicateName() { return null; },
|
||||
async fetchPlaylistById() { return { id: 4, name: 'Loop', canvas_id: 7 }; }
|
||||
},
|
||||
readArrayField(body, keys) {
|
||||
for (const key of keys) {
|
||||
if (Object.hasOwn(body, key)) return Array.isArray(body[key]) ? body[key] : [body[key]];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
fetchScreensByPlaylistId: async () => [], getAuditUserId: () => 12,
|
||||
redirectAfterSave(_req, res, url) { res.redirect(url); }, notifyPlayerScreens: async () => {}, broadcastDashboardState: async () => {},
|
||||
requirePermission: () => () => {}
|
||||
});
|
||||
|
||||
const response = createResponse();
|
||||
await handlers['/playlists/:id'].at(-1)({
|
||||
params: { id: '4' },
|
||||
body: {
|
||||
name: 'Loop',
|
||||
slide_id: ['11', '13'], row_key: ['101', 'new-slide'],
|
||||
duration_seconds: ['20', '12'], use_video_duration: ['0', '0'], disable_audio: ['1', '1'],
|
||||
group_key: ['news', ''], group_name: ['News updated', ''], group_pick_count: ['1', ''], group_shuffle: ['0', ''],
|
||||
group_mode: ['repeat', ''], group_repeat_count: ['3', ''],
|
||||
group_schedule_rules_json: [JSON.stringify([{ start_time: '08:30', end_time: '17:30', days: [1, 2] }]), ''],
|
||||
schedule_rule_row_key: ['101'], schedule_rule_start_time: ['09:30'],
|
||||
schedule_rule_end_time: ['18:30'], schedule_rule_days_csv: ['']
|
||||
}
|
||||
}, response, error => { throw error; });
|
||||
|
||||
assert.equal(response.redirectedTo, '/playlists/4/edit', response.statusCode + ': ' + String(response.body || ''));
|
||||
assert.deepEqual(writes, [
|
||||
{ kind: 'update-group', id: 201, name: 'News updated' },
|
||||
{ kind: 'update-rule', id: 301 },
|
||||
{ kind: 'update-slide', id: 101, duration: 20 },
|
||||
{ kind: 'update-rule', id: 302 },
|
||||
{ kind: 'insert-slide', slideId: 13 },
|
||||
{ kind: 'delete-slide', id: 102 }
|
||||
]);
|
||||
assert.deepEqual(items.map(item => item.id), [101, 103]);
|
||||
assert.deepEqual(rules.map(rule => rule.id), [301, 302]);
|
||||
});
|
||||
|
||||
test('playlist audit compares persisted items and schedules without recreated row IDs', async () => {
|
||||
let items = [{ id: 101, slide_id: 11, group_id: null, duration_seconds: 10 }];
|
||||
let rules = [{ playlist_slide_id: 101, start_time: '08:00:00', end_time: '17:00:00', schedule_days_json: '[1,2]' }];
|
||||
let items = [{ id: 101, slide_id: 11, group_id: null, duration_seconds: 10, position: 0, use_video_duration: 0, disable_audio: 1 }];
|
||||
let rules = [{ id: 301, playlist_slide_id: 101, playlist_group_id: null, position: 0, start_datetime: null, end_datetime: null, start_time: '08:00:00', end_time: '17:00:00', schedule_days_json: '[1,2]' }];
|
||||
const events = [];
|
||||
const connection = {
|
||||
async commit() {}, async rollback() {}, release() {},
|
||||
async beginTransaction() {}, async commit() {}, async rollback() {}, release() {},
|
||||
async query(sql, params) {
|
||||
if (sql.startsWith('SELECT ps.id, ps.slide_id, ps.group_id FROM c_playlist_slides')) return [structuredClone(items)];
|
||||
if (sql.includes('FROM c_playlist_slides ps') && sql.includes('WHERE ps.playlist_id')) return [structuredClone(items)];
|
||||
if (sql.startsWith('SELECT id, group_key FROM c_playlist_groups')) return [[]];
|
||||
if (sql.includes('FROM c_playlist_schedule_rules r')) return [structuredClone(rules)];
|
||||
if (sql.startsWith('SELECT sl.id, st.canvas_size_id')) return [[{ id: 11, canvas_size_id: 7 }]];
|
||||
if (sql.startsWith('DELETE FROM c_playlist_slides')) { items = []; rules = []; }
|
||||
if (sql.startsWith('DELETE FROM c_playlist_groups')) return [{ affectedRows: 0 }];
|
||||
if (sql.startsWith('INSERT INTO c_playlist_slides')) {
|
||||
items = [{ id: 202, slide_id: params[1], group_id: params[6], duration_seconds: params[3] }];
|
||||
return [{ insertId: 202 }];
|
||||
if (sql.startsWith('UPDATE c_playlist_slides SET')) {
|
||||
items = items.map(item => item.id === params[7] ? Object.assign({}, item, {
|
||||
slide_id: params[0], position: params[1], duration_seconds: params[2],
|
||||
use_video_duration: params[3], disable_audio: params[4], group_id: params[5]
|
||||
}) : item);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('INSERT INTO c_playlist_schedule_rules')) {
|
||||
rules.push({ playlist_slide_id: params[0], start_time: params[4] + ':00', end_time: params[5] + ':00', schedule_days_json: params[6] });
|
||||
if (sql.startsWith('UPDATE c_playlist_schedule_rules SET')) {
|
||||
rules = rules.map(rule => rule.id === params[7] ? Object.assign({}, rule, {
|
||||
position: params[0], start_datetime: params[1], end_datetime: params[2],
|
||||
start_time: params[3] + ':00', end_time: params[4] + ':00', schedule_days_json: params[5]
|
||||
}) : rule);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('DELETE FROM c_playlist_slides')) {
|
||||
items = items.filter(item => item.id !== params[0]);
|
||||
rules = rules.filter(rule => rule.playlist_slide_id !== params[0]);
|
||||
}
|
||||
return [[]];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user