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, '"') } } }; 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: '' }); 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 valuesDecreasing values