Release v2.14.1
This commit is contained in:
1 parent
635e1a9942
commit
c3299abaac
174 files changed
+4483
-1687
No files matched your search
@@ -0,0 +1,215 @@
|
||||
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/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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') }
|
||||
}
|
||||
};
|
||||
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('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</);
|
||||
});
|
||||
Reference in new issue
Block a user