Files
pulse-signage/test/api-region-sort.test.js
T
lzstealth 46636939de
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m17s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 1m10s
Release v2.17.4: add API region sorting and indexed placeholders
2026-10-23 20:54:15 +01:00

275 lines
9.9 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('API image bounds guide is limited to the small preview and hidden in the popup', () => {
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');
assert.match(preview, /class="api-image-placeholder-bounds"/);
assert.match(preview, /border:1px dashed/);
assert.match(popupStyles, /#popup-preview-canvas \.api-image-placeholder-bounds\s*\{\s*border:\s*0 !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 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/);
});