Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c97fe81d1 | ||
|
|
ee3b1b51bf | ||
|
|
4e5a1bc393 |
@@ -2,6 +2,25 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 2.6.10 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- The onboarding landing page and form no longer restore a previously selected screen, so the screen picker always starts clean while still keeping the saved client name.
|
||||
|
||||
## 2.6.9 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- The connected-clients view no longer exposes or sorts by client IP, so the table stays focused on the player identity, screen, and playback state.
|
||||
- The connected-clients dashboard row renderer now keeps the actions column aligned after removing the IP column, so row updates no longer append a duplicate actions cell.
|
||||
|
||||
## 2.6.8 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- The screen-group command buttons stay disabled until a real target group is selected, so the placeholder "Select Screen Group" state cannot send commands.
|
||||
|
||||
## 2.6.7 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.6.7",
|
||||
"version": "2.6.10",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"repository": {
|
||||
|
||||
@@ -40,8 +40,7 @@ function normalizeRemoteAddress(value) {
|
||||
|
||||
function formatPlayerConnectionLabel(deviceId, remoteAddress) {
|
||||
const normalizedDeviceId = String(deviceId || '').trim() || 'unknown-player';
|
||||
const normalizedRemoteAddress = normalizeRemoteAddress(remoteAddress);
|
||||
return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId;
|
||||
return normalizedDeviceId;
|
||||
}
|
||||
|
||||
function normalizeProxyBaseUrl(value) {
|
||||
@@ -359,7 +358,7 @@ async function start() {
|
||||
return;
|
||||
}
|
||||
|
||||
logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId, socket.bridgeRemoteAddress)} has disconnected`);
|
||||
logBridge(`Player ${formatPlayerConnectionLabel(socket.playerDeviceId)} has disconnected`);
|
||||
}
|
||||
|
||||
function resolveMediaPath(fileName) {
|
||||
@@ -828,7 +827,7 @@ async function start() {
|
||||
});
|
||||
|
||||
playerSockets.set(deviceId, socket);
|
||||
logBridge(`Player ${formatPlayerConnectionLabel(deviceId, socket.bridgeRemoteAddress)} has connected`);
|
||||
logBridge(`Player ${formatPlayerConnectionLabel(deviceId)} has connected`);
|
||||
socket.send(JSON.stringify({ type: 'registered', ok: true, player: player }));
|
||||
return;
|
||||
}
|
||||
@@ -875,14 +874,12 @@ async function start() {
|
||||
}
|
||||
|
||||
if (!verifyRequestAuth(request)) {
|
||||
const remoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
|
||||
logBridge(remoteAddress ? `Player (ip ${remoteAddress}) denied with wrong shared secret` : 'Player denied with wrong shared secret');
|
||||
logBridge('Player denied with wrong shared secret');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
playersWs.handleUpgrade(request, socket, head, function (ws) {
|
||||
ws.bridgeRemoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
|
||||
playersWs.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,6 +114,7 @@ async function start() {
|
||||
Accept: 'application/json'
|
||||
}, authHeaders)
|
||||
});
|
||||
|
||||
return Boolean(response && response.ok);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
|
||||
@@ -56,10 +56,8 @@
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedClientName = getSessionStorageItem(clientNameKey) || "";
|
||||
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
|
||||
var clientNameInput = form.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput && storedClientName) { clientNameInput.value = storedClientName; }
|
||||
if (screenSelect && storedScreenSlug) { screenSelect.value = storedScreenSlug; }
|
||||
} catch (_error) {}
|
||||
});
|
||||
form.addEventListener("submit", function (event) {
|
||||
|
||||
@@ -132,14 +132,11 @@
|
||||
}
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
|
||||
var storedClientName = getSessionStorageItem(clientNameKey) || "";
|
||||
if (!storedClientName && storedScreenSlug) { storedClientName = getSessionStorageItem(getClientNameStorageKey(storedScreenSlug)) || ""; }
|
||||
if (storedClientName && localForm) {
|
||||
var clientNameInput = localForm.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput) { clientNameInput.value = storedClientName; }
|
||||
}
|
||||
if (storedScreenSlug && localScreenSelect) { localScreenSelect.value = storedScreenSlug; }
|
||||
} catch (_error) {}
|
||||
});
|
||||
redirectIfOnboarded(deviceId).then(function (redirected) {
|
||||
|
||||
+71
-14
@@ -45,6 +45,76 @@ function createPlayerRuntime(options) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
function firstHeaderValue(value) {
|
||||
return String(value || '').trim().split(',')[0].trim();
|
||||
}
|
||||
|
||||
function isPrivateOrReservedIp(ip) {
|
||||
const normalized = normalizeClientIp(ip);
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lower = normalized.toLowerCase();
|
||||
if (lower === 'localhost' || lower === '::1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^10\./.test(lower) || /^192\.168\./.test(lower) || /^127\./.test(lower) || /^169\.254\./.test(lower)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(lower)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lower.startsWith('fc') || lower.startsWith('fd') || lower.startsWith('fe80:') || lower.startsWith('::ffff:127.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function pickForwardedIp(candidates) {
|
||||
const normalizedCandidates = Array.isArray(candidates)
|
||||
? candidates.map(function (candidate) {
|
||||
return normalizeClientIp(candidate);
|
||||
}).filter(Boolean)
|
||||
: [];
|
||||
|
||||
const publicCandidate = normalizedCandidates.find(function (candidate) {
|
||||
return !isPrivateOrReservedIp(candidate);
|
||||
});
|
||||
|
||||
return publicCandidate || normalizedCandidates[0] || null;
|
||||
}
|
||||
|
||||
function resolveRequestIp(request) {
|
||||
const forwardedFor = String(request && request.headers && request.headers['x-forwarded-for'] || '').split(',');
|
||||
const forwardedForIp = pickForwardedIp(forwardedFor);
|
||||
if (forwardedForIp) {
|
||||
return forwardedForIp;
|
||||
}
|
||||
|
||||
const realIp = pickForwardedIp([firstHeaderValue(request && request.headers && request.headers['x-real-ip'])]);
|
||||
if (realIp) {
|
||||
return realIp;
|
||||
}
|
||||
|
||||
const forwarded = firstHeaderValue(request && request.headers && request.headers.forwarded);
|
||||
if (forwarded) {
|
||||
const forwardedMatches = Array.from(forwarded.matchAll(/(?:^|,\s*|;\s*)for=(?:"?\[?)([^"\];,\s]+)/gi)).map(function (match) {
|
||||
return match[1];
|
||||
});
|
||||
const forwardedIp = pickForwardedIp(forwardedMatches);
|
||||
if (forwardedIp) {
|
||||
return forwardedIp;
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeClientIp(request && request.socket && request.socket.remoteAddress);
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader) {
|
||||
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
|
||||
if (!pair) {
|
||||
@@ -145,7 +215,6 @@ function createPlayerRuntime(options) {
|
||||
const clientName = String(connection.clientName || '').trim();
|
||||
const clientId = String(connection.clientId || '').trim();
|
||||
const userAgent = String(connection.userAgent || '').trim();
|
||||
const clientIp = String(connection.clientIp || '').trim();
|
||||
const viewport = connection.viewport && typeof connection.viewport === 'object'
|
||||
? connection.viewport
|
||||
: null;
|
||||
@@ -161,10 +230,6 @@ function createPlayerRuntime(options) {
|
||||
labelParts.push(`id ${clientId.slice(-6)}`);
|
||||
}
|
||||
|
||||
if (clientIp) {
|
||||
labelParts.push(clientIp);
|
||||
}
|
||||
|
||||
if (viewport && Number.isFinite(Number(viewport.width)) && Number.isFinite(Number(viewport.height))) {
|
||||
labelParts.push(`${Number(viewport.width)}x${Number(viewport.height)}`);
|
||||
}
|
||||
@@ -198,8 +263,6 @@ function createPlayerRuntime(options) {
|
||||
blackout: Boolean(connection.blackout),
|
||||
currentSlideId: connection.currentSlide && connection.currentSlide.id ? connection.currentSlide.id : null,
|
||||
currentSlideTitle: connection.currentSlide && connection.currentSlide.title ? connection.currentSlide.title : null,
|
||||
clientIp: connection.clientIp || null,
|
||||
remoteAddress: connection.remoteAddress || null,
|
||||
connectedAt: connection.connectedAt ? connection.connectedAt.toISOString() : null,
|
||||
lastSeenAt: connection.lastSeenAt ? connection.lastSeenAt.toISOString() : null
|
||||
};
|
||||
@@ -431,9 +494,6 @@ function createPlayerRuntime(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteAddress = request.socket && request.socket.remoteAddress ? request.socket.remoteAddress : null;
|
||||
const forwardedFor = normalizeClientIp(String(request.headers['x-forwarded-for'] || '').split(',')[0]);
|
||||
const normalizedRemoteAddress = normalizeClientIp(remoteAddress);
|
||||
const connectionId = crypto.randomUUID();
|
||||
const connection = {
|
||||
id: connectionId,
|
||||
@@ -448,9 +508,7 @@ function createPlayerRuntime(options) {
|
||||
paused: false,
|
||||
blackout: false,
|
||||
playerPublicBaseUrl: null,
|
||||
clientIp: forwardedFor || normalizedRemoteAddress,
|
||||
remoteAddress: normalizedRemoteAddress,
|
||||
label: forwardedFor || normalizedRemoteAddress || 'connected client',
|
||||
label: 'connected client',
|
||||
connectedAt: new Date(),
|
||||
lastSeenAt: new Date()
|
||||
};
|
||||
@@ -491,7 +549,6 @@ function createPlayerRuntime(options) {
|
||||
}
|
||||
connection.paused = Boolean(payload.paused);
|
||||
connection.blackout = Boolean(payload.blackout);
|
||||
connection.clientIp = payload.clientIp ? normalizeClientIp(payload.clientIp) || connection.clientIp : connection.clientIp;
|
||||
connection.currentSlide = payload.currentSlide && typeof payload.currentSlide === 'object' ? {
|
||||
id: payload.currentSlide.id || null,
|
||||
title: payload.currentSlide.title || '',
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
}
|
||||
|
||||
var selectedSlug = String(select.value || '').trim();
|
||||
var hasSelectedGroup = Boolean(selectedSlug);
|
||||
var actionTarget = '/clients/' + encodeURIComponent(selectedSlug || '__all__') + '/commands';
|
||||
var selectedName = getSelectedScreenLabel();
|
||||
var selectedClients = getSelectedScreenClients(latestDashboardState);
|
||||
@@ -134,6 +135,10 @@
|
||||
var action = String(form.getAttribute('data-screen-command-action') || '').trim();
|
||||
var button = form.querySelector('button[type="submit"]');
|
||||
|
||||
if (button) {
|
||||
button.disabled = !hasSelectedGroup;
|
||||
}
|
||||
|
||||
if (action === 'pause' || action === 'blackout') {
|
||||
updateToggleButton(button, form, latestDashboardState);
|
||||
return;
|
||||
@@ -240,8 +245,6 @@
|
||||
client && client.slug,
|
||||
client && client.screen_slug,
|
||||
client && client.screen_name,
|
||||
client && client.ipAddress,
|
||||
client && client.clientIp,
|
||||
client && client.status,
|
||||
client && client.currentSlideTitle
|
||||
];
|
||||
@@ -259,7 +262,6 @@
|
||||
client: function (client) { return String(client && (getClientDisplayName(client) || client.client_name || client.name || client.clientId) || '').trim(); },
|
||||
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
|
||||
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
||||
ip: function (client) { return String(client && client.clientIp || '').trim(); },
|
||||
viewport: function (client) {
|
||||
var viewport = client && client.viewport;
|
||||
if (!viewport || !viewport.width || !viewport.height) {
|
||||
@@ -278,12 +280,6 @@
|
||||
? [normalizedSortKey]
|
||||
: ['client'];
|
||||
|
||||
if (sortKeys[0] === 'client') {
|
||||
sortKeys.push('ip');
|
||||
} else if (sortKeys[0] === 'ip') {
|
||||
sortKeys.push('client');
|
||||
}
|
||||
|
||||
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
|
||||
for (var index = 0; index < sortKeys.length; index += 1) {
|
||||
var sortKeyName = sortKeys[index];
|
||||
@@ -534,13 +530,13 @@
|
||||
}
|
||||
|
||||
if (!hasActionsColumn) {
|
||||
if (row.cells.length > 6) {
|
||||
if (row.cells.length > 5) {
|
||||
row.deleteCell(row.cells.length - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var actionCell = row.cells.length > 6 ? row.cells[6] : null;
|
||||
var actionCell = row.cells.length > 5 ? row.cells[5] : null;
|
||||
if (!actionCell) {
|
||||
actionCell = row.insertCell(-1);
|
||||
actionCell.setAttribute('data-label', 'Actions');
|
||||
@@ -552,8 +548,6 @@
|
||||
|
||||
function renderClientRow(client, hasActionsColumn) {
|
||||
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
||||
var clientIpValue = normalizeDisplayIp(client.clientIp);
|
||||
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<span class="empty">Unknown</span>';
|
||||
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
|
||||
var clientNameValue = getClientDisplayName(client);
|
||||
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
||||
@@ -566,7 +560,6 @@
|
||||
'<td data-label="Client"><div>' + clientName + '</div></td>',
|
||||
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
|
||||
'<td data-label="Current Slide">' + currentSlide + '</td>',
|
||||
'<td data-label="IP">' + clientIp + '</td>',
|
||||
'<td data-label="Viewport">' + viewport + '</td>',
|
||||
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
|
||||
actionCell,
|
||||
@@ -575,13 +568,11 @@
|
||||
}
|
||||
|
||||
function updateClientRowCells(row, client, hasActionsColumn) {
|
||||
if (!row || !row.cells || row.cells.length < 6) {
|
||||
if (!row || !row.cells || row.cells.length < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
||||
var clientIpValue = normalizeDisplayIp(client.clientIp);
|
||||
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<span class="empty">Unknown</span>';
|
||||
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
|
||||
var clientNameValue = getClientDisplayName(client);
|
||||
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
||||
@@ -598,9 +589,8 @@
|
||||
setCellHtml(row.cells[0], '<div>' + clientName + '</div>');
|
||||
setCellHtml(row.cells[1], '<div>' + screenName + '</div>');
|
||||
setCellHtml(row.cells[2], currentSlide);
|
||||
setCellHtml(row.cells[3], clientIp);
|
||||
setCellHtml(row.cells[4], viewport);
|
||||
setCellHtml(row.cells[5], connectedAt);
|
||||
setCellHtml(row.cells[3], viewport);
|
||||
setCellHtml(row.cells[4], connectedAt);
|
||||
|
||||
syncClientActionCell(row, client, hasActionsColumn);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ module.exports = function registerClientsRoutes(app, deps) {
|
||||
client: function (client) { return String(client && (client.client_name || client.name || client.clientId) || '').trim(); },
|
||||
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
|
||||
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
||||
ip: function (client) { return String(client && client.clientIp || '').trim(); },
|
||||
viewport: function (client) {
|
||||
const viewport = client && client.viewport;
|
||||
if (!viewport || !viewport.width || !viewport.height) {
|
||||
@@ -37,12 +36,6 @@ module.exports = function registerClientsRoutes(app, deps) {
|
||||
? [normalizedSortKey]
|
||||
: ['client'];
|
||||
|
||||
if (sortKeys[0] === 'client') {
|
||||
sortKeys.push('ip');
|
||||
} else if (sortKeys[0] === 'ip') {
|
||||
sortKeys.push('client');
|
||||
}
|
||||
|
||||
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
|
||||
for (let index = 0; index < sortKeys.length; index += 1) {
|
||||
const sortKeyName = sortKeys[index];
|
||||
@@ -71,7 +64,6 @@ module.exports = function registerClientsRoutes(app, deps) {
|
||||
'deviceId',
|
||||
'slug',
|
||||
'screen_slug',
|
||||
'ipAddress',
|
||||
'status'
|
||||
]);
|
||||
const filteredClients = (data.clients || []).filter(matchesSearch);
|
||||
|
||||
@@ -42,17 +42,17 @@
|
||||
<div class="screen-command-actions">
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-screen-command-form data-screen-command-action="reload" data-async-command>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger" disabled><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
||||
</form>
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-screen-command-form data-screen-command-action="pause" data-async-command>
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="paused" value="true" />
|
||||
<button type="submit" class="btn btn-sm btn-info"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
||||
<button type="submit" class="btn btn-sm btn-info" disabled><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
||||
</form>
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-screen-command-form data-screen-command-action="blackout" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="blackout" value="true" />
|
||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
||||
<button type="submit" class="btn btn-sm btn-secondary" disabled><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +88,6 @@
|
||||
<th data-table-sort-key="client">Client</th>
|
||||
<th data-table-sort-key="screen">Current Screen Group</th>
|
||||
<th data-table-sort-key="slide">Current Slide</th>
|
||||
<th data-table-sort-key="ip">IP</th>
|
||||
<th data-table-sort-key="viewport">Viewport</th>
|
||||
<th data-table-sort-key="connected">Connected/Updated</th>
|
||||
{{#if (hasPermission currentUser 'clients.allow')}}<th>Actions</th>{{/if}}
|
||||
@@ -111,7 +110,6 @@
|
||||
<span class="empty">No slide currently showing</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="IP">{{#if clientIp}}{{clientIp}}{{else}}<span class="empty">Unknown</span>{{/if}}</td>
|
||||
<td data-label="Viewport">
|
||||
{{#if viewport}}
|
||||
{{viewport.width}}x{{viewport.height}}
|
||||
@@ -169,7 +167,7 @@
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
|
||||
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}6{{else}}5{{/if}}" class="empty">No connected clients yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -342,3 +342,181 @@ test('dashboard move client button opens the move modal for the selected row', (
|
||||
assert.equal(clientNameInput.value, 'Lobby Client');
|
||||
assert.equal(playerBaseUrlInput.value, 'http://player.local');
|
||||
});
|
||||
|
||||
test('dashboard screen group controls stay disabled until a group is selected', () => {
|
||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/dashboard/dashboard-page.js'), 'utf8');
|
||||
const pauseInput = { value: 'true' };
|
||||
const blackoutInput = { value: 'true' };
|
||||
const reloadButton = {
|
||||
disabled: false,
|
||||
innerHTML: '',
|
||||
setAttribute() {},
|
||||
classList: { add() {}, remove() {} }
|
||||
};
|
||||
const pauseButton = {
|
||||
disabled: false,
|
||||
innerHTML: '',
|
||||
setAttribute() {},
|
||||
classList: { add() {}, remove() {} }
|
||||
};
|
||||
const blackoutButton = {
|
||||
disabled: false,
|
||||
innerHTML: '',
|
||||
setAttribute() {},
|
||||
classList: { add() {}, remove() {} }
|
||||
};
|
||||
const forms = [
|
||||
{
|
||||
getAttribute(name) {
|
||||
if (name === 'data-screen-command-action') {
|
||||
return 'reload';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
setAttribute() {},
|
||||
querySelector(selector) {
|
||||
if (selector === 'button[type="submit"]') {
|
||||
return reloadButton;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
getAttribute(name) {
|
||||
if (name === 'data-screen-command-action') {
|
||||
return 'pause';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
setAttribute() {},
|
||||
querySelector(selector) {
|
||||
if (selector === 'button[type="submit"]') {
|
||||
return pauseButton;
|
||||
}
|
||||
if (selector === 'input[name="paused"]') {
|
||||
return pauseInput;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
getAttribute(name) {
|
||||
if (name === 'data-screen-command-action') {
|
||||
return 'blackout';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
setAttribute() {},
|
||||
querySelector(selector) {
|
||||
if (selector === 'button[type="submit"]') {
|
||||
return blackoutButton;
|
||||
}
|
||||
if (selector === 'input[name="blackout"]') {
|
||||
return blackoutInput;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
];
|
||||
const select = {
|
||||
value: '',
|
||||
options: [
|
||||
{ textContent: 'Select Screen Group', getAttribute() { return ''; } },
|
||||
{ textContent: 'Lobby', getAttribute(name) { return name === 'data-screen-name' ? 'Lobby' : ''; } },
|
||||
{ textContent: 'All Screens', getAttribute(name) { return name === 'data-screen-name' ? 'All screens' : ''; } }
|
||||
],
|
||||
selectedIndex: 0,
|
||||
dataset: {},
|
||||
addEventListener(type, handler) {
|
||||
if (type === 'change') {
|
||||
this.changeHandler = handler;
|
||||
}
|
||||
}
|
||||
};
|
||||
const context = {
|
||||
document: {
|
||||
body: {
|
||||
classList: {
|
||||
toggle() {},
|
||||
add() {},
|
||||
remove() {}
|
||||
}
|
||||
},
|
||||
getElementById(id) {
|
||||
if (id === 'screen-command-select') {
|
||||
return select;
|
||||
}
|
||||
if (id === 'dashboard-clients-table') {
|
||||
return { getAttribute() { return 'false'; } };
|
||||
}
|
||||
if (id === 'dashboard-clients-table-body') {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === '[data-screen-command-select]') {
|
||||
return select;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '[data-screen-command-form]') {
|
||||
return forms;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
addEventListener() {}
|
||||
},
|
||||
window: {
|
||||
location: {
|
||||
search: '',
|
||||
protocol: 'http:',
|
||||
host: 'example.test'
|
||||
},
|
||||
webUiHelpers: {
|
||||
escapeHtml(value) { return String(value); },
|
||||
formatDashboardDate(value) { return String(value); },
|
||||
getClientRowKey(client) { return String(client && client.id || ''); },
|
||||
getClientDisplayName(client) { return String(client && (client.client_name || client.name || client.clientId) || ''); },
|
||||
setButtonVariant() {},
|
||||
normalizeDisplayIp(value) { return String(value); }
|
||||
},
|
||||
WebSocket: null,
|
||||
setTimeout() { return 1; },
|
||||
clearTimeout() {},
|
||||
alert() {},
|
||||
prompt() {
|
||||
return null;
|
||||
},
|
||||
webHandleDashboardState() {}
|
||||
},
|
||||
WebSocket: function MockWebSocket() {},
|
||||
JSON: JSON,
|
||||
Number: Number,
|
||||
String: String,
|
||||
Boolean: Boolean,
|
||||
Array: Array,
|
||||
Object: Object,
|
||||
Math: Math,
|
||||
Set: Set,
|
||||
URLSearchParams: URLSearchParams,
|
||||
FormData: function FormData() {}
|
||||
};
|
||||
context.window.document = context.document;
|
||||
context.window.WebSocket = context.WebSocket;
|
||||
|
||||
vm.runInNewContext(script, context);
|
||||
|
||||
assert.equal(reloadButton.disabled, true);
|
||||
assert.equal(pauseButton.disabled, true);
|
||||
assert.equal(blackoutButton.disabled, true);
|
||||
|
||||
select.value = 'lobby';
|
||||
select.selectedIndex = 1;
|
||||
select.changeHandler();
|
||||
|
||||
assert.equal(reloadButton.disabled, false);
|
||||
assert.equal(pauseButton.disabled, false);
|
||||
assert.equal(blackoutButton.disabled, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user