Files
lzstealth 49df51d92f
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s
Release 2.16.2
2026-10-21 01:38:22 +01:00

228 lines
8.4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
function loadChartData() {
const context = { window: {} };
vm.runInNewContext(fs.readFileSync('src/web/public/js/shared/placeholder-utils.js', 'utf8'), context);
vm.runInNewContext(fs.readFileSync('src/web/public/js/regions/chart-data.js', 'utf8'), context);
return context.window.pulseChartData;
}
function loadChartEditor() {
const definition = {};
const context = {
window: {
pulseChartData: loadChartData(),
pulseRegionTypes: { register: (_type, value) => Object.assign(definition, value) },
pulseRegionUtils: { escapeHtml: (value) => String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') }
}
};
vm.runInNewContext(fs.readFileSync('src/web/public/js/regions/type/chart.js', 'utf8'), context);
return definition;
}
test('normalizes manual chart rows into Chart.js data', () => {
const chartData = loadChartData();
const result = chartData.fromContent({
chart_type: 'line',
dataset_label: 'Visitors',
label_path: 'day',
value_path: 'count',
manual_rows: [
{ day: 'Mon', count: '12' },
{ day: 'Tue', count: 18 }
]
}, []);
assert.equal(result.type, 'line');
assert.deepEqual(Array.from(result.labels), ['Mon', 'Tue']);
assert.deepEqual(Array.from(result.datasets[0].data), [12, 18]);
assert.equal(result.datasets[0].label, 'Visitors');
});
test('uses an API source default items path and ignores non-numeric rows', () => {
const chartData = loadChartData();
const result = chartData.fromContent({
source_kind: 'api',
source_id: 4,
label_path: 'name',
value_path: 'total'
}, [{
id: 4,
items_path: 'payload.rows',
responseJson: {
payload: { rows: [
{ name: 'A', total: '1,200' },
{ name: 'Missing', total: 'n/a' },
{ name: 'B', total: 4 }
] }
}
}]);
assert.deepEqual(Array.from(result.labels), ['A', 'B']);
assert.deepEqual(Array.from(result.datasets[0].data), [1200, 4]);
});
test('API charts accept a single object items path without falling back on missing paths', () => {
const chartData = loadChartData();
const sources = [{ id: 4, items_path: 'data', responseJson: { data: { name: 'Campaign', amount_raised: { value: '200' } } } }];
const config = { source_kind: 'api', source_id: 4, label_path: 'name', value_path: 'amount_raised.value' };
const result = chartData.fromContent(config, sources);
assert.deepEqual(Array.from(result.labels), ['Campaign']);
assert.deepEqual(Array.from(result.datasets[0].data), [200]);
const missing = chartData.fromContent({ ...config, items_path: 'missing' }, sources);
assert.deepEqual(Array.from(missing.labels), []);
});
test('normalizes multiple series for line charts', () => {
const chartData = loadChartData();
const result = chartData.fromContent({
chart_type: 'line',
label_path: 'label',
colors: ['#111111', '#222222'],
datasets: [
{ label: 'Sales', value_path: 'sales' },
{ label: 'Returns', value_path: 'returns', color: '#333333' }
],
manual_rows: [
{ label: 'A', sales: 10, returns: 2 },
{ label: 'B', sales: 20, returns: 3, color: '#abcdef' },
{ label: 'C', sales: 30, returns: 4 }
]
}, []);
assert.deepEqual(Array.from(result.labels), ['A', 'B', 'C']);
assert.deepEqual(Array.from(result.datasets[0].data), [10, 20, 30]);
assert.deepEqual(Array.from(result.datasets[1].data), [2, 3, 4]);
assert.equal(result.datasets[0].backgroundColor, '#111111');
assert.equal(result.datasets[1].backgroundColor, '#333333');
});
test('uses per-item colors for bar values', () => {
const chartData = loadChartData();
const result = chartData.fromContent({
chart_type: 'bar',
label_path: 'label',
value_path: 'value',
manual_rows: [
{ label: 'A', value: 10 },
{ label: 'B', value: 20 },
{ label: 'C', value: 30 }
]
}, []);
assert.deepEqual(Array.from(result.datasets[0].backgroundColor), ['#36a269', '#3b82f6', '#f59e0b']);
assert.deepEqual(Array.from(result.datasets[0].borderColor), ['#36a269', '#3b82f6', '#f59e0b']);
});
test('applies source ordering, limits, and missing-value settings', () => {
const chartData = loadChartData();
const result = chartData.fromContent({
chart_type: 'line',
label_path: 'label',
source_options: { sort: 'value-descending', limit: 2, missing: 'zero' },
datasets: [
{ label: 'Sales', value_path: 'sales' },
{ label: 'Returns', value_path: 'returns' }
],
manual_rows: [
{ label: 'B', sales: 20 },
{ label: 'A', sales: 10, returns: 1 },
{ label: 'C', sales: 30, returns: 3 }
]
}, []);
assert.deepEqual(Array.from(result.labels), ['C', 'B']);
assert.deepEqual(Array.from(result.datasets[0].data), [30, 20]);
assert.deepEqual(Array.from(result.datasets[1].data), [3, 0]);
});
test('supports reverse original source order', () => {
const chartData = loadChartData();
const result = chartData.fromContent({
chart_type: 'bar',
label_path: 'label',
value_path: 'value',
source_options: { sort: 'reverse-original' },
manual_rows: [
{ label: 'A', value: 10 },
{ label: 'B', value: 20 },
{ label: 'C', value: 30 }
]
}, []);
assert.deepEqual(Array.from(result.labels), ['C', 'B', 'A']);
});
test('builds chart options from chart settings', () => {
const chartData = loadChartData();
const data = chartData.fromContent({
chart_type: 'bar',
chart_options: { legend: 'show', bar_orientation: 'horizontal', y_axis_min: 10, y_axis_max: 100, grid_color: '#123456', text_color: '#654321', text_size: 20 },
value_path: 'value',
manual_rows: [{ label: 'A', value: 20 }]
}, []);
const options = chartData.getChartOptions(data);
assert.equal(options.indexAxis, 'y');
assert.equal(options.plugins.legend.display, true);
assert.equal(options.scales.x.min, 10);
assert.equal(options.scales.x.max, 100);
assert.equal(options.scales.x.grid.color, '#123456');
assert.equal(options.scales.y.grid.color, '#123456');
assert.equal(options.scales.x.ticks.color, '#654321');
assert.equal(options.plugins.legend.labels.color, '#654321');
assert.equal(options.font.size, 20);
assert.equal(options.scales.x.ticks.font.size, 20);
assert.equal(options.plugins.legend.labels.font.size, 20);
assert.equal(options.plugins.tooltip.bodyFont.size, 20);
assert.equal(options.plugins.tooltip.bodyColor, '#654321');
});
test('clamps chart text size to supported bounds', () => {
const chartData = loadChartData();
assert.equal(chartData.normalizeConfig({ chart_options: { text_size: 64 } }).chart_options.text_size, 48);
assert.equal(chartData.normalizeConfig({ chart_options: { text_size: 4 } }).chart_options.text_size, 8);
assert.equal(chartData.normalizeConfig({}).chart_options.text_size, 12);
});
test('renders repeatable dataset controls instead of a JSON series editor', () => {
const editor = loadChartEditor();
const html = editor.renderEditorCard({
region: { id: 7, label: 'Revenue' },
config: {
type: 'bar',
source_kind: 'api',
datasets: [{ label: 'Sales', value_path: 'sales', color: '' }],
dataset_label: 'Value',
value_path: 'value',
colors_text: '#111111, #222222',
manual_rows_json: '[]'
},
sourceOptions: '<option value="1">Metrics</option>'
});
assert.match(html, /data-chart-datasets/);
assert.match(html, /data-chart-dataset-add/);
assert.match(html, /data-chart-dataset-remove/);
assert.match(html, /data-chart-source-panel="api"/);
assert.match(html, /data-chart-source-panel="manual"/);
assert.match(html, /data-chart-dataset-editor hidden/);
assert.match(html, /Series color/);
assert.match(html, /data-chart-dataset-color[^>]*type="color"/);
assert.doesNotMatch(html, /data-chart-dataset-color-auto/);
assert.doesNotMatch(html, /Default series label/);
assert.doesNotMatch(html, /Color palette/);
assert.doesNotMatch(html, /Series mappings \(JSON\)/);
assert.match(html, /Chart options/);
assert.match(html, /Source options/);
assert.match(html, /region_chart_value_format_7/);
assert.match(html, /region_chart_text_size_7/);
assert.match(html, /region_chart_source_missing_7/);
assert.match(html, /data-chart-option-types="line bar"/);
assert.match(html, />Increasing values</);
assert.match(html, />Decreasing values</);
});