const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const Module = require('module'); const originalResolveFilename = Module._resolveFilename; Module._resolveFilename = function (request, parent, isMain, options) { if (request === '#src/data/announcement-icons') { return path.join(__dirname, '..', 'src', 'data', 'announcement-icons.js'); } if (request === '#root/package.json') { return path.join(__dirname, '..', 'package.json'); } return originalResolveFilename.call(this, request, parent, isMain, options); }; const { buildDuplicateAnnouncementName, buildDuplicateAnnouncement } = require('../src/web/routes/signage/announcements/duplicate'); const { buildAnnouncementFormViewModel } = require('../src/web/routes/signage/announcements/form-view-model'); const renderAnnouncementsPage = require('../src/web/routes/signage/announcements/list'); const announcementIcons = require('../src/data/announcement-icons'); test('announcement duplicate helper copies editable fields and resets runtime state', () => { const source = { id: 7, message: 'Maintenance starts soon.', short_label: 'Maintenance', announcement_type: 'top-banner', color_key: 'warning', icon_key: 'info-circle', duration_seconds: 120, expires_at: '2026-08-04T10:00:00.000Z', screen_ids: [2, 5] }; const duplicate = buildDuplicateAnnouncement(source, buildDuplicateAnnouncementName(source.short_label)); assert.equal(duplicate.id, null); assert.equal(duplicate.short_label, 'Copy of Maintenance'); assert.equal(duplicate.expires_at, null); assert.equal(duplicate.message, 'Maintenance starts soon.'); assert.equal(duplicate.announcement_type, 'top-banner'); assert.equal(duplicate.color_key, 'warning'); assert.equal(duplicate.icon_key, 'info-circle'); assert.equal(duplicate.duration_seconds, 120); assert.deepEqual(duplicate.screen_ids, [2, 5]); }); test('announcement duplicate form model keeps info toast variant and secondary actions', () => { const model = buildAnnouncementFormViewModel({ id: null, short_label: 'Maintenance' }, 'Review the copied values and save when ready.', { permissionKeys: ['announcements.create'] }, { messageVariant: 'info', announcementTypes: [], announcementColors: [], announcementIcons: [], announcementScreens: [] }, false); assert.equal(model.messageVariant, 'info'); assert.equal(model.showSaveSecondaryActions, true); }); test('announcement form model disables play when there are no screen targets', () => { const model = buildAnnouncementFormViewModel({ id: 7, short_label: 'Maintenance', screen_ids: [], expires_at: '2026-08-03T00:00:00.000Z' }, '', { permissionKeys: ['announcements.update'] }, { announcementTypes: [], announcementColors: [], announcementIcons: [], announcementScreens: [] }, true); assert.equal(model.actionClassName, 'btn-outline-info'); assert.equal(model.actionDisabled, true); assert.match(model.actionDisabledTitle, /Select at least one screen group/i); }); test('announcement list renders a disabled info play button without targets', () => { const html = renderAnnouncementsPage({ announcements: [ { id: 7, short_label: 'Maintenance', message: 'Maintenance starts soon.', icon_key: 'info-circle', color_key: 'warning', announcement_type: 'top-banner', duration_seconds: null, expires_at: '2026-08-03T00:00:00.000Z', screen_target_count: 0, total_screen_count: 2, screen_targets_label: '' } ], pagination: { totalItems: 1, currentPage: 1 } }, '', { permissionKeys: ['announcements.update'] }); assert.match(html, /class="btn btn-sm btn-outline-info" type="submit" disabled title="Select at least one screen group to play this announcement\."/); }); test('announcement list template includes duplicate action', () => { const template = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'signage', 'announcements', 'list.hbs'), 'utf8'); assert.match(template, /\/announcements\/{\{id\}\}\/duplicate/); assert.match(template, />Dupe<\/a>/); }); test('announcement form template uses an icon-backed type picker', () => { const template = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'signage', 'announcements', 'form.hbs'), 'utf8'); assert.match(template, /data-announcement-type-picker-toggle/); assert.match(template, /data-announcement-type-option/); assert.match(template, /data-type-icon="\{\{icon\}\}"/); }); test('announcement form template includes a searchable bootstrap icon catalog', () => { const template = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'signage', 'announcements', 'form.hbs'), 'utf8'); assert.match(template, /data-announcement-icon-search/); assert.match(template, /announcementIconCatalog/); assert.match(template, /data-announcement-icon-grid/); assert.ok(!/data-announcement-icon-search-section/.test(template)); }); test('announcement icons accept catalog-only bootstrap icon keys', () => { assert.equal(announcementIcons.normalizeAnnouncementIcon('bank'), 'bank'); assert.ok(announcementIcons.ANNOUNCEMENT_ICON_CATALOG.some(function (icon) { return icon.value === 'bank'; })); assert.equal(announcementIcons.ANNOUNCEMENT_ICON_LABELS.bank, 'Bank'); }); test('announcement form template uses a checkbox card grid for screen groups', () => { const template = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'signage', 'announcements', 'form.hbs'), 'utf8'); assert.match(template, /announcement-screen-picker/); assert.match(template, /data-announcement-screen-select-all/); assert.match(template, /data-announcement-screen-select-none/); assert.match(template, /name="screen_ids\[\]"/); assert.match(template, /announcement-screen-chip/); assert.match(template, /announcement-screen-chip__check/); assert.ok(!/announcement-screen-chip__icon/.test(template)); });