187 lines
5.2 KiB
JavaScript
187 lines
5.2 KiB
JavaScript
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');
|
|
|
|
function loadAnnouncementTemplateHelpers() {
|
|
const sharedScriptPath = path.join(__dirname, '..', 'src', 'player', 'rendering-shared.js');
|
|
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-announcement-templates.js');
|
|
const sharedScript = fs.readFileSync(sharedScriptPath, 'utf8');
|
|
const script = fs.readFileSync(scriptPath, 'utf8');
|
|
|
|
const sandbox = {
|
|
window: {},
|
|
console
|
|
};
|
|
sandbox.window = sandbox;
|
|
|
|
vm.runInNewContext(sharedScript, sandbox, { filename: sharedScriptPath });
|
|
vm.runInNewContext(script, sandbox, { filename: scriptPath });
|
|
return sandbox;
|
|
}
|
|
|
|
function createTextNode(width) {
|
|
return {
|
|
width: width,
|
|
attributes: {},
|
|
cloneNode() {
|
|
return createTextNode(width);
|
|
},
|
|
setAttribute(name, value) {
|
|
this.attributes[name] = String(value);
|
|
},
|
|
getBoundingClientRect() {
|
|
return { width: this.width };
|
|
}
|
|
};
|
|
}
|
|
|
|
test('announcement marquee helper repeats enough content and measures a real cycle width', () => {
|
|
const sandbox = loadAnnouncementTemplateHelpers();
|
|
assert.equal(typeof sandbox.initPlayerAnnouncementMarquee, 'function');
|
|
|
|
const textNodes = [createTextNode(100), createTextNode(100)];
|
|
const track = {
|
|
dataset: {},
|
|
style: {
|
|
values: {},
|
|
setProperty(name, value) {
|
|
this.values[name] = String(value);
|
|
}
|
|
},
|
|
attributes: { 'data-announcement-marquee-rate': '50' },
|
|
parentElement: null,
|
|
appended: [],
|
|
getAttribute(name) {
|
|
return this.attributes[name];
|
|
},
|
|
querySelectorAll() {
|
|
return textNodes.slice();
|
|
},
|
|
appendChild(node) {
|
|
this.appended.push(node);
|
|
textNodes.push(node);
|
|
return node;
|
|
},
|
|
getBoundingClientRect() {
|
|
const gap = 20;
|
|
const width = textNodes.length * 100 + Math.max(0, textNodes.length - 1) * gap;
|
|
return { width: width };
|
|
}
|
|
};
|
|
|
|
const messageContainer = {
|
|
getBoundingClientRect() {
|
|
return { width: 420 };
|
|
}
|
|
};
|
|
|
|
sandbox.window.getComputedStyle = function () {
|
|
return { columnGap: '20px', gap: '20px' };
|
|
};
|
|
|
|
sandbox.initPlayerAnnouncementMarquee(track, { container: messageContainer });
|
|
|
|
assert.equal(track.dataset.marqueeMeasured, 'true');
|
|
assert.equal(track.style.values['--announcement-scroll-distance'], '120.00px');
|
|
assert.equal(track.style.values['--announcement-scroll-duration'], '8.00s');
|
|
assert.equal(track.style.animationDuration, '8.00s');
|
|
assert.ok(track.appended.length >= 3);
|
|
});
|
|
|
|
test('announcement markup strips line breaks from the message', () => {
|
|
const sandbox = loadAnnouncementTemplateHelpers();
|
|
sandbox.playerAnnouncementTemplates = {
|
|
topBanner: '<div class="message">{{MESSAGE}}</div>'
|
|
};
|
|
|
|
const markup = sandbox.renderPlayerAnnouncementMarkup({
|
|
announcement_type: 'top-banner',
|
|
message: 'First line\nSecond line\r\nThird line',
|
|
short_label: 'Breaking News',
|
|
color_key: 'primary',
|
|
icon_key: 'info-circle'
|
|
});
|
|
|
|
assert.match(markup, /First line Second line Third line/);
|
|
assert.doesNotMatch(markup, /<br\s*\/?\s*>/i);
|
|
});
|
|
|
|
test('announcement fallback polling stops when the websocket opens and resumes when it closes', () => {
|
|
const intervals = new Map();
|
|
const listeners = new Map();
|
|
let nextTimerId = 1;
|
|
let announcementSocket = null;
|
|
let announcementRequests = 0;
|
|
|
|
class MockWebSocket {
|
|
constructor() {
|
|
this.readyState = MockWebSocket.CONNECTING;
|
|
announcementSocket = this;
|
|
}
|
|
|
|
open() {
|
|
this.readyState = MockWebSocket.OPEN;
|
|
if (this.onopen) this.onopen();
|
|
}
|
|
|
|
close() {
|
|
this.readyState = MockWebSocket.CLOSED;
|
|
if (this.onclose) this.onclose();
|
|
}
|
|
}
|
|
MockWebSocket.CONNECTING = 0;
|
|
MockWebSocket.OPEN = 1;
|
|
MockWebSocket.CLOSED = 3;
|
|
|
|
class MockXhr {
|
|
open() {}
|
|
setRequestHeader() {}
|
|
send() { announcementRequests += 1; }
|
|
}
|
|
|
|
const window = {
|
|
slug: 'test',
|
|
location: { origin: 'http://localhost:8081' },
|
|
WebSocket: MockWebSocket,
|
|
setInterval(callback, delay) {
|
|
const id = nextTimerId++;
|
|
intervals.set(id, { callback: callback, delay: delay });
|
|
return id;
|
|
},
|
|
clearInterval(id) { intervals.delete(id); },
|
|
setTimeout() { return nextTimerId++; },
|
|
clearTimeout() {},
|
|
addEventListener() {}
|
|
};
|
|
const sandbox = {
|
|
window: window,
|
|
document: {
|
|
readyState: 'loading',
|
|
addEventListener(name, callback) { listeners.set(name, callback); }
|
|
},
|
|
WebSocket: MockWebSocket,
|
|
XMLHttpRequest: MockXhr,
|
|
URL,
|
|
Date,
|
|
JSON,
|
|
String,
|
|
Array,
|
|
Math
|
|
};
|
|
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'player-page-announcements.js');
|
|
vm.runInNewContext(fs.readFileSync(scriptPath, 'utf8'), sandbox, { filename: scriptPath });
|
|
|
|
listeners.get('DOMContentLoaded')();
|
|
assert.equal(announcementRequests, 0);
|
|
assert.equal(intervals.size, 1);
|
|
assert.equal(Array.from(intervals.values())[0].delay, 15000);
|
|
|
|
announcementSocket.open();
|
|
assert.equal(announcementRequests, 1);
|
|
assert.equal(intervals.size, 0);
|
|
|
|
announcementSocket.close();
|
|
assert.equal(intervals.size, 1);
|
|
}); |