Compare commits

...
2 Commits
Author SHA1 Message Date
lzstealth d3e059a878 Release v2.5.9 2026-08-05 21:05:43 +01:00
lzstealth 38d82d2ac6 Fix QR background gradient handling 2026-08-05 19:51:54 +01:00
13 changed files with 285 additions and 32 deletions
+19
View File
@@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 2.5.9 - 2026-08-05
### Fixed
- Screen playlist refreshes now wait until the next slide transition before applying a pending update, so one-slide playlists do not swap immediately during a refresh.
- Playlist rows now show the mute control for any playable media region, including RTMP slides, instead of only video regions.
## 2.5.8 - 2026-08-05
### Fixed
- MariaDB migrations now retry alternate foreign-key constraint names when screen-to-player constraint creation reports a duplicate-key error.
## 2.5.7 - 2026-08-05
### Fixed
- QR template backgrounds now use a single solid color only, and the editor no longer exposes a background gradient mode.
## 2.5.6 - 2026-08-05 ## 2.5.6 - 2026-08-05
### Added ### Added
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.5.6", "version": "2.5.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.5.6", "version": "2.5.9",
"dependencies": { "dependencies": {
"@sparticuz/chromium": "^137.0.0", "@sparticuz/chromium": "^137.0.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.5.6", "version": "2.5.9",
"private": false, "private": false,
"description": "Pulse Signage application with MySQL and media storage", "description": "Pulse Signage application with MySQL and media storage",
"repository": { "repository": {
+3 -12
View File
@@ -312,14 +312,6 @@ function buildQrStylingOptions(source) {
{ offset: 1, color: normalizeQrStyleColor(qrStyle.qr_corners_dot_gradient_color_2, '#ffffff') } { offset: 1, color: normalizeQrStyleColor(qrStyle.qr_corners_dot_gradient_color_2, '#ffffff') }
] ]
} : null; } : null;
const backgroundGradient = qrStyle.qr_background_color_mode === 'gradient' ? {
type: String(qrStyle.qr_background_gradient_type || 'linear').trim() || 'linear',
rotation: normalizeQrStyleNumber(qrStyle.qr_background_gradient_rotation, 0, undefined, undefined),
colorStops: [
{ offset: 0, color: normalizeQrStyleColor(qrStyle.qr_background_gradient_color_1, normalizeQrStyleColor(qrStyle.qr_background_color, '#ffffff')) },
{ offset: 1, color: normalizeQrStyleColor(qrStyle.qr_background_gradient_color_2, '#ffffff') }
]
} : null;
return { return {
width: QR_PNG_WIDTH, width: QR_PNG_WIDTH,
@@ -344,8 +336,7 @@ function buildQrStylingOptions(source) {
gradient: cornersDotGradient || undefined gradient: cornersDotGradient || undefined
}, },
backgroundOptions: { backgroundOptions: {
color: qrStyle.qr_background_transparent ? 'transparent' : normalizeQrStyleColor(qrStyle.qr_background_color, '#ffffff'), color: qrStyle.qr_background_transparent ? 'transparent' : normalizeQrStyleColor(qrStyle.qr_background_color, '#ffffff')
gradient: backgroundGradient || undefined
}, },
image: String(qrStyle.qr_image || '').trim() || undefined, image: String(qrStyle.qr_image || '').trim() || undefined,
imageOptions: { imageOptions: {
@@ -491,7 +482,7 @@ async function buildQrCodeContent(value, options) {
qr_border_radius: Number.isFinite(Number(source.qr_border_radius)) ? Math.max(0, Math.round(Number(source.qr_border_radius))) : undefined, qr_border_radius: Number.isFinite(Number(source.qr_border_radius)) ? Math.max(0, Math.round(Number(source.qr_border_radius))) : undefined,
qr_background_color: String(source.qr_background_color === undefined || source.qr_background_color === null ? '' : source.qr_background_color).trim() || undefined, qr_background_color: String(source.qr_background_color === undefined || source.qr_background_color === null ? '' : source.qr_background_color).trim() || undefined,
qr_background_transparent: hasBooleanField('qr_background_transparent') ? Boolean(source.qr_background_transparent) : undefined, qr_background_transparent: hasBooleanField('qr_background_transparent') ? Boolean(source.qr_background_transparent) : undefined,
qr_background_color_mode: String(source.qr_background_color_mode === undefined || source.qr_background_color_mode === null ? '' : source.qr_background_color_mode).trim() || undefined, qr_background_color_mode: 'single',
qr_background_gradient_type: String(source.qr_background_gradient_type === undefined || source.qr_background_gradient_type === null ? '' : source.qr_background_gradient_type).trim() || undefined, qr_background_gradient_type: String(source.qr_background_gradient_type === undefined || source.qr_background_gradient_type === null ? '' : source.qr_background_gradient_type).trim() || undefined,
qr_background_gradient_rotation: normalizeNumber(source.qr_background_gradient_rotation), qr_background_gradient_rotation: normalizeNumber(source.qr_background_gradient_rotation),
qr_background_gradient_color_1: normalizeHex(source.qr_background_gradient_color_1), qr_background_gradient_color_1: normalizeHex(source.qr_background_gradient_color_1),
@@ -533,7 +524,7 @@ async function buildQrCodeContent(value, options) {
} }
}); });
content.qr_background_use_gradient = content.qr_background_gradient_type && content.qr_background_gradient_type !== 'none'; content.qr_background_use_gradient = false;
content.qr_dots_use_gradient = content.qr_dots_gradient_type && content.qr_dots_gradient_type !== 'none'; content.qr_dots_use_gradient = content.qr_dots_gradient_type && content.qr_dots_gradient_type !== 'none';
content.qr_corners_square_use_gradient = content.qr_corners_square_gradient_type && content.qr_corners_square_gradient_type !== 'none'; content.qr_corners_square_use_gradient = content.qr_corners_square_gradient_type && content.qr_corners_square_gradient_type !== 'none';
content.qr_corners_dot_use_gradient = content.qr_corners_dot_gradient_type && content.qr_corners_dot_gradient_type !== 'none'; content.qr_corners_dot_use_gradient = content.qr_corners_dot_gradient_type && content.qr_corners_dot_gradient_type !== 'none';
+1 -1
View File
@@ -282,7 +282,7 @@ async function ensureForeignKey(pool, tableName, constraintName, columnName, ref
await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + candidateName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction); await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + candidateName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction);
return; return;
} catch (error) { } catch (error) {
if (!error || (error.code !== 'ER_FK_DUP_NAME' && error.errno !== 1826)) { if (!error || (error.code !== 'ER_FK_DUP_NAME' && error.errno !== 1826 && error.errno !== 121)) {
throw error; throw error;
} }
} }
@@ -130,6 +130,7 @@ function scheduleSlideAdvance(delayMs) {
slideExpiresAt = null; slideExpiresAt = null;
pausedRemainingMs = null; pausedRemainingMs = null;
clearSlideOutroTimer(); clearSlideOutroTimer();
applyPendingPlaylistUpdate();
const activeSlides = getCurrentActiveSlides(); const activeSlides = getCurrentActiveSlides();
if (activeSlides.length < 2) { if (activeSlides.length < 2) {
refresh(); refresh();
+7 -9
View File
@@ -85,7 +85,6 @@ function applyPendingPlaylistUpdate() {
// Render the current active slide or the empty state. // Render the current active slide or the empty state.
function showCurrent() { function showCurrent() {
clearSlideTimer(); clearSlideTimer();
applyPendingPlaylistUpdate();
const activeSlides = getCurrentActiveSlides(); const activeSlides = getCurrentActiveSlides();
syncWebpagePreloads(activeSlides, index); syncWebpagePreloads(activeSlides, index);
if (typeof syncRtmpWarmups === 'function') { if (typeof syncRtmpWarmups === 'function') {
@@ -202,14 +201,13 @@ function refresh() {
syncRtmpWarmups(getActiveSlidesFrom(nextSlides), index); syncRtmpWarmups(getActiveSlidesFrom(nextSlides), index);
} }
if (currentActiveSlides.length < 2) { if (currentActiveSlides.length < 2) {
slides = nextSlides; pendingPlaylistUpdate = {
currentPlaylistSignature = nextSignature; slides: nextSlides,
currentPlaylistFadeBetweenSlides = nextFadeBetweenSlides; signature: nextSignature,
currentPlaylistSkipUnavailableRtmp = nextSkipUnavailableRtmp; fadeBetweenSlides: nextFadeBetweenSlides,
pendingPlaylistUpdate = null; skipUnavailableRtmp: nextSkipUnavailableRtmp
index = 0; };
showCurrent(); logDebug('Playlist update detected; applying on next slide transition.');
sendCommandState(lastRenderedSlide);
return; return;
} }
pendingPlaylistUpdate = { pendingPlaylistUpdate = {
+9 -3
View File
@@ -251,6 +251,7 @@
qr_margin: normalizeMarginValue(source.qr_margin, defaultQrOptions.margin), qr_margin: normalizeMarginValue(source.qr_margin, defaultQrOptions.margin),
qr_background_color: normalizeHexColorValue(source.qr_background_color, '#ffffff'), qr_background_color: normalizeHexColorValue(source.qr_background_color, '#ffffff'),
qr_background_transparent: normalizeBooleanValue(source.qr_background_transparent, false), qr_background_transparent: normalizeBooleanValue(source.qr_background_transparent, false),
qr_background_color_mode: 'single',
qr_background_gradient_type: normalizeGradientTypeValue(source.qr_background_gradient_type, 'linear'), qr_background_gradient_type: normalizeGradientTypeValue(source.qr_background_gradient_type, 'linear'),
qr_background_gradient_rotation: normalizeGradientRotationValue(source.qr_background_gradient_rotation, 0), qr_background_gradient_rotation: normalizeGradientRotationValue(source.qr_background_gradient_rotation, 0),
qr_background_gradient_color_1: normalizeHexColorValue(source.qr_background_gradient_color_1, '#ffffff'), qr_background_gradient_color_1: normalizeHexColorValue(source.qr_background_gradient_color_1, '#ffffff'),
@@ -729,7 +730,6 @@
var dotsGradient = style.qr_dots_color_mode === 'gradient' ? buildGradientOptions('qr_dots', style) : null; var dotsGradient = style.qr_dots_color_mode === 'gradient' ? buildGradientOptions('qr_dots', style) : null;
var cornersSquareGradient = style.qr_corners_square_color_mode === 'gradient' ? buildGradientOptions('qr_corners_square', style) : null; var cornersSquareGradient = style.qr_corners_square_color_mode === 'gradient' ? buildGradientOptions('qr_corners_square', style) : null;
var cornersDotGradient = style.qr_corners_dot_color_mode === 'gradient' ? buildGradientOptions('qr_corners_dot', style) : null; var cornersDotGradient = style.qr_corners_dot_color_mode === 'gradient' ? buildGradientOptions('qr_corners_dot', style) : null;
var backgroundGradient = style.qr_background_color_mode === 'gradient' ? buildGradientOptions('qr_background', style) : null;
return { return {
width: defaultQrOptions.width, width: defaultQrOptions.width,
height: defaultQrOptions.height, height: defaultQrOptions.height,
@@ -753,8 +753,7 @@
gradient: cornersDotGradient || undefined gradient: cornersDotGradient || undefined
}, },
backgroundOptions: { backgroundOptions: {
color: style.qr_background_transparent ? 'transparent' : style.qr_background_color, color: style.qr_background_transparent ? 'transparent' : style.qr_background_color
gradient: backgroundGradient || undefined
}, },
image: style.qr_image || undefined, image: style.qr_image || undefined,
imageOptions: { imageOptions: {
@@ -818,6 +817,13 @@
function renderColorModeSection(accordionId, sectionKey, prefix, regionId, label, style, singleControlsHtml, gradientLabel, fallbackColor, currentOpen, styleSelectHtml) { function renderColorModeSection(accordionId, sectionKey, prefix, regionId, label, style, singleControlsHtml, gradientLabel, fallbackColor, currentOpen, styleSelectHtml) {
var mode = normalizeColorModeValue(style[prefix + '_color_mode'], 'single'); var mode = normalizeColorModeValue(style[prefix + '_color_mode'], 'single');
if (prefix === 'qr_background') {
return renderAccordionSection(accordionId, sectionKey, label, '' +
'<div class="d-grid gap-3" data-color-mode-accordion-body>' +
'<div data-single-color-config>' + singleControlsHtml + '</div>' +
styleSelectHtml +
'</div>', currentOpen);
}
var colorModeSelect = '' + var colorModeSelect = '' +
'<div>' + '<div>' +
'<label class="form-label mb-1" for="' + prefix + '_color_mode_' + regionId + '">Colour mode</label>' + '<label class="form-label mb-1" for="' + prefix + '_color_mode_' + regionId + '">Colour mode</label>' +
@@ -193,7 +193,7 @@ function buildPlaylistFormViewModel(playlist, data, message, currentUser, option
isLast: index === items.length - 1, isLast: index === items.length - 1,
canvasSizeId: Number(item.canvas_size_id) || null, canvasSizeId: Number(item.canvas_size_id) || null,
showVideoDurationButton: hasVideoRegion(item.content_json), showVideoDurationButton: hasVideoRegion(item.content_json),
showMuteButton: hasVideoRegion(item.content_json), showMuteButton: hasPlayableMediaRegion(item.content_json),
videoSourcePath: getVideoSourcePath(item), videoSourcePath: getVideoSourcePath(item),
videoDurationSeconds: getVideoDurationSeconds(item), videoDurationSeconds: getVideoDurationSeconds(item),
useVideoDuration: Boolean(item.use_video_duration), useVideoDuration: Boolean(item.use_video_duration),
@@ -221,7 +221,7 @@ function buildPlaylistFormViewModel(playlist, data, message, currentUser, option
canvasSizeId: Number(slide.canvas_size_id) || null, canvasSizeId: Number(slide.canvas_size_id) || null,
isAssigned: assignedSlideIds.has(slide.id), isAssigned: assignedSlideIds.has(slide.id),
showVideoDurationButton: hasVideoRegion(slide.content_json), showVideoDurationButton: hasVideoRegion(slide.content_json),
showMuteButton: hasVideoRegion(slide.content_json), showMuteButton: hasPlayableMediaRegion(slide.content_json),
videoSourcePath: getVideoSourcePath(slide), videoSourcePath: getVideoSourcePath(slide),
videoDurationSeconds: getVideoDurationSeconds(slide), videoDurationSeconds: getVideoDurationSeconds(slide),
useVideoDuration: Boolean(slide.use_video_duration), useVideoDuration: Boolean(slide.use_video_duration),
+136
View File
@@ -0,0 +1,136 @@
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
test('playlist refresh queues updates until the next slide transition', async () => {
const calls = {
showCurrent: 0,
logDebug: [],
scheduleSlideAdvance: []
};
const sandbox = {
window: null,
location: { origin: 'http://localhost', href: 'http://localhost/screen/test2' },
Date,
JSON,
Math,
Number,
String,
Boolean,
Array,
Object,
Promise,
setTimeout,
clearTimeout,
console,
currentPlaylistSignature: 'old-signature',
currentPlaylistFadeBetweenSlides: false,
currentPlaylistSkipUnavailableRtmp: false,
slug: 'test2',
pendingPlaylistUpdate: null,
slides: [{ id: 1, duration_seconds: 12 }],
lastRenderedSlide: { id: 1, duration_seconds: 12 },
index: 0,
timer: null,
slideExpiresAt: null,
pausedRemainingMs: null,
currentPlaylistEtag: '',
activeSlidesCacheKey: '',
activeSlidesCacheValue: [],
renderCacheViewportKey: '',
slideMarkupCache: Object.create(null),
templateLayoutCache: Object.create(null),
templateRenderPlanCache: Object.create(null),
getCurrentActiveSlides() {
return sandbox.slides;
},
getPlaylistRevision(data) {
return data.signature;
},
normalizeSlide(slide) {
return slide;
},
getActiveSlidesFrom(slideList) {
return slideList;
},
savePlaylistSnapshot() {},
markRefreshHealthy() {},
setOfflineBannerVisible() {},
scheduleRefreshRetry() {},
syncWebpagePreloads() {},
syncRtmpWarmups() {},
showCurrent() {
calls.showCurrent += 1;
},
sendCommandState() {},
scheduleSlideAdvance(delayMs) {
calls.scheduleSlideAdvance.push(delayMs);
},
logDebug(...args) {
calls.logDebug.push(args.join(' '));
}
};
sandbox.window = sandbox;
class XhrStub {
open(method, url) {
this.method = method;
this.url = url;
}
setRequestHeader() {}
getResponseHeader(name) {
return name === 'ETag' ? '"next-etag"' : '';
}
send() {
this.readyState = 4;
this.status = 200;
this.responseText = JSON.stringify({
signature: 'next-signature',
slides: [{ id: 1, duration_seconds: 12, disable_audio: false }],
playlist: { fade_between_slides: false, skip_unavailable_rtmp: false }
});
if (typeof this.onreadystatechange === 'function') {
this.onreadystatechange();
}
}
}
sandbox.XMLHttpRequest = XhrStub;
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playback.js');
const script = fs.readFileSync(scriptPath, 'utf8');
const prelude = `
var pendingPlaylistUpdate = null;
var slides = [{ id: 1, duration_seconds: 12 }];
var currentPlaylistSignature = 'old-signature';
var currentPlaylistFadeBetweenSlides = false;
var currentPlaylistSkipUnavailableRtmp = false;
var currentPlaylistEtag = '';
var lastRenderedSlide = { id: 1, duration_seconds: 12 };
var index = 0;
var timer = null;
var slideExpiresAt = null;
var pausedRemainingMs = null;
var activeSlidesCacheKey = '';
var activeSlidesCacheValue = [];
var renderCacheViewportKey = '';
var slideMarkupCache = Object.create(null);
var templateLayoutCache = Object.create(null);
var templateRenderPlanCache = Object.create(null);
`;
vm.runInNewContext(prelude + '\n' + script, sandbox, { filename: scriptPath });
await sandbox.refresh();
assert.equal(calls.showCurrent, 0);
assert.equal(sandbox.currentPlaylistSignature, 'old-signature');
assert.equal(sandbox.slides[0].disable_audio, undefined);
assert.equal(calls.logDebug.some((entry) => entry.includes('Unable to load screen playlist.')), false);
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), true);
});
+20 -2
View File
@@ -68,6 +68,22 @@ test('buildScreenPlaylist assembles slides, templates, and derived values', asyn
canvas_size_height: 1080, canvas_size_height: 1080,
canvas_width: 1920, canvas_width: 1920,
canvas_height: 1080 canvas_height: 1080
}, {
id: 102,
title: 'Live Feed',
template_id: 33,
content_json: '{"rtmpRegion":{"type":"rtmp","value":"rtmp://example/live"}}',
modified_at: '2026-08-03T00:00:01.000Z',
position: 2,
duration_seconds: 15,
use_video_duration: 0,
disable_audio: 1,
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')) { if (sql.includes('FROM c_playlist_slide_schedule_rules')) {
@@ -77,11 +93,11 @@ test('buildScreenPlaylist assembles slides, templates, and derived values', asyn
]]; ]];
} }
if (sql.includes('FROM c_templates st')) { if (sql.includes('FROM c_templates st')) {
assert.deepEqual(params, [[33]]); assert.deepEqual(params, [[33, 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' }]]; 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')) { if (sql.includes('FROM c_template_regions')) {
assert.deepEqual(params, [[33]]); assert.deepEqual(params, [[33, 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' }]]; 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}`); throw new Error(`unexpected query: ${sql}`);
@@ -111,6 +127,8 @@ test('buildScreenPlaylist assembles slides, templates, and derived values', asyn
assert.equal(payload.slides[0].disable_audio, true); 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.disable_audio, true);
assert.equal(payload.slides[0].content.videoRegion.cache_bust, '2026-08-03T00:00:01.000Z'); assert.equal(payload.slides[0].content.videoRegion.cache_bust, '2026-08-03T00:00:01.000Z');
assert.equal(payload.slides[1].disable_audio, true);
assert.equal(payload.slides[1].content.rtmpRegion.disable_audio, true);
assert.equal(typeof payload.slides[0].content.qrRegion.qr_preview, 'string'); assert.equal(typeof payload.slides[0].content.qrRegion.qr_preview, 'string');
assert.match(payload.slides[0].content.qrRegion.qr_preview, /^data:image\/svg\+xml/); assert.match(payload.slides[0].content.qrRegion.qr_preview, /^data:image\/svg\+xml/);
assert.equal(payload.slides[0].scheduleRules.length, 2); assert.equal(payload.slides[0].scheduleRules.length, 2);
+57
View File
@@ -0,0 +1,57 @@
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 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);
});
+27
View File
@@ -18,6 +18,33 @@ test('createStyledQrCodeSvg renders a styled qr svg for non-empty values', async
assert.match(svg, /<svg[\s>]/); assert.match(svg, /<svg[\s>]/);
}); });
test('createStyledQrCodeSvg keeps the qr background solid when gradient fields are provided', async () => {
const svg = await createStyledQrCodeSvg({
value: 'https://example.com',
qr_background_color_mode: 'gradient',
qr_background_gradient_type: 'radial',
qr_background_gradient_color_1: '#ff0000',
qr_background_gradient_color_2: '#0000ff'
});
assert.equal(typeof svg, 'string');
assert.match(svg, /<svg[\s>]/);
assert.doesNotMatch(svg, /<linearGradient|<radialGradient/i);
});
test('buildQrCodeContent forces qr background to single colour mode', async () => {
const content = await buildQrCodeContent({
value: 'https://example.com',
qr_background_color_mode: 'gradient',
qr_background_gradient_type: 'radial',
qr_background_gradient_color_1: '#ff0000',
qr_background_gradient_color_2: '#0000ff'
});
assert.equal(content.qr_background_color_mode, 'single');
assert.equal(content.qr_background_use_gradient, false);
});
test('createStyledQrCodeDataUrl renders a styled qr data url for non-empty values', async () => { test('createStyledQrCodeDataUrl renders a styled qr data url for non-empty values', async () => {
const dataUrl = await createStyledQrCodeDataUrl('https://example.com'); const dataUrl = await createStyledQrCodeDataUrl('https://example.com');