Release v2.17.4: add API region sorting and indexed placeholders
This commit is contained in:
1 parent
bc8075379a
commit
46636939de
18 files changed
+709
-232
No files matched your search
@@ -0,0 +1,274 @@
|
||||
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/);
|
||||
});
|
||||
Reference in new issue
Block a user