312 lines
12 KiB
JavaScript
312 lines
12 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const { buildSlidePayload } = require('../src/data/slides');
|
|
|
|
function loadApiRenderer(apiSources) {
|
|
let renderer;
|
|
const window = {
|
|
placeholderUtils: {},
|
|
pulsePlayerRegionTypes: {
|
|
register: function (_type, module) {
|
|
renderer = module;
|
|
}
|
|
},
|
|
pulsePlayerRenderingShared: {
|
|
normalizeRegionValue: function (value) {
|
|
return String(value || '');
|
|
}
|
|
}
|
|
};
|
|
const context = {
|
|
window,
|
|
initialData: { apiSources: apiSources },
|
|
escapeHtml: String,
|
|
sanitizeFontFamily: String,
|
|
sanitizeFontSize: function (value) { return Number(value) || 16; },
|
|
sanitizeTextColor: String,
|
|
sanitizeRichText: String,
|
|
renderEditorJsContent: String
|
|
};
|
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), context);
|
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/player/regions/api.js'), 'utf8'), context);
|
|
return renderer;
|
|
}
|
|
|
|
function loadApiEditor() {
|
|
let editor;
|
|
const window = {
|
|
pulseRegionTypes: {
|
|
register: function (_type, module) {
|
|
editor = module;
|
|
}
|
|
},
|
|
pulseRegionUtils: {
|
|
escapeHtml: String,
|
|
sanitizePreviewHtml: String,
|
|
sanitizeFontFamily: String,
|
|
sanitizeTextColor: String
|
|
}
|
|
};
|
|
const context = { window };
|
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), context);
|
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/regions/type/api.js'), 'utf8'), context);
|
|
return editor;
|
|
}
|
|
|
|
test('API region sort settings persist in slide content', async () => {
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM c_templates st')) {
|
|
return [[{ id: 8, name: 'Template 8', canvas_size_id: 1, canvas_size_width: 1920, canvas_size_height: 1080 }]];
|
|
}
|
|
if (sql.includes('FROM c_template_regions')) {
|
|
return [[{ id: 23, template_id: 8, region_key: 'apiRegion', region_type: 'api', label: 'API' }]];
|
|
}
|
|
return [[]];
|
|
}
|
|
};
|
|
|
|
const payload = await buildSlidePayload(pool, {
|
|
body: {
|
|
title: 'API Slide',
|
|
template_id: '8',
|
|
region_api_source_id_23: '11',
|
|
region_api_items_path_23: 'results',
|
|
region_api_sort_mode_23: 'field',
|
|
region_api_sort_by_23: 'stats.kills',
|
|
region_api_sort_direction_23: 'desc',
|
|
region_text_23: '{{name}}'
|
|
},
|
|
files: []
|
|
}, null);
|
|
|
|
const content = JSON.parse(payload.contentJson).apiRegion;
|
|
assert.equal(content.sort_by, 'stats.kills');
|
|
assert.equal(content.sort_direction, 'desc');
|
|
assert.equal(content.items_path, 'results');
|
|
assert.equal(content.item_number, 1);
|
|
});
|
|
|
|
test('API field sorting defaults to ascending', async () => {
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('FROM c_templates st')) {
|
|
return [[{ id: 8, name: 'Template 8', canvas_size_id: 1, canvas_size_width: 1920, canvas_size_height: 1080 }]];
|
|
}
|
|
if (sql.includes('FROM c_template_regions')) {
|
|
return [[{ id: 23, template_id: 8, region_key: 'apiRegion', region_type: 'api', label: 'API' }]];
|
|
}
|
|
return [[]];
|
|
}
|
|
};
|
|
|
|
const payload = await buildSlidePayload(pool, {
|
|
body: {
|
|
title: 'API Slide',
|
|
template_id: '8',
|
|
region_api_source_id_23: '11',
|
|
region_api_sort_mode_23: 'field',
|
|
region_api_sort_by_23: 'stats.kills',
|
|
region_text_23: '{{name}}'
|
|
},
|
|
files: []
|
|
}, null);
|
|
|
|
assert.equal(JSON.parse(payload.contentJson).apiRegion.sort_direction, 'asc');
|
|
});
|
|
|
|
test('player API region selects the requested item after sorting', () => {
|
|
const renderer = loadApiRenderer([{
|
|
id: 11,
|
|
responseJson: {
|
|
results: [
|
|
{ name: 'Ari', stats: { kills: 8 } },
|
|
{ name: 'Bea', stats: { kills: 12 } },
|
|
{ name: 'Cam', stats: { kills: 4 } }
|
|
]
|
|
}
|
|
}]);
|
|
const rendered = renderer.renderRegion({ pixelWidth: 400, pixelHeight: 100, canvasScale: 1, baseStyle: '' }, {
|
|
type: 'api',
|
|
source_id: 11,
|
|
item_number: 2,
|
|
items_path: 'results',
|
|
sort_by: 'stats.kills',
|
|
sort_direction: 'desc',
|
|
value: '{{0.name}} / {{1.name}} / {{name}}'
|
|
});
|
|
|
|
assert.match(rendered, /Bea \/ Ari \/ Ari/);
|
|
assert.doesNotMatch(rendered, /Cam/);
|
|
});
|
|
|
|
test('API editor preview resolves zero-based indexed placeholders from sorted results', () => {
|
|
const editor = loadApiEditor();
|
|
const preview = editor.renderPreview({ id: 23, region_key: 'apiRegion', fontFamily: 'Arial', fontSize: 20, fontColor: '#000000' }, {
|
|
source_id: 11,
|
|
item_number: 2,
|
|
items_path: 'results',
|
|
sort_by: 'stats.kills',
|
|
sort_direction: 'desc',
|
|
value: '{{0.name}} / {{1.name}} / {{name}}'
|
|
}, {
|
|
sources: [{
|
|
id: 11,
|
|
responseJson: {
|
|
results: [
|
|
{ name: 'Ari', stats: { kills: 8 } },
|
|
{ name: 'Bea', stats: { kills: 12 } },
|
|
{ name: 'Cam', stats: { kills: 4 } }
|
|
]
|
|
}
|
|
}],
|
|
existingContent: {}
|
|
});
|
|
|
|
assert.match(preview, /Bea \/ Ari \/ Ari/);
|
|
assert.doesNotMatch(preview, /Cam/);
|
|
});
|
|
|
|
test('image and icon placeholder bounds are hidden in the popup but remain in the editor', () => {
|
|
const editor = loadApiEditor();
|
|
const preview = editor.renderPreview({ id: 23, region_key: 'apiRegion', fontFamily: 'Arial', fontSize: 20, fontColor: '#000000' }, {
|
|
source_id: 11,
|
|
items_path: 'results',
|
|
value: '{{image.image(120,80)}}'
|
|
}, {
|
|
sources: [{ id: 11, responseJson: { results: [{ image: 'https://example.com/image.png' }] } }],
|
|
existingContent: {}
|
|
});
|
|
const popupStyles = fs.readFileSync(path.join(__dirname, '../src/web/views/signage/slides/popup-preview.hbs'), 'utf8');
|
|
const rssSource = fs.readFileSync(path.join(__dirname, '../src/web/public/js/regions/type/rss.js'), 'utf8');
|
|
const weatherSource = fs.readFileSync(path.join(__dirname, '../src/web/public/js/regions/type/weather.js'), 'utf8');
|
|
const weatherIconsSource = fs.readFileSync(path.join(__dirname, '../src/web/public/js/shared/weather-icons.js'), 'utf8');
|
|
|
|
assert.match(preview, /class="api-image-placeholder-bounds slide-preview-image-placeholder-bounds"/);
|
|
assert.match(preview, /border:1px dashed/);
|
|
assert.match(rssSource, /class="slide-preview-image-placeholder-bounds"/);
|
|
assert.match(weatherSource, /class="slide-preview-image-placeholder-bounds"/);
|
|
assert.match(weatherIconsSource, /class="slide-preview-image-placeholder-bounds"/);
|
|
assert.match(popupStyles, /#popup-preview-canvas \.slide-preview-image-placeholder-bounds,[\s\S]*?#popup-preview-canvas \.api-image-placeholder-bounds\s*\{\s*border:\s*0 !important;[\s\S]*?outline:\s*0 !important;[\s\S]*?box-shadow:\s*none !important;/);
|
|
});
|
|
|
|
test('API region plain placeholders work with a single object response', () => {
|
|
const renderer = loadApiRenderer([{
|
|
id: 11,
|
|
responseJson: { name: 'Pikachu', type: 'Electric' }
|
|
}]);
|
|
const rendered = renderer.renderRegion({ pixelWidth: 400, pixelHeight: 100, canvasScale: 1, baseStyle: '' }, {
|
|
type: 'api',
|
|
source_id: 11,
|
|
value: '{{name}} - {{type}}'
|
|
});
|
|
|
|
assert.match(rendered, /Pikachu - Electric/);
|
|
});
|
|
|
|
test('API region results panel exposes its open control without resize buttons', () => {
|
|
const editor = loadApiEditor();
|
|
const markup = editor.renderEditorCard({
|
|
region: { id: 23 },
|
|
current: { value: '{{name}}' },
|
|
config: { source_id: 11, item_number: 1, items_path: '', sort_by: '', sort_direction: 'asc', default_items_path: '' },
|
|
sourceOptions: '',
|
|
placeholderFields: [],
|
|
placeholderChips: '',
|
|
sampleDataPreview: '[]',
|
|
fontSize: 32
|
|
});
|
|
|
|
assert.match(markup, /data-api-sample-toggle[^>]*aria-expanded="false"/);
|
|
assert.doesNotMatch(markup, /data-api-sample-resize/);
|
|
assert.match(markup, /data-api-sample-data-panel hidden/);
|
|
assert.match(markup, /btn-outline-secondary w-100" data-api-advanced-toggle aria-expanded="false"/);
|
|
assert.match(markup, /data-api-advanced-options hidden/);
|
|
assert.match(markup, />Order results</);
|
|
|
|
const expandedMarkup = editor.renderEditorCard({
|
|
region: { id: 24 },
|
|
current: { value: '{{name}}' },
|
|
config: { source_id: 11, item_number: 1, items_path: '', sort_by: 'stats.kills', sort_direction: 'desc', default_items_path: '' },
|
|
sourceOptions: '',
|
|
placeholderFields: [],
|
|
placeholderChips: '',
|
|
sampleDataPreview: '[]',
|
|
fontSize: 32
|
|
});
|
|
assert.match(expandedMarkup, /btn-outline-secondary w-100" data-api-advanced-toggle aria-expanded="false"/);
|
|
assert.match(expandedMarkup, /id="api-region-advanced-24"[^>]*hidden/);
|
|
assert.match(expandedMarkup, /name="region_api_sort_by_24"[^>]*value="stats\.kills"/);
|
|
});
|
|
|
|
test('API editor preserves saved item paths instead of replacing them with the source default', () => {
|
|
const editor = loadApiEditor();
|
|
const context = {
|
|
region: { id: 23 },
|
|
current: { value: '{{0.name}}' },
|
|
config: { source_id: 11, default_items_path: 'data' },
|
|
sourceOptions: '',
|
|
placeholderFields: [],
|
|
placeholderChips: '',
|
|
sampleDataPreview: '[]',
|
|
fontSize: 32
|
|
};
|
|
|
|
for (const [savedPath, expectedPath] of [['data.players', 'data.players'], ['', ''], [null, ''], [undefined, 'data']]) {
|
|
const savedContext = { ...context, config: { ...context.config, items_path: savedPath } };
|
|
const builtContext = editor.buildEditorCardContext(savedContext, {}, []);
|
|
assert.equal(builtContext.itemsPath, expectedPath);
|
|
for (const cardContext of [savedContext, builtContext]) {
|
|
const markup = editor.renderEditorCard(cardContext);
|
|
assert.match(markup, new RegExp('name="region_api_items_path_23"[^>]*value="' + expectedPath.replace('.', '\\.') + '"'));
|
|
}
|
|
}
|
|
|
|
const explicitContext = editor.renderEditorCard(editor.buildEditorCardContext({
|
|
...context,
|
|
itemsPath: 'other.players',
|
|
config: { ...context.config, items_path: 'data.players' }
|
|
}, {}, []));
|
|
assert.match(explicitContext, /name="region_api_items_path_23"[^>]*value="other\.players"/);
|
|
});
|
|
|
|
test('API results preview always includes the first result completely', () => {
|
|
let source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/slides/slide-form-regions.js'), 'utf8');
|
|
source = source.replace("import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-preview.js';", '').replace('export function createSlideFormRegionHelpers', 'function createSlideFormRegionHelpers');
|
|
const context = {
|
|
window: {
|
|
placeholderUtils: require('../src/web/public/js/shared/placeholder-utils'),
|
|
placeholderChips: { renderChip: function (value) { return String(value); } }
|
|
},
|
|
createSlideFormPreviewHelpers: function () { return {}; }
|
|
};
|
|
vm.runInNewContext(source + '\nthis.createSlideFormRegionHelpers = createSlideFormRegionHelpers;', context);
|
|
const longDescription = 'x'.repeat(1500);
|
|
const helpers = context.createSlideFormRegionHelpers({
|
|
apiSources: [
|
|
{ id: 11, responseJson: { results: [{ name: 'Long result', products: [{ description: longDescription }] }, { name: 'Second result' }] } },
|
|
{ id: 12, responseJson: { name: 'Single object' } }
|
|
],
|
|
getRegionTypeModule: function () { return null; }
|
|
});
|
|
|
|
const markup = helpers.buildApiSampleDataMarkup(11, 'results', '', 'asc');
|
|
const firstItemMarkup = markup.match(/data-api-sample-data-output>([\s\S]*?)<\/div>/);
|
|
assert.ok(firstItemMarkup);
|
|
assert.ok(firstItemMarkup[1].includes(longDescription));
|
|
assert.match(firstItemMarkup[1], /\}\s*$/);
|
|
assert.equal((markup.match(/class="api-region-sample-preview(?:\s|\")/g) || []).length, 1);
|
|
assert.match(markup, /Example result \(first item\)/);
|
|
assert.match(markup, /data-json-toggle-collapse-all/);
|
|
assert.match(markup, /data-json-toggle-expand-all/);
|
|
assert.match(markup, /data-json-toggle-panel/);
|
|
assert.doesNotMatch(markup, /Second result|Remaining results/);
|
|
|
|
const objectMarkup = helpers.buildApiSampleDataMarkup(12, '', '', 'asc');
|
|
assert.match(objectMarkup, /Example result</);
|
|
assert.doesNotMatch(objectMarkup, /first item/);
|
|
});
|