const test = require('node:test'); const assert = require('node:assert/strict'); require('../src/common'); const registerScreensRoutes = require('../src/web/routes/signage/screens/routes'); const { renderScreenEditPage } = require('../src/web/pages'); test('screen edit page omits player URL data', async () => { const handlers = {}; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; let renderedArgs = null; const pages = { renderScreenEditPage(screen, data, message, currentUser) { renderedArgs = { screen, data, message, currentUser }; return 'ok'; } }; registerScreensRoutes(app, { pool: { async query() { return [[]]; } }, common: { async fetchScreenById() { return { id: 7, name: 'Demo Conference', slug: 'demo-conference', playlist_id: null }; }, async fetchScreenEditData() { return { playlists: [] }; }, async fetchPlayerRegistrations() { return [ { identifier: 'player-alpha', public_base_url: 'http://alpha.example', last_seen_at: new Date().toISOString() }, { identifier: 'player-beta', public_base_url: 'http://beta.example/', last_seen_at: new Date().toISOString() } ]; }, async fetchPlayerPublicBaseUrl() { return 'http://player.example'; } }, pages, buildDashboardState: async () => ({ screens: [] }), getScreenDeleteBlockMessage: async () => '', getScreenConnections: async (slug) => { if (slug !== 'demo-conference') { return { connections: [] }; } return { connections: [ { id: 'conn-1', clientId: 'conn-1', deviceId: 'device-1', playerPublicBaseUrl: 'http://player.example/' } ] }; }, playerPublicBaseUrl: 'http://player.example', requirePermission() { return function (_req, _res, next) { next(); }; } }); const handler = handlers['/screens/:id/edit']; assert.equal(Array.isArray(handler), true); const res = { send(value) { this.body = value; }, status(code) { this.statusCode = code; return this; } }; await handler[1]({ params: { id: '7' }, query: {}, currentUser: { id: 1 } }, res, () => {}); assert.equal(res.body, 'ok'); assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'player_urls'), false); assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'launcher_downloads'), false); }); test('screen edit query route omits player URLs', async () => { const handlers = {}; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; let renderedArgs = null; const pages = { renderScreenEditPage(screen, data, message, currentUser) { renderedArgs = { screen, data, message, currentUser }; return 'ok'; } }; registerScreensRoutes(app, { pool: { async query() { return [[]]; } }, common: { async fetchScreenById() { return { id: 7, name: 'Demo Conference', slug: 'demo-conference', playlist_id: null }; }, async fetchScreenEditData() { return { playlists: [] }; }, async fetchPlayerRegistrations() { return [ { identifier: 'player-alpha', public_base_url: 'http://alpha.example', last_seen_at: new Date().toISOString() } ]; }, async fetchPlayerPublicBaseUrl() { return 'http://player.example'; } }, pages, buildDashboardState: async () => ({ screens: [] }), getScreenDeleteBlockMessage: async () => '', getScreenConnections: async () => [], playerPublicBaseUrl: 'http://player.example', requirePermission() { return function (_req, _res, next) { next(); }; } }); const handler = handlers['/screens']; assert.equal(Array.isArray(handler), true); const res = { send(value) { this.body = value; }, status(code) { this.statusCode = code; return this; } }; await handler[1]({ query: { edit: '7' }, currentUser: { id: 1 } }, res, () => {}); assert.equal(res.body, 'ok'); assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'player_urls'), false); assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'launcher_downloads'), false); }); test('screen edit page does not load player URL registrations', async () => { const handlers = {}; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; let renderedArgs = null; const now = Date.now(); const recentSeenAt = new Date(now - 10 * 1000).toISOString(); const staleSeenAt = new Date(now - 5 * 60 * 1000).toISOString(); const pages = { renderScreenEditPage(screen, data, message, currentUser) { renderedArgs = { screen, data, message, currentUser }; return 'ok'; } }; registerScreensRoutes(app, { pool: { async query() { return [[]]; } }, common: { async fetchScreenById() { return { id: 7, name: 'Demo Conference', slug: 'demo-conference', playlist_id: null }; }, async fetchScreenEditData() { return { playlists: [] }; }, async fetchPlayerRegistrations() { return [ { identifier: 'player-stale', public_base_url: 'http://stale.example', last_seen_at: staleSeenAt }, { identifier: 'player-recent', public_base_url: 'http://recent.example', last_seen_at: recentSeenAt } ]; }, async fetchPlayerPublicBaseUrl() { return 'http://player.example'; } }, pages, buildDashboardState: async () => ({ screens: [] }), getScreenDeleteBlockMessage: async () => '', getScreenConnections: async () => [], formatDashboardDate(value) { return `formatted:${String(value)}`; }, playerPublicBaseUrl: 'http://player.example', requirePermission() { return function (_req, _res, next) { next(); }; } }); const handler = handlers['/screens/:id/edit']; assert.equal(Array.isArray(handler), true); const res = { send(value) { this.body = value; }, status(code) { this.statusCode = code; return this; } }; await handler[1]({ params: { id: '7' }, query: {}, currentUser: { id: 1 } }, res, () => {}); assert.equal(res.body, 'ok'); assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'player_urls'), false); }); test('screen edit page does not render a player URL table', async () => { const html = renderScreenEditPage( { id: 7, name: 'Demo Conference', slug: 'demo-conference', playlist_id: null, slug_update_confirm_live_connection_count: 2, slug_update_confirm_message: 'Are you sure you want to update the slug? This will refresh all screens using this slug.', }, { playlists: [] }, '', { id: 1 } ); assert.doesNotMatch(html, /Player URLs/); assert.doesNotMatch(html, /player-alpha/); assert.match(html, /]+id="screen-slug"[^>]+disabled/); assert.match(html, /Slug cannot be changed after creation/); }); test('screen launcher downloads are shared and attached', async () => { const handlers = {}; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; registerScreensRoutes(app, { pool: { async query() { return [[]]; } }, common: { async fetchScreenById() { return { id: 7, name: 'Demo Conference', slug: 'demo-conference', playlist_id: null }; }, async fetchScreenEditData() { return { playlists: [] }; }, async fetchScreenPlayerUrls() { return { 'demo-conference': 'http://player.example/screen/demo-conference' }; }, async fetchPlayerPublicBaseUrl() { return 'http://player.example'; } }, pages: { renderScreenEditPage() { return 'ok'; } }, buildDashboardState: async () => ({ screens: [] }), getScreenDeleteBlockMessage: async () => '', getScreenConnections: async () => [], playerPublicBaseUrl: 'http://player.example', requirePermission() { return function (_req, _res, next) { next(); }; } }); const windowsHandler = handlers['/downloads/kiosk/pulse-signage-kiosk.bat']; assert.equal(Array.isArray(windowsHandler), true); const res = { headers: {}, statusCode: 200, set(name, value) { this.headers[name.toLowerCase()] = value; }, attachment(filename) { this.headers['content-disposition'] = `attachment; filename="${filename}"`; }, type(value) { this.headers['content-type'] = value; }, send(value) { this.body = value; }, status(code) { this.statusCode = code; return this; } }; await windowsHandler[1]({ params: { id: '7' } }, res, () => {}); assert.equal(res.statusCode, 200); assert.match(res.headers['content-disposition'], /pulse-signage-kiosk\.bat/); assert.match(res.body, /TARGET_URL=http:\/\/player\.example/); assert.match(res.body, /--kiosk/); }); test('screen launcher downloads can target an explicit player url', async () => { const handlers = {}; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; registerScreensRoutes(app, { pool: { async query() { return [[]]; } }, common: { async fetchScreenById() { return { id: 7, name: 'Demo Conference', slug: 'demo-conference', playlist_id: null }; }, async fetchScreenEditData() { return { playlists: [] }; }, async fetchPlayerPublicBaseUrl() { return 'http://player.example'; } }, pages: { renderScreenEditPage() { return 'ok'; } }, buildDashboardState: async () => ({ screens: [] }), getScreenDeleteBlockMessage: async () => '', getScreenConnections: async () => [], playerPublicBaseUrl: 'http://player.example', requirePermission() { return function (_req, _res, next) { next(); }; } }); const windowsHandler = handlers['/downloads/kiosk/pulse-signage-kiosk.bat']; assert.equal(Array.isArray(windowsHandler), true); const res = { headers: {}, statusCode: 200, set(name, value) { this.headers[name.toLowerCase()] = value; }, attachment(filename) { this.headers['content-disposition'] = `attachment; filename="${filename}"`; }, type(value) { this.headers['content-type'] = value; }, send(value) { this.body = value; }, status(code) { this.statusCode = code; return this; } }; await windowsHandler[1]({ query: { playerUrl: 'http://player-b.example/screen/demo-conference' } }, res, () => {}); assert.equal(res.statusCode, 200); assert.match(res.body, /TARGET_URL=http:\/\/player-b\.example\/screen\/demo-conference/); assert.doesNotMatch(res.body, /TARGET_URL=http:\/\/player\.example/); });