Release v2.15.2
This commit is contained in:
1 parent
ddcc665f81
commit
7295f83f04
43 files changed
+2526
-104
No files matched your search
@@ -46,6 +46,14 @@ test('playlist duplicate form page reuses the copied playlist id for rows', asyn
|
||||
assert.match(html, /data-playlist-id="12"/);
|
||||
assert.match(html, /Welcome/);
|
||||
assert.match(html, /Save and Close/);
|
||||
assert.ok(html.indexOf('id="playlist-create-group"') < html.indexOf('id="playlist-open-slide-modal"'));
|
||||
assert.doesNotMatch(html, /<th>Group<\/th>/);
|
||||
assert.match(html, /name="group_key\[\]"/);
|
||||
assert.match(html, /name="group_schedule_rules_json\[\]"/);
|
||||
assert.match(html, /name="group_shuffle\[\]" value="0"/);
|
||||
assert.match(html, /name="group_mode\[\]" value="select"/);
|
||||
assert.match(html, /name="group_repeat_count\[\]" value="1"/);
|
||||
assert.match(html, /name="duration_seconds\[\]" type="number" inputmode="decimal" min="1" step="any"/);
|
||||
});
|
||||
|
||||
test('playlist list template includes duplicate action', () => {
|
||||
@@ -53,4 +61,177 @@ test('playlist list template includes duplicate action', () => {
|
||||
|
||||
assert.match(template, /\/playlists\/\{\{id\}\}\/duplicate/);
|
||||
assert.match(template, />Dupe<\/a>/);
|
||||
});
|
||||
|
||||
test('playlist group colours differ, survive reorder, and clear on ungrouped slides', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'js', 'playlists', 'playlist-schedule.js'), 'utf8');
|
||||
const functionSource = source.match(/function updatePlaylistGroupColours\(rows, blocks\) \{[\s\S]*?\n \}/)[0];
|
||||
const updateColours = require('node:vm').runInNewContext('(' + functionSource + ')');
|
||||
function makeRow(isHeader) {
|
||||
const attributes = new Map();
|
||||
return {
|
||||
style: { setProperty(key, value) { this[key] = value; }, removeProperty(key) { delete this[key]; } },
|
||||
getAttribute(key) { return attributes.get(key) || null; },
|
||||
setAttribute(key, value) { attributes.set(key, value); },
|
||||
matches() { return isHeader; }
|
||||
};
|
||||
}
|
||||
const firstHeader = makeRow(true);
|
||||
const secondHeader = makeRow(true);
|
||||
firstHeader.setAttribute('data-group-colour', 'primary');
|
||||
secondHeader.setAttribute('data-group-colour', 'secondary');
|
||||
const firstMember = makeRow(false);
|
||||
const secondMember = makeRow(false);
|
||||
const blocks = [{ nodes: [firstHeader, firstMember] }, { nodes: [secondHeader, secondMember] }];
|
||||
updateColours([firstMember, secondMember], blocks);
|
||||
const firstColour = firstHeader.style['--playlist-group-accent'];
|
||||
const secondColour = secondHeader.style['--playlist-group-accent'];
|
||||
assert.notEqual(firstColour, secondColour);
|
||||
assert.equal(firstHeader.getAttribute('data-group-colour'), 'success');
|
||||
assert.equal(secondHeader.getAttribute('data-group-colour'), 'warning');
|
||||
const paletteHeaders = Array.from({ length: 8 }, () => makeRow(true));
|
||||
updateColours([], paletteHeaders.map(header => ({ nodes: [header] })));
|
||||
paletteHeaders.forEach(header => {
|
||||
assert.ok(['success', 'warning', 'info', 'danger'].includes(header.getAttribute('data-group-colour')));
|
||||
});
|
||||
assert.equal(firstMember.style['--playlist-group-accent'], firstColour);
|
||||
assert.equal(secondMember.style['--playlist-group-bg'], secondHeader.style['--playlist-group-bg']);
|
||||
updateColours([secondMember, firstMember], blocks.slice().reverse());
|
||||
assert.equal(firstHeader.style['--playlist-group-accent'], firstColour);
|
||||
assert.equal(secondHeader.style['--playlist-group-accent'], secondColour);
|
||||
blocks[0].nodes = [firstHeader];
|
||||
updateColours([firstMember, secondMember], blocks);
|
||||
assert.equal(firstMember.style['--playlist-group-accent'], undefined);
|
||||
assert.equal(firstMember.style['--playlist-group-bg'], undefined);
|
||||
});
|
||||
|
||||
test('empty playlist groups keep a drop area until a member is added', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'js', 'playlists', 'playlist-schedule.js'), 'utf8');
|
||||
const functionSource = source.match(/function updatePlaylistGroupEmptyRows\(\) \{[\s\S]*?\n \}/)[0];
|
||||
const emptyRows = new Set();
|
||||
const members = [];
|
||||
const header = { getAttribute() { return 'new-group'; }, after(row) { emptyRows.add(row); } };
|
||||
const tbody = { querySelectorAll() { return Array.from(emptyRows); } };
|
||||
const updateEmptyRows = require('node:vm').runInNewContext('(' + functionSource + ')', {
|
||||
document: {
|
||||
getElementById() { return tbody; },
|
||||
createElement() {
|
||||
return {
|
||||
attributes: {},
|
||||
setAttribute(key, value) { this.attributes[key] = value; },
|
||||
remove() { emptyRows.delete(this); }
|
||||
};
|
||||
}
|
||||
},
|
||||
getGroupHeaders() { return [header]; },
|
||||
getGroupRows() { return members; }
|
||||
});
|
||||
updateEmptyRows();
|
||||
assert.equal(emptyRows.size, 1);
|
||||
const emptyRow = Array.from(emptyRows)[0];
|
||||
assert.equal(emptyRow.attributes['data-group-key'], 'new-group');
|
||||
assert.match(emptyRow.innerHTML, /colspan="5"/);
|
||||
assert.doesNotMatch(emptyRow.innerHTML, /<input/);
|
||||
updateEmptyRows();
|
||||
assert.equal(emptyRows.size, 1);
|
||||
members.push({});
|
||||
updateEmptyRows();
|
||||
assert.equal(emptyRows.size, 0);
|
||||
members.pop();
|
||||
updateEmptyRows();
|
||||
assert.equal(emptyRows.size, 1);
|
||||
});
|
||||
|
||||
test('group metadata clamps Pick to membership and propagates shuffle state', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'js', 'playlists', 'playlist-schedule.js'), 'utf8');
|
||||
const functionSource = source.match(/function syncGroupRowMetadata\(header\) \{[\s\S]*?\n \}/)[0];
|
||||
const pick = { value: '99' };
|
||||
const controls = {
|
||||
'[data-playlist-group-name]': { value: 'News' },
|
||||
'[data-playlist-group-pick-count]': pick,
|
||||
'[data-playlist-group-repeat-count]': { value: '2' },
|
||||
'[data-playlist-group-shuffle]': { getAttribute() { return 'true'; } },
|
||||
'[data-playlist-group-schedule-rules]': { value: '[]' }
|
||||
};
|
||||
let mode = 'select';
|
||||
const header = { getAttribute(key) { return key === 'data-group-mode' ? mode : 'news'; }, querySelector(selector) { return controls[selector]; } };
|
||||
const members = Array.from({ length: 3 }, () => {
|
||||
const fields = new Map();
|
||||
return {
|
||||
classList: { add() {} }, setAttribute() {},
|
||||
querySelector(selector) { if (!fields.has(selector)) fields.set(selector, {}); return fields.get(selector); }
|
||||
};
|
||||
});
|
||||
const sync = require('node:vm').runInNewContext('(' + functionSource + ')', { getGroupRows() { return members; }, syncPlaylistGroupModeControls() {} });
|
||||
sync(header);
|
||||
assert.equal(pick.value, '3');
|
||||
assert.equal(pick.max, '3');
|
||||
assert.equal(members[0].querySelector('[data-playlist-group-shuffle-value]').value, '1');
|
||||
assert.equal(members[0].querySelector('[data-playlist-group-mode-value]').value, 'select');
|
||||
mode = 'repeat';
|
||||
sync(header);
|
||||
assert.equal(members[0].querySelector('[data-playlist-group-mode-value]').value, 'repeat');
|
||||
assert.equal(members[0].querySelector('[data-playlist-group-repeat-count-value]').value, '2');
|
||||
assert.equal(members[0].querySelector('[data-playlist-group-shuffle-value]').value, '1');
|
||||
controls['[data-playlist-group-repeat-count]'].value = '101';
|
||||
sync(header);
|
||||
assert.equal(controls['[data-playlist-group-repeat-count]'].value, '100');
|
||||
controls['[data-playlist-group-repeat-count]'].value = '0';
|
||||
sync(header);
|
||||
assert.equal(controls['[data-playlist-group-repeat-count]'].value, '1');
|
||||
pick.value = '0';
|
||||
sync(header);
|
||||
assert.equal(pick.value, '1');
|
||||
pick.value = '2.9';
|
||||
sync(header);
|
||||
assert.equal(pick.value, '2');
|
||||
members.splice(1);
|
||||
sync(header);
|
||||
assert.equal(pick.value, '1');
|
||||
assert.equal(pick.max, '1');
|
||||
members.pop();
|
||||
sync(header);
|
||||
assert.equal(pick.disabled, true);
|
||||
});
|
||||
|
||||
test('playlist numbering gives groups whole numbers and members hierarchical numbers', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'js', 'playlists', 'playlist-schedule.js'), 'utf8');
|
||||
const functionSource = source.match(/function updateRowOrder\(markDirty\) \{[\s\S]*?\n \}/)[0];
|
||||
function makeRow(groupKey, isHeader) {
|
||||
const number = { textContent: '' };
|
||||
const key = { value: groupKey };
|
||||
return {
|
||||
number, key,
|
||||
matches() { return Boolean(isHeader); },
|
||||
querySelector(selector) {
|
||||
if (selector === '.playlist-order-number' || (isHeader && selector === '[data-playlist-group-order]')) return number;
|
||||
if (selector === '[data-playlist-group-key]') return key;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
const first = makeRow('');
|
||||
const header = makeRow('news', true);
|
||||
const members = [makeRow('news'), makeRow('news')];
|
||||
const last = makeRow('');
|
||||
const rows = [first, ...members, last];
|
||||
const blocks = [{ key: '', nodes: [first] }, { key: 'news', nodes: [header, ...members] }, { key: '', nodes: [last] }];
|
||||
const sandbox = {
|
||||
getRows() { return rows; }, getGroupHeaders() { return [header]; }, getTopLevelPlaylistBlocks() { return blocks; }
|
||||
};
|
||||
for (const name of ['updatePlaylistGroupEmptyRows', 'syncGroupRowMetadata', 'updatePlaylistGroupColours', 'syncAddSlideOptions', 'updateGroupScheduleWarnings', 'syncPlaylistCanvasLock', 'syncAddSlideButtonState', 'updateEmptyState']) {
|
||||
sandbox[name] = function () {};
|
||||
}
|
||||
const update = require('node:vm').runInNewContext('(' + functionSource + ')', sandbox);
|
||||
update(false);
|
||||
assert.deepEqual([first, header, ...members, last].map(row => row.number.textContent), ['1', '2', '2.1', '2.2', '3']);
|
||||
blocks.unshift(blocks.splice(1, 1)[0]);
|
||||
update(false);
|
||||
assert.deepEqual([header, ...members, first, last].map(row => row.number.textContent), ['1', '1.1', '1.2', '2', '3']);
|
||||
members[1].key.value = '';
|
||||
blocks[0].nodes.pop();
|
||||
blocks.splice(1, 0, { key: '', nodes: [members[1]] });
|
||||
update(false);
|
||||
assert.equal(members[1].number.textContent, '2');
|
||||
assert.equal(last.number.textContent, '4');
|
||||
});
|
||||
Reference in new issue
Block a user