259 lines
10 KiB
JavaScript
259 lines
10 KiB
JavaScript
// Generated, do not edit. sha256:81c29b0b17dff467d076992c914cba7f0090a65799888be3dc9347a850d7de68
|
||||
|
|
import { describe, it } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
|
|||
|
|
import { trapPunten, magnetischeIndex, withSpot, mountSparkline, changePctHtml, smoothPath, labelLeft } from '../../client/ui/sparkline.js';
|
|||
|
|
|
|||
|
|
const p = (px, py) => ({ px, py });
|
|||
|
|
|
|||
|
|
describe('trapPunten', () => {
|
|||
|
|
it('laat een vlakke reeks ongemoeid', () => {
|
|||
|
|
const in_ = [p(0, 50), p(10, 50), p(20, 50)];
|
|||
|
|
assert.deepEqual(trapPunten(in_), in_);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('zet bij een stap een hoekpunt op de x van de NIEUWE waarde', () => {
|
|||
|
|
const uit = trapPunten([p(0, 80), p(10, 40)]);
|
|||
|
|
assert.deepEqual(uit, [p(0, 80), p(10, 80), p(10, 40)]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('werkt net zo omlaag, want een verkoop verlaagt de inleg', () => {
|
|||
|
|
const uit = trapPunten([p(0, 40), p(10, 80)]);
|
|||
|
|
assert.deepEqual(uit, [p(0, 40), p(10, 40), p(10, 80)]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('doet niets bij een verschil onder de drempel', () => {
|
|||
|
|
const in_ = [p(0, 50), p(10, 50.005)];
|
|||
|
|
assert.deepEqual(trapPunten(in_), in_);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt de eerste en de laatste waarde op hun plek', () => {
|
|||
|
|
const uit = trapPunten([p(0, 90), p(5, 60), p(10, 20)]);
|
|||
|
|
assert.deepEqual(uit[0], p(0, 90));
|
|||
|
|
assert.deepEqual(uit[uit.length - 1], p(10, 20));
|
|||
|
|
assert.equal(uit.length, 5, 'drie punten met twee stappen worden vijf hoekpunten');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('valt niet om op een lege of eenpuntige reeks', () => {
|
|||
|
|
assert.deepEqual(trapPunten([]), []);
|
|||
|
|
assert.deepEqual(trapPunten([p(0, 1)]), [p(0, 1)]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('labelLeft: het label blijft binnen de grafiek', () => {
|
|||
|
|
it('staat op het punt als er ruimte is', () => {
|
|||
|
|
assert.equal(labelLeft(115, 100, 230), 115);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('schuift naar binnen aan beide randen, met 4 px afstand', () => {
|
|||
|
|
assert.equal(labelLeft(5, 200, 230), 104);
|
|||
|
|
assert.equal(labelLeft(225, 200, 230), 126);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('staat in het midden als het label breder is dan de grafiek', () => {
|
|||
|
|
assert.equal(labelLeft(10, 260, 230), 115);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('magnetischeIndex', () => {
|
|||
|
|
const reeks = Array.from({ length: 11 }, (_, i) => ({ fx: i / 10 }));
|
|||
|
|
|
|||
|
|
it('geeft het dichtstbijzijnde punt als er geen markering in de buurt is', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [8], 0.22, 0.03), 2);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('klikt op de markering zodra die binnen de tolerantie ligt', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [5], 0.52, 0.03), 5);
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [5], 0.48, 0.03), 5);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat de markering NIET winnen buiten de tolerantie', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [5], 0.30, 0.03), 3);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('kiest de dichtstbijzijnde markering als er twee in de buurt liggen', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [4, 5], 0.49, 0.05), 5);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('wint binnen de tolerantie ook van een punt dat NÓG dichterbij ligt', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [5], 0.401, 0.11), 5);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('valt terug op het dichtstbijzijnde punt zonder markeringen', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [], 0.71, 0.03), 7);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('negeert een markering die buiten de reeks wijst', () => {
|
|||
|
|
assert.equal(magnetischeIndex(reeks, [99], 0.31, 0.5), 3);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('withSpot', () => {
|
|||
|
|
const DAG = 86400000;
|
|||
|
|
const dag = iso => Date.parse(`${iso}T00:00:00Z`);
|
|||
|
|
const nu = dag('2026-09-04') + 13 * 3600000;
|
|||
|
|
const week = [
|
|||
|
|
[dag('2026-09-01'), 100],
|
|||
|
|
[dag('2026-09-02'), 110],
|
|||
|
|
[dag('2026-09-03'), 120],
|
|||
|
|
[dag('2026-09-04'), 130],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
it('vervangt de waarde van vandaag en maakt de reeks niet langer', () => {
|
|||
|
|
const uit = withSpot(week, 140, nu);
|
|||
|
|
assert.equal(uit.length, week.length);
|
|||
|
|
assert.deepEqual(uit[uit.length - 1], [dag('2026-09-04'), 140]);
|
|||
|
|
assert.deepEqual(uit.slice(0, -1), week.slice(0, -1));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt het punt op middernacht van die dag en zet er geen tijdstip in', () => {
|
|||
|
|
assert.equal(withSpot(week, 140, nu).at(-1)[0] % DAG, 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('voegt vandaag toe als de reeks bij gisteren ophoudt', () => {
|
|||
|
|
const totGisteren = week.slice(0, -1);
|
|||
|
|
const uit = withSpot(totGisteren, 140, nu);
|
|||
|
|
assert.equal(uit.length, totGisteren.length + 1);
|
|||
|
|
assert.deepEqual(uit.at(-1), [dag('2026-09-04'), 140]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('laat een reeks die voorbij vandaag loopt ongemoeid', () => {
|
|||
|
|
const toekomst = [...week, [dag('2026-09-05'), 150]];
|
|||
|
|
assert.equal(withSpot(toekomst, 140, nu), toekomst);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft de reeks onveranderd terug zonder bruikbare prijs', () => {
|
|||
|
|
for (const prijs of [null, undefined, NaN, 0, -1, '140']) {
|
|||
|
|
assert.equal(withSpot(week, prijs, nu), week);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft een lege of ontbrekende reeks onveranderd terug', () => {
|
|||
|
|
assert.deepEqual(withSpot([], 140, nu), []);
|
|||
|
|
assert.equal(withSpot(null, 140, nu), null);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('mountSparkline', () => {
|
|||
|
|
const DAG = 86400000;
|
|||
|
|
const punten = [[0, 100], [DAG, 110], [2 * DAG, 105]];
|
|||
|
|
|
|||
|
|
function bouw(opties = {}) {
|
|||
|
|
const leeg = () => ({ className: '', innerHTML: '', appendChild() {}, querySelector: () => ({}), addEventListener() {} });
|
|||
|
|
globalThis.document = { createElement: leeg };
|
|||
|
|
const host = { innerHTML: '', appendChild(kind) { this.kind = kind; } };
|
|||
|
|
mountSparkline(host, punten, opties);
|
|||
|
|
return host.kind.innerHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('zet de markeringen, de hover-lijn en het puntje ín de kliplaag', () => {
|
|||
|
|
const html = bouw({ markers: [{ x: 0, variant: 'buy' }] });
|
|||
|
|
const clip = html.indexOf('<div class="spark-mini-clip">');
|
|||
|
|
const label = html.indexOf('<div class="spark-mini-label"');
|
|||
|
|
assert.ok(clip >= 0 && label > clip, 'de kliplaag hoort te bestaan en vóór het label te staan');
|
|||
|
|
const binnen = html.slice(clip, label);
|
|||
|
|
for (const deel of ['spark-mini-mark', 'spark-mini-vline', 'spark-mini-dot']) {
|
|||
|
|
assert.ok(binnen.includes(deel), `${deel} hoort in de kliplaag te zitten`);
|
|||
|
|
}
|
|||
|
|
assert.equal((binnen.match(/<div/g) || []).length, (binnen.match(/<\/div>/g) || []).length,
|
|||
|
|
'de kliplaag hoort gesloten te zijn vóór het prijslabel');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('houdt het prijslabel BUITEN de kliplaag', () => {
|
|||
|
|
const html = bouw();
|
|||
|
|
const label = html.indexOf('<div class="spark-mini-label"');
|
|||
|
|
assert.ok(label > 0);
|
|||
|
|
assert.equal(html.slice(label).includes('spark-mini-dot'), false);
|
|||
|
|
assert.equal(html.slice(label).includes('spark-mini-clip'), false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft een markering op het eerste punt zijn echte x en schuift hem niet naar binnen', () => {
|
|||
|
|
const html = bouw({ markers: [{ x: 0, variant: 'buy' }] });
|
|||
|
|
assert.match(html, /spark-mini-mark is-buy[^>]*--spark-x:0\.00%/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('draagt de sleutel van een markering mee als `data-mark`', () => {
|
|||
|
|
const html = bouw({ markers: [{ x: DAG, variant: 'sell', key: 't42' }] });
|
|||
|
|
assert.match(html, /data-mark="t42"/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('maakt het vak leeg bij te weinig punten', () => {
|
|||
|
|
const leeg = () => ({ className: '', innerHTML: '', appendChild() {}, querySelector: () => ({}), addEventListener() {} });
|
|||
|
|
globalThis.document = { createElement: leeg };
|
|||
|
|
const host = { innerHTML: 'oud', appendChild() { throw new Error('er hoort niets gemount te worden'); } };
|
|||
|
|
mountSparkline(host, [[0, 100]]);
|
|||
|
|
assert.equal(host.innerHTML, '');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('changePctHtml: de verandering als pil', () => {
|
|||
|
|
it('geeft een stijging een groene pil, met het teken uit de instelling', () => {
|
|||
|
|
assert.equal(changePctHtml([[0, 100], [1, 110]]), '<span class="spark-change delta t-pos">10,0%</span>');
|
|||
|
|
assert.match(changePctHtml([[0, 100], [1, 110]], '', { signs: 'both' }), />\+10,0%</);
|
|||
|
|
assert.match(changePctHtml([[0, 100], [1, 90]], '', { signs: 'none' }), /class="spark-change delta t-neg">10,0%</);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft een daling een rode pil, met komma en minteken zoals elk ander percentage', () => {
|
|||
|
|
assert.match(changePctHtml([[0, 100], [1, 90]]), /class="spark-change delta t-neg">−10,0%</);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('geeft niets bij te weinig punten of een ongeldig begin', () => {
|
|||
|
|
assert.equal(changePctHtml([[0, 100]]), '');
|
|||
|
|
assert.equal(changePctHtml([[0, 0], [1, 5]]), '');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('zet een staart ín de pil, achter het getal', () => {
|
|||
|
|
assert.equal(changePctHtml([[0, 100], [1, 110]], '<i></i>'), '<span class="spark-change delta t-pos">10,0%<i></i></span>');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('fine: een tweede decimaal als één decimaal de beweging verbergt', () => {
|
|||
|
|
assert.match(changePctHtml([[0, 10000], [1, 10003]], '', { fine: true }), />0,03%</);
|
|||
|
|
assert.match(changePctHtml([[0, 10000], [1, 10003]]), />0,0%</);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('smoothPath', () => {
|
|||
|
|
const p = ys => ys.map((py, i) => ({ px: i * 10, py }));
|
|||
|
|
const segmenten = d => [...d.matchAll(/C[\d.-]+,([\d.-]+) [\d.-]+,([\d.-]+) [\d.-]+,([\d.-]+)/g)]
|
|||
|
|
.map(m => m.slice(1).map(Number));
|
|||
|
|
|
|||
|
|
it('loopt door elk punt', () => {
|
|||
|
|
const ys = [50, 20, 80, 10, 60];
|
|||
|
|
const d = smoothPath(p(ys));
|
|||
|
|
assert.match(d, /^M0\.00,50\.00 /);
|
|||
|
|
assert.deepEqual(segmenten(d).map(s => s[2]), ys.slice(1));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('schiet nergens door: de controlepunten blijven binnen hun segment', () => {
|
|||
|
|
const ys = [50, 20, 80, 10, 60, 61, 95, 94, 3];
|
|||
|
|
segmenten(smoothPath(p(ys))).forEach(([c1, c2], i) => {
|
|||
|
|
const lo = Math.min(ys[i], ys[i + 1]) - 1e-9, hi = Math.max(ys[i], ys[i + 1]) + 1e-9;
|
|||
|
|
assert.ok(c1 >= lo && c1 <= hi && c2 >= lo && c2 <= hi, `segment ${i}: ${c1}, ${c2} buiten [${lo}, ${hi}]`);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('een top of dal krijgt een vlakke raaklijn', () => {
|
|||
|
|
const [s0, s1] = segmenten(smoothPath(p([50, 20, 80])));
|
|||
|
|
assert.equal(s0[1], 20);
|
|||
|
|
assert.equal(s1[0], 20);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('twee punten zijn een rechte lijn, en geen punten geen pad', () => {
|
|||
|
|
assert.equal(smoothPath(p([10, 20])), 'M0.00,10.00 L10.00,20.00');
|
|||
|
|
assert.equal(smoothPath([]), '');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('mountSparkline tekent standaard rond (overal, gebruiker 08-10-2026), en recht met `smooth: false`', () => {
|
|||
|
|
const leeg = () => ({ className: '', innerHTML: '', appendChild() {}, querySelector: () => ({}), addEventListener() {} });
|
|||
|
|
globalThis.document = { createElement: leeg };
|
|||
|
|
const teken = opties => {
|
|||
|
|
const host = { innerHTML: '', appendChild(kind) { this.kind = kind; } };
|
|||
|
|
mountSparkline(host, [[0, 100], [1, 110], [2, 105]], opties);
|
|||
|
|
return host.kind.innerHTML;
|
|||
|
|
};
|
|||
|
|
assert.match(teken({}), /<path class="spark-mini-line" d="M[^"]* C/);
|
|||
|
|
assert.match(teken({}), /<path class="spark-mini-area" d="M[^"]* C[^"]*Z"/);
|
|||
|
|
assert.match(teken({ smooth: false }), /<polyline class="spark-mini-line"/);
|
|||
|
|
assert.match(teken({ vergelijk: [[0, 90], [1, 90], [2, 95]] }), /<polyline class="spark-mini-cmp"/);
|
|||
|
|
});
|
|||
|
|
});
|