const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const slideFormEditorSource = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-form-editor.js'), 'utf8'); const slideFormSource = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-form.js'), 'utf8'); const slideFormTemplateSource = fs.readFileSync(require.resolve('../src/web/views/signage/slides/form.hbs'), 'utf8'); const mediaPickerSource = fs.readFileSync(require.resolve('../src/web/public/js/shared/ui/media-picker.js'), 'utf8'); const slideThumbnailsSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnails.js'), 'utf8'); const slideHtmlRegionSource = fs.readFileSync(require.resolve('../src/web/public/js/regions/type/html.js'), 'utf8'); const slideWebpageRegionSource = fs.readFileSync(require.resolve('../src/web/public/js/regions/type/webpage.js'), 'utf8'); test('slide editor disables pasted data images in TinyMCE', () => { assert.ok(slideFormEditorSource.includes('paste_data_images: false')); assert.ok(slideFormEditorSource.includes('paste_block_drop: true')); assert.ok(slideFormEditorSource.includes('resize: false')); }); test('slide editor enables server-backed image uploads', () => { assert.ok(slideFormEditorSource.includes("plugins: 'lists code advlist fullscreen table image'")); assert.ok(slideFormEditorSource.includes('automatic_uploads: true')); assert.ok(slideFormEditorSource.includes('images_file_types: imageUploadFileTypes')); assert.ok(slideFormEditorSource.includes('images_upload_handler: uploadEditorImage')); assert.ok(slideFormEditorSource.includes('table image chip | fullscreen')); assert.ok(slideFormEditorSource.includes('relative_urls: false')); assert.ok(slideFormEditorSource.includes('remove_script_host: false')); }); test('slide editor keeps normal TinyMCE image insertion unchanged', () => { assert.doesNotMatch(slideFormEditorSource, /bindPlaceholderImageSupport|placeholderFallbackSource/); assert.ok(slideFormEditorSource.includes('images_upload_handler: uploadEditorImage')); }); test('slide editor preserves player-local media paths and image placeholders without web-editor URL prefixes', () => { assert.ok(slideFormEditorSource.includes('convert_urls: false')); assert.doesNotMatch(slideFormEditorSource, /convert_urls:\s*true/); }); test('slide editor inserts tables with zero padding and spacing by default', () => { assert.ok(slideFormEditorSource.includes("table_default_attributes: {")); assert.ok(slideFormEditorSource.includes("cellpadding: '0'")); assert.ok(slideFormEditorSource.includes("cellspacing: '0'")); assert.ok(slideFormEditorSource.includes('td, th { border: 1px solid currentColor; padding: 0; vertical-align: top; }')); }); test('slide editor uses a smaller wysiwyg image limit', () => { assert.ok(slideFormEditorSource.includes('var imageUploadMaxBytes = Math.max(1, Number(settings.imageUploadMaxBytes || 2 * 1024 * 1024));')); assert.ok(slideFormEditorSource.includes('Image must be ')); assert.ok(slideFormEditorSource.includes('Larger images should use the dedicated Image region.')); assert.ok(slideFormEditorSource.includes("xhr.setRequestHeader('X-Upload-Context', imageUploadContext);")); }); test('slide editor tracks uploaded image paths for cleanup', () => { assert.ok(slideFormEditorSource.includes('var editorImageUploadPaths = new Set();')); assert.ok(slideFormEditorSource.includes('getImageUploadCleanupPaths')); assert.ok(slideFormEditorSource.includes('getCommittedImageUploadCleanupPaths')); assert.ok(slideFormEditorSource.includes('getPendingImageUploadPaths')); assert.ok(slideFormEditorSource.includes('clearImageUploadPaths')); }); test('slide editor marks the owning form dirty after TinyMCE edits', () => { assert.ok(slideFormEditorSource.includes("var markFormDirty = typeof settings.markFormDirty === 'function' ? settings.markFormDirty : function () {};")); assert.match(slideFormEditorSource, /if \(hydrated\)\s*\{\s*markFormDirty\(\);/); assert.match(slideFormSource, /markFormDirty:\s*function \(\)\s*\{\s*slideForm\.dataset\.dirty = 'true';/); }); test('slide editor keeps image-only rich text from being treated as empty', () => { assert.ok(slideFormEditorSource.includes('/ { assert.ok(slideFormSource.includes('getImageUploadCleanupPaths')); assert.ok(slideFormSource.includes('getCommittedImageUploadCleanupPaths')); assert.ok(slideFormSource.includes("regionMediaController.queueUploadCleanup(slideFormEditorController.getImageUploadCleanupPaths())")); }); test('media library selections keep image and video remove controls', () => { assert.ok(mediaPickerSource.includes("var removeAction = isVideo ? 'region-video' : isQrImage ? 'region-qr-image' : 'region-image';")); assert.ok(mediaPickerSource.includes('data-remove-\' + removeAction')); assert.ok(mediaPickerSource.includes('slide-image-region-preview-remove')); assert.ok(mediaPickerSource.includes("document.querySelectorAll('.modal.show')")); assert.ok(mediaPickerSource.includes('nestedParentFocusTrap.deactivate()')); assert.ok(mediaPickerSource.includes('pickerZIndex - 5')); const pickerModal = fs.readFileSync(require.resolve('../src/web/views/shared/media-picker-modal.hbs'), 'utf8'); assert.ok(pickerModal.indexOf('data-media-picker-confirm') < pickerModal.indexOf('data-media-picker-cancel')); assert.match(pickerModal, /class="btn btn-warning" data-media-picker-cancel>Cancel/); }); test('slide thumbnail previews treat image-only text as visible content', () => { assert.ok(slideThumbnailsSource.includes('/ { const popupPreviewSource = fs.readFileSync(require.resolve('../src/web/views/signage/slides/popup-preview.hbs'), 'utf8'); const stylesheet = fs.readFileSync(require.resolve('../src/web/public/css/theme-custom.css'), 'utf8'); assert.ok(popupPreviewSource.includes('slideEditorScripts')); assert.ok(popupPreviewSource.includes('/assets/js/shared/slide-preview-renderer.js')); assert.ok(popupPreviewSource.includes('window.pulseSlidePreviewRenderer.render(payload)')); assert.ok(popupPreviewSource.includes('background: var(--bs-body-bg);')); assert.ok(popupPreviewSource.includes('color: var(--bs-body-color);')); assert.match(stylesheet, /\.slide-preview-popup-body\s*\{[^}]*background:\s*var\(--bs-body-bg\);[^}]*color:\s*var\(--bs-body-color\);/s); assert.match(stylesheet, /\.slide-preview-popup-stage\s*\{[^}]*background:\s*var\(--bs-body-bg\);/s); assert.ok(popupPreviewSource.includes('.slide-preview-chart-region canvas')); assert.ok(popupPreviewSource.includes('background: transparent !important')); assert.ok(popupPreviewSource.includes('#popup-preview-canvas iframe')); assert.ok(popupPreviewSource.includes('#popup-preview-canvas .slide-preview-webpage-region iframe')); assert.ok(popupPreviewSource.includes('#popup-preview-canvas .slide-preview-html-region iframe')); }); test('slide preview shrinks proportionally without adding a nested scrollbar', () => { const stylesheet = fs.readFileSync(require.resolve('../src/web/public/css/theme-custom.css'), 'utf8'); assert.match(stylesheet, /\.slide-editor-shell \.slide-preview-stage\s*\{[^}]*max-height:\s*max\(12rem,\s*calc\(100vh\s*-\s*22rem\)\);/s); assert.doesNotMatch(stylesheet, /\.slide-editor-shell \.slide-preview-scroll\s*\{[^}]*overflow:\s*auto;/s); assert.match(slideFormSource, /if \(!Number\.isFinite\(maxHeight\)\)\s*\{\s*slidePreviewStage\.style\.removeProperty\('width'\);\s*slidePreviewStage\.style\.removeProperty\('margin-inline'\);\s*return;\s*\}/); assert.match(slideFormSource, /function sizePreviewStage\(canvasWidth, canvasHeight\)[\s\S]*?stageContainer\.clientWidth - parseFloat\(containerStyle\.paddingLeft\) - parseFloat\(containerStyle\.paddingRight\)[\s\S]*?Math\.min\(availableWidth, maxHeight \* canvasWidth \/ canvasHeight\)[\s\S]*?marginInline = 'auto'/); }); test('browser preview helpers normalize object-shaped HTML and webpage values', () => { assert.ok(slideHtmlRegionSource.includes('normalizePreviewValue')); assert.ok(slideWebpageRegionSource.includes('normalizePreviewValue')); }); test('webpage preview updates only through the explicit button', () => { assert.ok(slideWebpageRegionSource.includes('input-group flex-nowrap')); assert.ok(slideWebpageRegionSource.includes('data-webpage-preview-update')); assert.ok(slideFormSource.includes('input[type="url"][name^="region_webpage_"]')); assert.ok(slideFormSource.includes('requestPreviewRender();')); }); test('HTML editor shares bounded size controls without changing its content', async () => { const source = { dataset: {}, style: {}, value: '
Keep this HTML
', getAttribute: () => '708' }; let clickHandler; const templateFields = { addEventListener: (event, handler) => { clickHandler = handler; }, querySelector: (selector) => selector.includes('.html-editor-source') ? source : null, querySelectorAll: (selector) => selector === '.html-editor-source' ? [source] : [] }; const sandbox = { window: {} }; vm.runInNewContext(slideFormEditorSource.replace('export function', 'function'), sandbox); const controller = sandbox.createSlideFormEditorController({ templateFields }); await controller.renderEditors(); assert.equal(source.style.height, '360px'); function resize(action, count = 1) { const button = { getAttribute: () => action, closest: () => ({ getAttribute: () => '708' }) }; for (let index = 0; index < count; index += 1) { clickHandler({ target: { closest: () => button }, preventDefault() {} }); } } resize('increase'); assert.equal(source.style.height, '440px'); resize('decrease'); assert.equal(source.style.height, '360px'); resize('decrease', 20); assert.equal(source.style.height, '240px'); resize('increase', 20); assert.equal(source.style.height, '960px'); assert.equal(source.value, '
Keep this HTML
'); assert.match(slideHtmlRegionSource, /style="resize:none;"/); assert.match(slideHtmlRegionSource, /sandboxed iframe preview\.<\/div>'\s*\+\s*'