477 lines
23 KiB
JavaScript
477 lines
23 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const packageVersion = require('../package.json').version;
|
|
const renderingShared = require('../src/player/rendering-shared');
|
|
const { renderPlayerPage, renderPlayerOnboardingFormPage } = require('../src/player/render');
|
|
|
|
require('../src/common');
|
|
|
|
const timetableRegionSource = fs.readFileSync(require.resolve('../src/player/regions/timetable.js'), 'utf8');
|
|
|
|
const {
|
|
mediaKind,
|
|
normalizeSlide,
|
|
renderEditorJsContent,
|
|
renderHtmlRegionContent,
|
|
sanitizeRichText,
|
|
getAnnouncementIconsDataScript,
|
|
getPlayerRuntimeScripts
|
|
} = require('../src/player/render-helpers');
|
|
|
|
test('mediaKind classifies player media by extension', () => {
|
|
assert.equal(mediaKind('poster.PNG'), 'image');
|
|
assert.equal(mediaKind('intro.mp4'), 'video');
|
|
assert.equal(mediaKind('manual.pdf'), 'pdf');
|
|
assert.equal(mediaKind('notes.txt'), 'file');
|
|
});
|
|
|
|
test('sanitizeRichText strips unsafe content but preserves allowed markup', () => {
|
|
const html = '<div class="wrap"><a href="https://example.com" target="_blank">Link</a><script>alert(1)</script><span style="color:red">Text</span><table class="grid"><colgroup><col span="1" style="width:50%"><col span="1" style="width:50%"></colgroup><thead><tr><th scope="col">Name</th><th scope="col">Value</th></tr></thead><tbody><tr><td>Alpha</td><td>Beta</td></tr></tbody></table><img src="/media/uploads/photo.png" alt="Photo" loading="lazy" onerror="alert(1)"></div>';
|
|
|
|
assert.equal(
|
|
sanitizeRichText(html),
|
|
'<div class="wrap"><a href="https://example.com" target="_blank" rel="noreferrer noopener">Link</a><span style="color:red">Text</span><table class="grid"><colgroup><col span="1" style="width:50%"><col span="1" style="width:50%"></colgroup><thead><tr><th scope="col">Name</th><th scope="col">Value</th></tr></thead><tbody><tr><td>Alpha</td><td>Beta</td></tr></tbody></table><img src="/media/uploads/photo.png" alt="Photo" loading="lazy"></div>'
|
|
);
|
|
});
|
|
|
|
test('normalizeSlide normalizes nested content without mutating the source', () => {
|
|
const slide = {
|
|
id: 12,
|
|
content: {
|
|
hero: {
|
|
value: '{"headline":"Hello"}',
|
|
font_family: ' Open Sans! ',
|
|
font_size: '42',
|
|
font_color: 'not-a-color',
|
|
type: 'text'
|
|
},
|
|
footer: 'plain text'
|
|
}
|
|
};
|
|
|
|
const normalized = normalizeSlide(slide);
|
|
|
|
assert.notEqual(normalized.content, slide.content);
|
|
assert.deepEqual(normalized.content.hero, {
|
|
value: { headline: 'Hello' },
|
|
font_family: 'Open Sans',
|
|
font_size: 42,
|
|
font_color: '#000000',
|
|
type: 'text'
|
|
});
|
|
assert.deepEqual(normalized.content.footer, {
|
|
type: 'text',
|
|
value: 'plain text'
|
|
});
|
|
assert.deepEqual(slide.content.hero, {
|
|
value: '{"headline":"Hello"}',
|
|
font_family: ' Open Sans! ',
|
|
font_size: '42',
|
|
font_color: 'not-a-color',
|
|
type: 'text'
|
|
});
|
|
});
|
|
|
|
test('server and browser slide normalization stay in sync', () => {
|
|
const browserContext = { window: {} };
|
|
const sharedRenderingSource = fs.readFileSync(require.resolve('../src/player/rendering-shared.js'), 'utf8');
|
|
const browserRenderingSource = fs.readFileSync(require.resolve('../src/player/public/js/player-page-rendering.js'), 'utf8');
|
|
const slide = {
|
|
content: {
|
|
hero: {
|
|
value: '{"headline":"Hello"}',
|
|
font_family: ' Open Sans! ',
|
|
font_size: '42',
|
|
font_color: 'not-a-color',
|
|
type: 'text'
|
|
}
|
|
}
|
|
};
|
|
vm.runInNewContext(sharedRenderingSource, browserContext);
|
|
vm.runInNewContext(browserRenderingSource, browserContext);
|
|
|
|
assert.equal(JSON.stringify(browserContext.normalizeSlide(slide)), JSON.stringify(normalizeSlide(slide)));
|
|
});
|
|
|
|
test('renderEditorJsContent sanitizes editor blocks and wraps legacy text', () => {
|
|
const editorJson = {
|
|
blocks: [
|
|
{ type: 'header', data: { level: 2, text: '<strong>Title</strong><script>bad()</script>' } },
|
|
{ type: 'paragraph', data: { text: '<a href="javascript:alert(1)">bad</a><em>ok</em>' } },
|
|
{ type: 'list', data: { style: 'ordered', items: ['One', { text: '<span>Two</span>' }] } }
|
|
]
|
|
};
|
|
|
|
assert.equal(
|
|
renderEditorJsContent(editorJson),
|
|
'<h2><strong>Title</strong></h2><p><a>bad</a><em>ok</em></p><ol style="list-style-type:decimal;padding-left:1.4em;"><li>One</li><li><span>Two</span></li></ol>'
|
|
);
|
|
assert.equal(renderEditorJsContent('plain text'), '<p>plain text</p>');
|
|
});
|
|
|
|
test('region animation decoration preserves plain markup and annotates animated roots', () => {
|
|
const plainMarkup = '<div class="template-region">Text</div>';
|
|
const animationConfig = { intro: { preset: 'fadeIn', duration_ms: 700 } };
|
|
|
|
assert.equal(renderingShared.decorateRegionMarkup(plainMarkup, { animationConfig: {} }), plainMarkup);
|
|
assert.equal(
|
|
renderingShared.decorateRegionMarkup(plainMarkup, { animationConfig: animationConfig }),
|
|
'<div class="template-region" data-animation-json="{"intro":{"preset":"fadeIn","duration_ms":700}}">Text</div>'
|
|
);
|
|
|
|
const browserContext = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/player/rendering-shared.js'), 'utf8'), browserContext);
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/player/public/js/player-page-rendering.js'), 'utf8'), browserContext);
|
|
browserContext.getTemplateRenderPlan = function () {
|
|
return {
|
|
layout: {
|
|
regions: [{ regionKey: 'hero', regionType: 'text', animationJson: JSON.stringify(animationConfig) }],
|
|
canvasWidth: 1920,
|
|
canvasHeight: 1080,
|
|
backgroundColor: '#000000',
|
|
backgroundGradient: '',
|
|
background: ''
|
|
},
|
|
renderRegion: function () { return plainMarkup; }
|
|
};
|
|
};
|
|
browserContext.normalizePlayerAnimationConfig = function (value) { return JSON.parse(value); };
|
|
browserContext.selectPlayerMediaSource = function () { return ''; };
|
|
browserContext.setPlayerCanvasDimensions = function () {};
|
|
browserContext.buildBackdropStyle = function () { return ''; };
|
|
browserContext.renderSlideShell = function (_slide, _canvasClass, _width, _height, content) { return content; };
|
|
|
|
const renderedSlide = browserContext.renderTemplateSlideMarkup({ template: {}, content: {} });
|
|
assert.match(renderedSlide, /data-animation-json=/);
|
|
});
|
|
|
|
test('browser rendering helpers select media and expose playlist revision and canvas helpers', () => {
|
|
const styles = {};
|
|
const browserContext = {
|
|
window: {},
|
|
document: {
|
|
documentElement: {
|
|
style: {
|
|
setProperty: function (key, value) { styles[key] = value; }
|
|
}
|
|
}
|
|
},
|
|
currentPlaylistEtag: '"cached-revision"'
|
|
};
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/player/rendering-shared.js'), 'utf8'), browserContext);
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/player/public/js/player-page-rendering.js'), 'utf8'), browserContext);
|
|
|
|
assert.equal(browserContext.selectPlayerMediaSource('/media/original.jpg', [
|
|
{ url: '/media/small.jpg', width: 400 },
|
|
{ url: '/media/large.jpg', width: 1200 }
|
|
], 800), '/media/large.jpg');
|
|
assert.equal(browserContext.getPlaylistRevision({ revision: 'new-revision' }), 'new-revision');
|
|
assert.equal(browserContext.getPlaylistRevision({}), 'cached-revision');
|
|
browserContext.setPlayerCanvasDimensions(1920, 1080);
|
|
assert.deepEqual(styles, {
|
|
'--player-canvas-width': '1920px',
|
|
'--player-canvas-height': '1080px'
|
|
});
|
|
});
|
|
|
|
test('server and browser rich-text rendering stay in sync', () => {
|
|
const browserContext = {
|
|
window: {}
|
|
};
|
|
const sharedRenderingSource = fs.readFileSync(require.resolve('../src/player/rendering-shared.js'), 'utf8');
|
|
const browserRenderingSource = fs.readFileSync(require.resolve('../src/player/public/js/player-page-rendering.js'), 'utf8');
|
|
vm.runInNewContext(sharedRenderingSource, browserContext);
|
|
vm.runInNewContext(browserRenderingSource, browserContext);
|
|
|
|
const value = {
|
|
blocks: [{
|
|
type: 'paragraph',
|
|
data: { text: '<table><tbody class="body"><tr><td>Cell</td></tr></tbody></table>' }
|
|
}]
|
|
};
|
|
|
|
assert.equal(browserContext.renderEditorJsContent(value), renderEditorJsContent(value));
|
|
});
|
|
|
|
test('shared API item selection treats object paths as one item and preserves array/root behavior', () => {
|
|
const helpers = require('../src/web/public/js/shared/placeholder-utils');
|
|
const campaign = { name: 'Campaign', amount_raised: { value: '200' }, goal: { value: '1000' } };
|
|
const response = { data: campaign };
|
|
assert.deepEqual(helpers.resolveApiItems(response, 'data'), [campaign]);
|
|
assert.deepEqual(helpers.resolveApiItems(response, ''), [response]);
|
|
assert.deepEqual(helpers.resolveApiItems(response, 'missing'), []);
|
|
assert.deepEqual(helpers.resolveApiItems(response, 'data.name'), []);
|
|
assert.deepEqual(helpers.resolveApiItems(response, 'data.absent.nested'), []);
|
|
assert.deepEqual(helpers.resolveApiItems({ data: null }, 'data'), []);
|
|
assert.deepEqual(helpers.resolveApiItems({ data: {} }, 'data'), [{}]);
|
|
const rows = [campaign];
|
|
for (const key of ['items', 'results', 'data']) {
|
|
assert.equal(helpers.resolveApiItems({ [key]: rows }, ''), rows);
|
|
assert.equal(helpers.resolveApiItems({ [key]: rows }, key), rows);
|
|
}
|
|
assert.equal(helpers.resolveApiItems(rows, ''), rows);
|
|
assert.deepEqual(helpers.resolveApiItems({ data: rows }, 'data[0]'), [campaign]);
|
|
const sandbox = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), sandbox);
|
|
assert.equal(JSON.stringify(sandbox.window.placeholderUtils.resolveApiItems(response, 'data')), JSON.stringify([campaign]));
|
|
});
|
|
|
|
test('shared API item sorting supports nested numeric and text fields with stable ties and missing values last', () => {
|
|
const helpers = require('../src/web/public/js/shared/placeholder-utils');
|
|
const rows = [
|
|
{ name: 'First', stats: { kills: 8 } },
|
|
{ name: 'Second', stats: { kills: 12 } },
|
|
{ name: 'Third', stats: { kills: 12 } },
|
|
{ name: 'Missing' }
|
|
];
|
|
assert.deepEqual(helpers.sortApiItems(rows, 'stats.kills', 'desc').map((item) => item.name), ['Second', 'Third', 'First', 'Missing']);
|
|
assert.deepEqual(helpers.sortApiItems(rows, 'name', 'asc').map((item) => item.name), ['First', 'Missing', 'Second', 'Third']);
|
|
assert.deepEqual(helpers.sortApiItems(rows, '', 'desc'), rows);
|
|
assert.deepEqual(helpers.sortApiItems(rows, 'stats.kills', 'asc').map((item) => item.name), ['First', 'Second', 'Third', 'Missing']);
|
|
});
|
|
|
|
test('API editor and player resolve object paths, root overrides, and numeric placeholders consistently', () => {
|
|
let editor;
|
|
const campaign = { name: 'Campaign', amount_raised: { value: '200' }, goal: { value: '1000' } };
|
|
const response = { data: campaign };
|
|
const sources = [{ id: 4, items_path: 'data', responseJson: response }];
|
|
const context = {
|
|
window: {
|
|
pulseRegionTypes: { register(_type, definition) { editor = definition; } },
|
|
pulsePlayerRegionTypes: { register() {} }
|
|
},
|
|
initialData: { apiSources: sources },
|
|
escapeHtml: String
|
|
};
|
|
for (const file of ['../src/web/public/js/shared/placeholder-utils.js', '../src/web/public/js/regions/type/api.js', '../src/player/regions/api.js']) {
|
|
vm.runInNewContext(fs.readFileSync(require.resolve(file), 'utf8'), context);
|
|
}
|
|
assert.equal(editor.getItems(4, sources)[0], campaign);
|
|
assert.equal(editor.getItemCount(4, sources), 1);
|
|
assert.equal(context.getApiItem(4, 1), campaign);
|
|
assert.equal(context.getApiItem(4, 2), null);
|
|
assert.equal(editor.getItems(4, sources, '')[0], response);
|
|
assert.equal(context.getApiItem(4, 1, ''), response);
|
|
assert.equal(editor.getItemCount(4, sources, 'missing'), 0);
|
|
assert.equal(context.getApiItem(4, 1, 'missing'), null);
|
|
assert.equal(context.substituteApiVariables('{{amount_raised.value}}', campaign, 4), '200');
|
|
assert.match(context.substituteApiVariables('{{progress(amount_raised.value,goal.value)}}', campaign, 4), /width:20%/);
|
|
});
|
|
|
|
test('shared API progress placeholder renders a clamped bar from numeric fields', () => {
|
|
const sandbox = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), sandbox);
|
|
const placeholderUtils = sandbox.window.placeholderUtils;
|
|
const markup = placeholderUtils.renderProgressPlaceholder(
|
|
{ fundraising: { current: '$1,250.00', total: '$1,000.00' } },
|
|
'progress(fundraising.current,fundraising.total,success,light,striped,animated)'
|
|
);
|
|
|
|
assert.match(markup, /<span class="progress api-progress" style="background-color:#f8f9fa;font-size:inherit;--bs-progress-font-size:inherit;--bs-progress-height:1em;height:1em;border-radius:var\(--bs-border-radius\);"/);
|
|
assert.match(markup, /class="progress-bar bg-success progress-bar-striped progress-bar-animated"/);
|
|
assert.match(markup, /style="width:100%;color:inherit;background-color:#198754;"/);
|
|
assert.match(markup, /aria-valuenow="100"/);
|
|
assert.match(markup, /width:100%/);
|
|
|
|
const objectValueMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{ fundraising: { current: { value: 30, currency: 'USD' }, total: { amount: 100, currency: 'USD' } } },
|
|
'progress(fundraising.current,fundraising.total)'
|
|
);
|
|
assert.match(objectValueMarkup, /aria-valuenow="30"/);
|
|
assert.match(objectValueMarkup, /width:30%/);
|
|
|
|
const announcementColorMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{ current: 25, total: 100 },
|
|
'progress(current,total,midnight)'
|
|
);
|
|
assert.match(announcementColorMarkup, /background-color:#1e1d2d/);
|
|
assert.doesNotMatch(announcementColorMarkup, /bg-primary/);
|
|
|
|
const omittedOptionsMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{ current: 25, total: 100 },
|
|
'progress(current,total)'
|
|
);
|
|
assert.match(omittedOptionsMarkup, /aria-valuenow="25"/);
|
|
|
|
const literalMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{},
|
|
'progress("30","100")'
|
|
);
|
|
assert.match(literalMarkup, /aria-valuenow="30"/);
|
|
});
|
|
|
|
test('progress placeholders ignore rich-text formatting inside numeric and field arguments', () => {
|
|
const sandbox = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), sandbox);
|
|
const placeholderUtils = sandbox.window.placeholderUtils;
|
|
const literal = 'progress(<span style="font-family: \'Ubuntu Regular\', Helvetica, sans-serif; font-size: 96px; color: #ffffff;">200</span>,<span style="font-family: \'Ubuntu Regular\', Helvetica, sans-serif; font-size: 96px; color: #ffffff;">1000</span>,#F47B20,#2D2D2D,pill)';
|
|
assert.equal(placeholderUtils.isProgressPlaceholderExpression(literal), true);
|
|
const markup = placeholderUtils.renderProgressPlaceholder({}, literal);
|
|
assert.match(markup, /aria-valuenow="20"/);
|
|
assert.match(markup, /width:20%/);
|
|
assert.match(markup, /background-color:#f47b20/);
|
|
assert.match(markup, /background-color:#2d2d2d/);
|
|
assert.doesNotMatch(markup, /Ubuntu|font-size:96px/);
|
|
const fields = placeholderUtils.renderProgressPlaceholder(
|
|
{ raised: { value: 200 }, target: { amount: 1000 } },
|
|
'progress(<strong><span>raised.value</span></strong>,<em>target.amount</em>,pill)'
|
|
);
|
|
assert.match(fields, /aria-valuenow="20"/);
|
|
});
|
|
|
|
test('shared placeholder transforms support arithmetic', () => {
|
|
const sandbox = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(require.resolve('../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), sandbox);
|
|
const placeholderUtils = sandbox.window.placeholderUtils;
|
|
|
|
assert.equal(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression({ amount: 12 }, 'amount.multiply(10)')), '120');
|
|
assert.equal(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression({ amount: 12 }, 'amount.add(3)')), '15');
|
|
assert.equal(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression({ amount: 12 }, 'amount.subtract(3)')), '9');
|
|
assert.equal(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression({ amount: 12 }, 'amount.divide(3)')), '4');
|
|
assert.equal(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression({ amount: 12 }, 'amount.add(3).multiply(10)')), '150');
|
|
|
|
const textlessMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{ current: 30, total: 100 },
|
|
'progress(current,total,textless)'
|
|
);
|
|
assert.match(textlessMarkup, /aria-label="30%"/);
|
|
assert.match(textlessMarkup, /aria-valuenow="30"/);
|
|
assert.match(textlessMarkup, /style="width:30%;color:inherit;"><\/span>/);
|
|
|
|
const radiusMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{ current: 30, total: 100 },
|
|
'progress(current,total,radius(12px))'
|
|
);
|
|
assert.match(radiusMarkup, /border-radius:12px;/);
|
|
|
|
const now = Date.now();
|
|
const timedMarkup = placeholderUtils.renderProgressPlaceholder(
|
|
{ start: new Date(now - 30 * 60 * 1000).toISOString(), end: new Date(now + 30 * 60 * 1000).toISOString() },
|
|
'progress(start,end)'
|
|
);
|
|
assert.match(timedMarkup, /aria-valuenow="50"/);
|
|
});
|
|
|
|
test('timetable region registers the timetable type', () => {
|
|
assert.ok(timetableRegionSource.includes("registry.register('timetable'"));
|
|
assert.ok(timetableRegionSource.includes("sanitizeRichText(substituteTimetableVariables(value"));
|
|
});
|
|
|
|
test('player announcement bootstrap includes catalog-only icons', () => {
|
|
assert.match(getAnnouncementIconsDataScript(), /bank/);
|
|
});
|
|
|
|
test('onboarding form page injects its configured device id', () => {
|
|
const page = renderPlayerOnboardingFormPage('device-123');
|
|
|
|
assert.match(page, /var deviceId = "device-123";/);
|
|
});
|
|
|
|
test('player runtime script URLs include content fingerprints', () => {
|
|
const page = renderPlayerPage('test', { slides: [] });
|
|
|
|
assert.match(page, new RegExp('player-script/playback\\.js\\?v=' + packageVersion.replace(/\./g, '\\.') + '-[a-f0-9]{12}'));
|
|
});
|
|
|
|
test('player runtime script assembly includes shared and region scripts once', () => {
|
|
const runtimeScripts = getPlayerRuntimeScripts();
|
|
const scriptNames = runtimeScripts.map(function (entry) { return entry[0]; });
|
|
const sharedRenderingIndex = scriptNames.indexOf('rendering-shared');
|
|
const pageRenderingIndex = scriptNames.indexOf('rendering');
|
|
const htmlRegionScript = runtimeScripts.find(function (entry) {
|
|
return entry[0].startsWith('region-') && entry[1].includes("registry.register('html'");
|
|
});
|
|
|
|
assert.ok(sharedRenderingIndex >= 0 && sharedRenderingIndex < pageRenderingIndex);
|
|
assert.ok(scriptNames.includes('placeholder-utils'));
|
|
assert.ok(scriptNames.includes('qr-code-svg'));
|
|
assert.ok(htmlRegionScript);
|
|
assert.equal(new Set(scriptNames).size, scriptNames.length);
|
|
});
|
|
|
|
test('shared iframe renderers size preview content explicitly', () => {
|
|
const playerHtmlRegionSource = fs.readFileSync(require.resolve('../src/player/regions/html.js'), 'utf8');
|
|
const playerWebpageRegionSource = fs.readFileSync(require.resolve('../src/player/regions/webpage.js'), 'utf8');
|
|
const playerRenderHelpersSource = fs.readFileSync(require.resolve('../src/player/render-helpers.js'), 'utf8');
|
|
const slideThumbnailPreviewSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnail-preview.js'), 'utf8');
|
|
const slideThumbnailsSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnails.js'), 'utf8');
|
|
|
|
assert.ok(playerHtmlRegionSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"'));
|
|
assert.ok(playerWebpageRegionSource.includes('style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"'));
|
|
assert.ok(playerRenderHelpersSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"'));
|
|
assert.ok(slideThumbnailPreviewSource.includes('slide-preview-webpage-region'));
|
|
assert.ok(slideThumbnailPreviewSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"'));
|
|
assert.ok(slideThumbnailsSource.includes('slide-preview-webpage-placeholder'));
|
|
assert.ok(slideThumbnailsSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"'));
|
|
});
|
|
|
|
test('html region helpers unwrap object-shaped values before rendering', () => {
|
|
const rendered = renderHtmlRegionContent({ html: '<div>Hi</div>' });
|
|
|
|
assert.match(rendered, /srcdoc="<!doctype html><html><head><style>html,body\{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;\}<\/style><\/head><body><div>Hi<\/div><\/body><\/html>"/);
|
|
assert.doesNotMatch(rendered, /\[object Object\]/);
|
|
});
|
|
|
|
test('shared render value normalization follows caller key order recursively', () => {
|
|
assert.equal(
|
|
renderingShared.normalizeRenderableString({ value: { content: '<strong>Hi</strong>' } }, ['value', 'text', 'html', 'content']),
|
|
'<strong>Hi</strong>'
|
|
);
|
|
assert.equal(
|
|
renderingShared.normalizeRenderableString({ value: { url: 'https://example.test' } }, ['value', 'text', 'url', 'href', 'src', 'content']),
|
|
'https://example.test'
|
|
);
|
|
assert.equal(renderingShared.normalizeRenderableString(null, ['value', 'text']), '');
|
|
});
|
|
|
|
test('shared region value normalization preserves selected values and stringifies other objects', () => {
|
|
const nestedValue = { amount: 12 };
|
|
assert.equal(renderingShared.normalizeRegionValue({ value: nestedValue }, ['value', 'text', 'html']), nestedValue);
|
|
assert.equal(renderingShared.normalizeRegionValue({ text: 'hello' }, ['value', 'text', 'html']), 'hello');
|
|
assert.equal(renderingShared.normalizeRegionValue({ amount: 12 }, ['value', 'text', 'html']), '{"amount":12}');
|
|
assert.equal(renderingShared.normalizeRegionValue(12, ['value', 'text', 'html']), 12);
|
|
const circularValue = {};
|
|
circularValue.self = circularValue;
|
|
assert.equal(renderingShared.normalizeRegionValue(circularValue, ['value', 'text', 'html']), '');
|
|
});
|
|
|
|
test('shared HTML document builder wraps fragments and preserves complete documents', () => {
|
|
assert.equal(
|
|
renderingShared.buildHtmlDocument('<main>Hi</main>'),
|
|
'<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body><main>Hi</main></body></html>'
|
|
);
|
|
assert.equal(renderingShared.buildHtmlDocument('<!doctype html><html><body>Hi</body></html>'), '<!doctype html><html><body>Hi</body></html>');
|
|
assert.equal(renderingShared.buildHtmlDocument(' '), '');
|
|
});
|
|
|
|
test('browser HTML and webpage regions preserve their distinct output modes', () => {
|
|
const modules = {};
|
|
const browserContext = {
|
|
window: {
|
|
pulsePlayerRegionTypes: {
|
|
register: function (type, regionModule) {
|
|
modules[type] = regionModule;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
[
|
|
'../src/player/rendering-shared.js',
|
|
'../src/player/public/js/player-page-rendering.js',
|
|
'../src/player/regions/html.js',
|
|
'../src/player/regions/webpage.js'
|
|
].forEach(function (scriptPath) {
|
|
vm.runInNewContext(fs.readFileSync(require.resolve(scriptPath), 'utf8'), browserContext);
|
|
});
|
|
|
|
const htmlFragment = modules.html.renderRegion({ baseStyle: '' }, { value: { content: '<strong>Hi</strong>' } });
|
|
const htmlDocument = modules.html.renderRegion({ baseStyle: '' }, { value: '<!doctype html><html><body>Hi</body></html>' });
|
|
const webpage = modules.webpage.renderRegion({ baseStyle: '', label: 'Weather' }, { value: { url: 'https://example.test' } });
|
|
|
|
assert.match(htmlFragment, /template-region-html-content/);
|
|
assert.match(htmlFragment, /<strong>Hi<\/strong>/);
|
|
assert.match(htmlDocument, /sandbox=""/);
|
|
assert.match(webpage, /src="https:\/\/example\.test"/);
|
|
}); |