Versie 0.3.3
This commit is contained in:
1 parent
1cf6eeed29
commit
6e741a51fe
57 files changed
+1362
-2135
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:3914f1ddc9004d83b8401e7c0b8dbbc55835029676e9b46f33978bfb59e5b5ee
|
||||
// Generated, do not edit. sha256:b4e0276b7d4bc8c4728c77282dde12358131e2103dd3182aa9dbf27cc334764b
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -261,6 +261,12 @@ describe('seedPreferences', () => {
|
||||
assert.equal(PREFERENCES.app.market_spot.initial, 'off', 'de terugval van een bestaande portefeuille');
|
||||
});
|
||||
|
||||
it('zet de AI-koppeling uit in een verse database, en laat de terugval aan', () => {
|
||||
seedPreferences();
|
||||
assert.equal(waarde('ai', 'enabled'), 0);
|
||||
assert.equal(PREFERENCES.ai.enabled.initial, 1, 'de terugval van een bestaande portefeuille');
|
||||
});
|
||||
|
||||
it('laat de andere voorkeuren op hun initial', () => {
|
||||
seedPreferences();
|
||||
assert.equal(waarde('app', 'result_signs'), PREFERENCES.app.result_signs.initial);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Generated, do not edit. sha256:4bf1820de70357b7a7e29ccee6fc599f970ee70dfc5b2f00ce791c38d33bc92e
|
||||
import { describe, it, mock, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const { fetchJson } = await import('../../client/price/btc-explorer.js');
|
||||
|
||||
const echteFetch = globalThis.fetch;
|
||||
afterEach(() => { globalThis.fetch = echteFetch; mock.timers.reset(); });
|
||||
|
||||
async function uitzitten(belofte) {
|
||||
let klaar = false;
|
||||
const uit = belofte.then(v => ({ v }), e => ({ e })).finally(() => { klaar = true; });
|
||||
while (!klaar) {
|
||||
await new Promise(r => setImmediate(r));
|
||||
mock.timers.runAll();
|
||||
}
|
||||
return uit;
|
||||
}
|
||||
|
||||
const ok = body => ({ ok: true, status: 200, json: async () => body });
|
||||
const status = s => ({ ok: false, status: s, json: async () => ({}) });
|
||||
|
||||
describe('fetchJson: opnieuw proberen', () => {
|
||||
it('zit drie netwerkfouten uit en geeft dan het antwoord', async () => {
|
||||
mock.timers.enable({ apis: ['setTimeout'] });
|
||||
let n = 0;
|
||||
globalThis.fetch = async () => { if (++n <= 3) throw new TypeError('Failed to fetch'); return ok({ x: 1 }); };
|
||||
const { v, e } = await uitzitten(fetchJson('https://x/api', 'X'));
|
||||
assert.equal(e, undefined);
|
||||
assert.deepEqual(v, { x: 1 });
|
||||
assert.equal(n, 4);
|
||||
});
|
||||
|
||||
it('geeft na de vierde mislukte poging de fout van de laatste', async () => {
|
||||
mock.timers.enable({ apis: ['setTimeout'] });
|
||||
let n = 0;
|
||||
globalThis.fetch = async () => { n++; return status(429); };
|
||||
const { e } = await uitzitten(fetchJson('https://x/api', 'X'));
|
||||
assert.ok(e);
|
||||
assert.equal(n, 4);
|
||||
});
|
||||
|
||||
it('probeert een 404 niet opnieuw', async () => {
|
||||
mock.timers.enable({ apis: ['setTimeout'] });
|
||||
let n = 0;
|
||||
globalThis.fetch = async () => { n++; return status(404); };
|
||||
const { e } = await uitzitten(fetchJson('https://x/api', 'X'));
|
||||
assert.ok(e);
|
||||
assert.equal(n, 1);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:38c4b07d1566cda74ef6cac4d031f49fd52004b6938fdee8f13e3e7678fc12c7
|
||||
// Generated, do not edit. sha256:13b6b32cbbf6b8c4a6a6069af37a7be33560a8abfc2af81ae2b4009e9692d7c6
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createDbHarness } from '../helpers/db-node.mjs';
|
||||
@@ -22,6 +22,7 @@ mock.module('../../server/license.mjs', {
|
||||
|
||||
const { createAsset, updateAsset, deleteAsset, listAssets } = await import('../../server/data/assets.js');
|
||||
const { createAccount, listAccounts } = await import('../../server/data/accounts.js');
|
||||
const { mergeAccounts } = await import('../../server/data/accounts-merge.js');
|
||||
const { createValuation, listValuations } = await import('../../server/data/valuations.js');
|
||||
const { saveConnection, listConnections } = await import('../../server/data/connections.js');
|
||||
const { setPriceSourceRank } = await import('../../server/data/price-source.js');
|
||||
@@ -148,23 +149,10 @@ describe('de gratis stand op balansen', () => {
|
||||
rateAt: `${dag}T00:00:00Z`, source: 'manual',
|
||||
});
|
||||
|
||||
it('laat ÉÉN balans toe en weigert de tweede', async () => {
|
||||
it('kent geen grens: ook de gratis stand legt balans na balans vast', async () => {
|
||||
const asset = await createAsset(EEN);
|
||||
await createValuation(balans(asset, '2026-01-01'));
|
||||
await assert.rejects(() => createValuation(balans(asset, '2026-02-01')), (fout) => {
|
||||
assert.equal(fout.key, 'valuations.maxFree');
|
||||
assert.equal(fout.params.max, 1);
|
||||
return true;
|
||||
});
|
||||
assert.equal(listValuations().length, 1);
|
||||
});
|
||||
|
||||
it('laat de tweede WEL toe in de betaalde stand', async () => {
|
||||
const asset = await createAsset(EEN);
|
||||
await createValuation(balans(asset, '2026-01-01'));
|
||||
stand = { level: 'full', enforced: true, why: 'licensed' };
|
||||
await createValuation(balans(asset, '2026-02-01'));
|
||||
assert.equal(listValuations().length, 2);
|
||||
for (const dag of ['2026-01-01', '2026-02-01', '2026-03-01']) await createValuation(balans(asset, dag));
|
||||
assert.equal(listValuations().length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,4 +200,15 @@ describe('de features van de gratis stand', () => {
|
||||
await assert.rejects(() => setPriceSourceRank(asset, 'spot', 1),
|
||||
(f) => f.key === 'integrations.matrixPaid');
|
||||
});
|
||||
|
||||
it('weigert accounts samenvoegen en laat beide accounts staan; betaald gaat het door', async () => {
|
||||
const wallet = (name) => ({ name, kind: 'wallet', isOwnCustody: true, scanEnabled: false, taxCategory: null });
|
||||
const a = await createAccount(wallet('A'));
|
||||
const b = await createAccount(wallet('B'));
|
||||
await assert.rejects(() => mergeAccounts(a, b), (f) => f.key === 'accounts.mergePaid');
|
||||
assert.equal(listAccounts().length, 2);
|
||||
stand = { level: 'full', enforced: true, why: 'licensed' };
|
||||
await mergeAccounts(a, b);
|
||||
assert.deepEqual(listAccounts().map(r => r.id), [a]);
|
||||
});
|
||||
});
|
||||
@@ -1,302 +0,0 @@
|
||||
// Generated, do not edit. sha256:ba6482c6f8221f445c935cfd65e76c09e943da662ff5dab04e50d8a76e004316
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import { toSearchName } from '../../shared/search-name.js';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { createDbHarness } from '../helpers/db-node.mjs';
|
||||
import { ACCOUNT_SOURCE_KINDS } from '../../shared/config.js';
|
||||
|
||||
const harness = createDbHarness();
|
||||
mock.module('../../server/db.js', { namedExports: harness.exports });
|
||||
|
||||
const { mergeAccounts, mergeCheck } = await import('../../server/data/accounts-merge.js');
|
||||
const { eventsNetByAccount } = await import('../../server/data/events.js');
|
||||
|
||||
let btc;
|
||||
beforeEach(() => {
|
||||
harness.recreate();
|
||||
harness.exports.run(
|
||||
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES ('BTC','Bitcoin','bitcoin','crypto','BTC','sat',8,'unit')`);
|
||||
btc = harness.exports.lastId();
|
||||
});
|
||||
|
||||
function account(naam, kind = 'wallet') {
|
||||
const eigen = ACCOUNT_SOURCE_KINDS[kind].length > 0 ? 1 : 0;
|
||||
harness.exports.run(
|
||||
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES (?,?,?,?,?,1,NULL)`, [naam, toSearchName(naam), kind, eigen, 0]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function event({ type = 'buy', from = null, to = null, amount = 1000, at = '2024-01-01T00:00:00', unitFee = null }) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO Event (assetId, type, occurredAt, amount, fiatPrice, unitFee, fromAccountId, toAccountId, source, createdAt)
|
||||
VALUES (?,?,?,?,?,?,?,?,'manual',?)`,
|
||||
[btc, type, at, amount, type === 'buy' || type === 'sell' ? 1 : null, unitFee, from, to, at]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function waardering({ acc, onDate, balance, rateAt = null }) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO Valuation (assetId, valuedOn, accountId, balance, fiatRate, rateAt, source)
|
||||
VALUES (?,?,?,?,?,?, 'manual')`,
|
||||
[btc, onDate, acc, balance, 50000, rateAt ?? `${onDate}T00:00:00`]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
const tel = (tabel, kolom, id) =>
|
||||
harness.exports.one(`SELECT COUNT(*) AS n FROM ${tabel} WHERE ${kolom} = ?`, [id]).n;
|
||||
const bestaatAccount = id => harness.exports.one(`SELECT 1 AS x FROM Account WHERE id=?`, [id]) != null;
|
||||
|
||||
describe('mergeCheck: compatibiliteit', () => {
|
||||
it('vraagt om twee accounts', () => {
|
||||
const a = account('A');
|
||||
assert.equal(mergeCheck(a, null).reason, 'pickTwo');
|
||||
assert.equal(mergeCheck(null, a).reason, 'pickTwo');
|
||||
assert.equal(mergeCheck(a, null).counts, null);
|
||||
});
|
||||
|
||||
it('weigert een account met zichzelf', () => {
|
||||
const a = account('A');
|
||||
assert.equal(mergeCheck(a, a).reason, 'sameAccount');
|
||||
assert.equal(mergeCheck(String(a), a).reason, 'sameAccount',
|
||||
'ook als het id als string binnenkomt (uit een <select>)');
|
||||
});
|
||||
|
||||
it('weigert een onbekend account', () => {
|
||||
const a = account('A');
|
||||
assert.equal(mergeCheck(a, 9999).reason, 'notFound');
|
||||
});
|
||||
|
||||
it('weigert custodial × trackable', () => {
|
||||
const beurs = account('Beurs', 'exchange');
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
assert.equal(mergeCheck(beurs, wallet).reason, 'sourceMismatch');
|
||||
assert.equal(mergeCheck(wallet, beurs).reason, 'sourceMismatch',
|
||||
'de regel geldt in beide richtingen');
|
||||
});
|
||||
|
||||
it('weigert een overlever die de bron-soorten van de opheffer niet kan dragen', () => {
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
const node = account('Node', 'lightning');
|
||||
assert.equal(mergeCheck(wallet, node).reason, 'kindCannotCarry');
|
||||
});
|
||||
|
||||
it('staat de andere richting wél toe', () => {
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
const node = account('Node', 'lightning');
|
||||
assert.equal(mergeCheck(node, wallet).ok, true,
|
||||
'lightning kan alles dragen wat wallet kan');
|
||||
});
|
||||
|
||||
it('staat twee custodial soorten toe', () => {
|
||||
const bank = account('Bank', 'bank');
|
||||
const kluis = account('Kluis', 'vault');
|
||||
assert.equal(mergeCheck(bank, kluis).ok, true);
|
||||
});
|
||||
|
||||
it('telt wat er zou verhuizen, en kijkt naar beide event-kanten', () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v });
|
||||
event({ type: 'sell', from: v });
|
||||
event({ type: 'buy', to: s });
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt)
|
||||
VALUES (?, 'address', 'bc1qtest', '2024-01-01T00:00:00')`, [v]);
|
||||
waardering({ acc: v, onDate: '2024-02-01', balance: 1000 });
|
||||
|
||||
const chk = mergeCheck(s, v);
|
||||
assert.equal(chk.ok, true);
|
||||
assert.deepEqual(chk.counts, { sources: 1, events: 2, schedules: 0, valuations: 1 });
|
||||
assert.equal(chk.survivorName, 'S');
|
||||
assert.equal(chk.victimName, 'V');
|
||||
});
|
||||
|
||||
it('verandert niets aan de database', () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v });
|
||||
mergeCheck(s, v);
|
||||
assert.equal(tel('Event', 'toAccountId', v), 1, 'een preview hoort niets te muteren');
|
||||
assert.ok(bestaatAccount(v));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeAccounts: verhuizing van kinddata', () => {
|
||||
it('weigert een incompatibele merge en laat alles staan', async () => {
|
||||
const beurs = account('Beurs', 'exchange');
|
||||
const wallet = account('Wallet', 'wallet');
|
||||
event({ type: 'buy', to: wallet });
|
||||
|
||||
await assert.rejects(() => mergeAccounts(beurs, wallet), (e) => e.key === 'accounts.mergeReason_sourceMismatch');
|
||||
assert.ok(bestaatAccount(wallet), 'de opheffer bestaat nog');
|
||||
assert.equal(tel('Event', 'toAccountId', wallet), 1);
|
||||
});
|
||||
|
||||
it('verhuist alle acht soorten kinddata en verwijdert de opheffer', async () => {
|
||||
const s = account('Overlever');
|
||||
const v = account('Opheffer');
|
||||
|
||||
event({ type: 'buy', to: v });
|
||||
event({ type: 'sell', from: v });
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt)
|
||||
VALUES (?, 'address', 'bc1qbron', '2024-01-01T00:00:00')`, [v]);
|
||||
harness.exports.run(`INSERT INTO WalletAddress (accountId, address) VALUES (?, 'bc1qadres')`, [v]);
|
||||
harness.exports.run(`INSERT INTO WalletTransaction (accountId, layer, txid, direction, valueSats, createdAt)
|
||||
VALUES (?, 'onchain', 'tx1', 'in', 100, '2024-01-01T00:00:00')`, [v]);
|
||||
harness.exports.run(`INSERT INTO ScanLog (accountId, scannedAt, status)
|
||||
VALUES (?, '2024-01-01T00:00:00', 'success')`, [v]);
|
||||
harness.exports.run(`INSERT INTO ReconcileProposal
|
||||
(accountId, assetId, amount, direction, suggestedType, status, detectedAt)
|
||||
VALUES (?,?, 100, 'in', 'transfer', 'pending', '2024-01-01T00:00:00')`, [v, btc]);
|
||||
harness.exports.run(`INSERT INTO ImportBatch (accountId, filename, importedAt, rowCount)
|
||||
VALUES (?, 'x.csv', '2024-01-01T00:00:00', 3)`, [v]);
|
||||
harness.exports.run(`INSERT INTO InterestSchedule
|
||||
(accountId, rateKind, payoutAssetId, startsOn, compounding, createdAt)
|
||||
VALUES (?, 'variable', ?, '2024-01-01', 0, '2024-01-01T00:00:00')`, [v, btc]);
|
||||
|
||||
const counts = await mergeAccounts(s, v);
|
||||
|
||||
assert.equal(tel('Event', 'toAccountId', s), 1);
|
||||
assert.equal(tel('Event', 'fromAccountId', s), 1);
|
||||
assert.equal(tel('WalletSource', 'accountId', s), 1);
|
||||
assert.equal(tel('WalletAddress', 'accountId', s), 1);
|
||||
assert.equal(tel('WalletTransaction', 'accountId', s), 1);
|
||||
assert.equal(tel('ScanLog', 'accountId', s), 1);
|
||||
assert.equal(tel('ReconcileProposal', 'accountId', s), 1);
|
||||
assert.equal(tel('ImportBatch', 'accountId', s), 1);
|
||||
assert.equal(tel('InterestSchedule', 'accountId', s), 1);
|
||||
|
||||
for (const [tabel, kolom] of [['Event', 'toAccountId'], ['Event', 'fromAccountId'],
|
||||
['WalletSource', 'accountId'], ['WalletAddress', 'accountId'], ['WalletTransaction', 'accountId'],
|
||||
['ScanLog', 'accountId'], ['ReconcileProposal', 'accountId'], ['ImportBatch', 'accountId'],
|
||||
['InterestSchedule', 'accountId']]) {
|
||||
assert.equal(tel(tabel, kolom, v), 0, `${tabel}.${kolom} verwijst nog naar de opheffer`);
|
||||
}
|
||||
|
||||
assert.equal(bestaatAccount(v), false, 'de opheffer hoort verdwenen te zijn');
|
||||
assert.ok(bestaatAccount(s));
|
||||
assert.equal(counts.events, 2, 'de teruggegeven counts komen uit de voorbeschouwing');
|
||||
});
|
||||
|
||||
it("laat de bron-id's ongemoeid, zodat NWC-secrets geldig blijven", async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt)
|
||||
VALUES (?, 'address', 'bc1qbron', '2024-01-01T00:00:00')`, [v]);
|
||||
const bronId = harness.exports.lastId();
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
const bron = harness.exports.one(`SELECT id, accountId FROM WalletSource`);
|
||||
assert.equal(bron.id, bronId, 'het bron-id mag niet wijzigen');
|
||||
assert.equal(bron.accountId, s);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeAccounts: waarderingen', () => {
|
||||
const verschil = (valId) => {
|
||||
const v = harness.exports.one(`SELECT accountId, assetId, rateAt, balance FROM Valuation WHERE id=?`, [valId]);
|
||||
return v.balance - eventsNetByAccount(v.accountId, v.assetId, v.rateAt);
|
||||
};
|
||||
|
||||
it('houdt het verschil van elke waardering gelijk', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'buy', to: s, amount: 500, at: '2024-01-01T00:00:00' });
|
||||
|
||||
const valV = waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
const valS = waardering({ acc: s, onDate: '2024-03-01', balance: 500 });
|
||||
|
||||
assert.equal(verschil(valV), 200);
|
||||
assert.equal(verschil(valS), 0);
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
assert.equal(verschil(valV), 200, 'een bestaand verschil moet behouden blijven');
|
||||
assert.equal(verschil(valS), 0, 'reconciled (0) moet 0 blijven');
|
||||
});
|
||||
|
||||
it('corrigeert de balance daarvoor, ook van de overlever zelf', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'buy', to: s, amount: 500, at: '2024-01-01T00:00:00' });
|
||||
const valV = waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
const valS = waardering({ acc: s, onDate: '2024-03-01', balance: 500 });
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
const balans = id => harness.exports.one(`SELECT balance FROM Valuation WHERE id=?`, [id]).balance;
|
||||
assert.equal(balans(valV), 1700, '1500 grootboek + 200 verschil');
|
||||
assert.equal(balans(valS), 1500, '1500 grootboek + 0 verschil, dus herschreven van 500');
|
||||
});
|
||||
|
||||
it('verhuist een niet-botsende waardering naar de overlever', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
const val = waardering({ acc: v, onDate: '2024-02-01', balance: 10 });
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(harness.exports.one(`SELECT accountId FROM Valuation WHERE id=?`, [val]).accountId, s);
|
||||
});
|
||||
|
||||
it('houdt bij een botsing precies één waardering over', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
waardering({ acc: s, onDate: '2024-02-01', balance: 500 });
|
||||
waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
const over = harness.exports.all(`SELECT accountId, balance FROM Valuation`);
|
||||
assert.equal(over.length, 1, 'geen dubbele rij, geen UNIQUE-fout');
|
||||
assert.equal(over[0].accountId, s, 'de overgebleven rij hangt aan de overlever');
|
||||
});
|
||||
|
||||
it('wist bij een botsing de rij van de opheffer, niet die van de overlever', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
const valS = waardering({ acc: s, onDate: '2024-02-01', balance: 500 });
|
||||
const valV = waardering({ acc: v, onDate: '2024-02-01', balance: 1200 });
|
||||
|
||||
await mergeAccounts(s, v);
|
||||
|
||||
assert.ok(harness.exports.one(`SELECT 1 AS x FROM Valuation WHERE id=?`, [valS]),
|
||||
'de waardering van de overlever blijft');
|
||||
assert.equal(harness.exports.one(`SELECT 1 AS x FROM Valuation WHERE id=?`, [valV]), null,
|
||||
'de botsende rij van de opheffer wijkt');
|
||||
assert.equal(harness.exports.one(`SELECT balance FROM Valuation`).balance, 500,
|
||||
'en dus blijft de gemeten balans van het behouden account staan');
|
||||
});
|
||||
|
||||
it('botst alleen op dezelfde datum, niet op hetzelfde asset alleen', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
waardering({ acc: s, onDate: '2024-02-01', balance: 500 });
|
||||
waardering({ acc: v, onDate: '2024-02-02', balance: 1200 });
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(harness.exports.one(`SELECT COUNT(*) AS n FROM Valuation`).n, 2);
|
||||
});
|
||||
|
||||
it('rekent het verschil met transfers en fees mee', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'transfer', from: v, to: null, amount: 300, unitFee: 20, at: '2024-01-15T00:00:00' });
|
||||
const val = waardering({ acc: v, onDate: '2024-02-01', balance: 700 });
|
||||
|
||||
assert.equal(verschil(val), 20);
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(verschil(val), 20);
|
||||
});
|
||||
|
||||
it('negeert waarderingen ná het rateAt-moment bij de correctie', async () => {
|
||||
const s = account('S');
|
||||
const v = account('V');
|
||||
event({ type: 'buy', to: v, amount: 1000, at: '2024-01-01T00:00:00' });
|
||||
event({ type: 'buy', to: s, amount: 9999, at: '2024-06-01T00:00:00' });
|
||||
const val = waardering({ acc: v, onDate: '2024-02-01', balance: 1000 });
|
||||
|
||||
assert.equal(verschil(val), 0);
|
||||
await mergeAccounts(s, v);
|
||||
assert.equal(verschil(val), 0, 'de late aankoop hoort buiten dit venster te vallen');
|
||||
assert.equal(harness.exports.one(`SELECT balance FROM Valuation WHERE id=?`, [val]).balance, 1000,
|
||||
'balance blijft 1000: het grootboek t/m die datum is niet veranderd');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:e7c1f8e0ef8ff3927bf8ff8a92bc2028aac07446bbf50cb1b0842db32ca8e5f3
|
||||
// Generated, do not edit. sha256:500c3d5e31a736de36a900bebc7682d080b1ebb8cda99a94331c6ce11a3fab67
|
||||
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
@@ -16,6 +16,7 @@ mock.module('../../server/license.mjs', {
|
||||
});
|
||||
|
||||
const { listPortfolios } = await import('../../server/portfolios.mjs');
|
||||
const { CONFIG } = await import('../../shared/config.js');
|
||||
const { createPortfolio, archivePortfolio } = await import('../../server/portfolios-admin.mjs');
|
||||
|
||||
let DIR;
|
||||
@@ -50,6 +51,14 @@ describe('de gratis stand op portefeuilles', () => {
|
||||
assert.deepEqual(listPortfolios().map((p) => p.naam), ['Eerste']);
|
||||
});
|
||||
|
||||
it('telt de demo niet mee: naast de demo past nog een eigen portefeuille', async () => {
|
||||
stand = { level: 'full', enforced: true, why: 'licensed' };
|
||||
await createPortfolio(CONFIG.PORTFOLIO.DEMO_NAME);
|
||||
stand = { level: 'free', enforced: true, why: 'none' };
|
||||
await createPortfolio('Eigen');
|
||||
await assert.rejects(() => createPortfolio('Tweede'), (fout) => fout.key === 'portfolios.maxFree');
|
||||
});
|
||||
|
||||
it('laat de tweede WEL toe in de betaalde stand', async () => {
|
||||
await createPortfolio('Eerste');
|
||||
stand = { level: 'full', enforced: true, why: 'licensed' };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:4b1eef4e0860bce440fbf0875965c32cd7b13b237d0ffc92ef0255b11fe9a950
|
||||
// Generated, do not edit. sha256:687aaa4f407ca89e2e9f7cb5d0e2f1e02a1d00b4fdf4b9f0890c187e9171588f
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
@@ -30,10 +30,13 @@ const wijsNaar = (naam) => writeFile(join(dir, 'active.json'), JSON.stringify({
|
||||
|
||||
before(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'assetapp-mcp-wissel-'));
|
||||
proc = spawn(process.execPath, [STDIO], {
|
||||
env: { ...process.env, ASSETAPP_DATA_DIR: dir },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
const env = { ...process.env, ASSETAPP_DATA_DIR: dir };
|
||||
const eerste = spawn(process.execPath, [STDIO], { env, stdio: 'ignore' });
|
||||
await new Promise((klaar) => eerste.once('exit', klaar));
|
||||
const db = new DatabaseSync(join(dir, 'MyAssets', 'db.sqlite'));
|
||||
db.exec(`UPDATE Preference SET value = '1' WHERE scope = 'ai' AND key = 'enabled'`);
|
||||
db.close();
|
||||
proc = spawn(process.execPath, [STDIO], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
proc.stdout.on('data', (brok) => {
|
||||
buffer += brok.toString();
|
||||
let n;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:7bb4b4b51bf14bf2fa32107d316282a41f1c7586c33fc94c43098199bfa75953
|
||||
// Generated, do not edit. sha256:d705db22a94bcceb849648fab7596bd5a84e34d8b58a472ec9dc677c8db0cc0a
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
|
||||
const WORTEL = join(fileURLToPath(import.meta.url), '..', '..', '..');
|
||||
const STDIO = join(WORTEL, 'server', 'mcp', 'stdio.mjs');
|
||||
|
||||
let dir, proc, buffer = '', wachtenden = [];
|
||||
let dir, dbPad, proc, buffer = '', wachtenden = [];
|
||||
|
||||
function stuur(bericht) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -24,10 +24,14 @@ function stuur(bericht) {
|
||||
|
||||
before(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'assetapp-mcp-stdio-'));
|
||||
proc = spawn(process.execPath, [STDIO], {
|
||||
env: { ...process.env, ASSETAPP_DATA_DIR: dir, ASSETAPP_DB_FILE: join(dir, 'db.sqlite') },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
dbPad = join(dir, 'MyAssets', 'db.sqlite');
|
||||
const env = { ...process.env, ASSETAPP_DATA_DIR: dir, ASSETAPP_DB_FILE: dbPad };
|
||||
const eerste = spawn(process.execPath, [STDIO], { env, stdio: 'ignore' });
|
||||
assert.equal(await new Promise((klaar) => eerste.once('exit', klaar)), 0, 'een verse installatie stopt met de koppeling uit');
|
||||
const db = new DatabaseSync(dbPad);
|
||||
db.exec(`UPDATE Preference SET value = '1' WHERE scope = 'ai' AND key = 'enabled'`);
|
||||
db.close();
|
||||
proc = spawn(process.execPath, [STDIO], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
proc.stdout.on('data', (brok) => {
|
||||
buffer += brok.toString();
|
||||
let n;
|
||||
@@ -74,7 +78,7 @@ describe('mcp-stdio: het proces beantwoordt bericht na bericht', () => {
|
||||
|
||||
describe('mcp-stdio: de schuif "Actief" zet dit proces uit', () => {
|
||||
it('stopt bij het eerstvolgende bericht zodra de schuif uit gaat', async () => {
|
||||
const db = new DatabaseSync(join(dir, 'db.sqlite'));
|
||||
const db = new DatabaseSync(dbPad);
|
||||
db.exec(`PRAGMA busy_timeout = 5000`);
|
||||
db.exec(`INSERT INTO Preference (scope, key, value) VALUES ('ai', 'enabled', '0')
|
||||
ON CONFLICT (scope, key) DO UPDATE SET value = '0'`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:e9def29bc2006594fdeb8199e56fcff107612198b70d50be3a6d86402660dd84
|
||||
// Generated, do not edit. sha256:27b79f8183ca4a0e1556b615e64322487b0abdd54fa1269b5d1f2751a7c279af
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -15,7 +15,7 @@ mock.module('../../client/data/ai-audit.js', { namedExports: { countAiToolCallsS
|
||||
mock.module('../../client/data/preferences.js', { namedExports: { getPref: async () => null } });
|
||||
mock.module('../../client/data/price-history.js', { namedExports: { listPriceGaps: async () => ({ rows: [], total: 0 }) } });
|
||||
mock.module('../../client/data/api-metrics.js', { namedExports: { apiCallsByProvider: leeg } });
|
||||
mock.module('../../client/license.js', { namedExports: { licenseChangedSinceSeen: async () => false } });
|
||||
mock.module('../../client/license.js', { namedExports: { licenseChangedSinceSeen: async () => false, locked: () => false } });
|
||||
mock.module('../../client/app-settings.js', { namedExports: { appSetting: () => null, baseCurrency: () => 'EUR' } });
|
||||
mock.module('../../client/price/providers.js', { namedExports: { rateSourceFor: async () => null } });
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:0c8f95dec7394e0dad71a6a790d7768a2d482ac976a2a68218282ae506402032
|
||||
// Generated, do not edit. sha256:ac1fa206a688dd1bdc09c884947e72f6ac770db7b7ba6d03716b15de2b2b3551
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -63,10 +63,15 @@ describe('activityItems', () => {
|
||||
describe('heartbeat', () => {
|
||||
const item = (s, dir) => ({ x: dag(s), dir });
|
||||
|
||||
it('loopt per dag van de dag vóór het eerste item tot vandaag, op nul waar niets gebeurde', () => {
|
||||
it('loopt per dag van de dag vóór het eerste item tot de dag na vandaag, op nul waar niets gebeurde', () => {
|
||||
const { points } = heartbeat([item('2026-03-02', 1)], '2026-03-05T12:00:00Z');
|
||||
assert.deepEqual(points.map(p => p[0]), [1, 2, 3, 4, 5].map(d => dag(`2026-03-0${d}`)));
|
||||
assert.deepEqual(points.map(p => p[1]), [0, 1, 0, 0, 0]);
|
||||
assert.deepEqual(points.map(p => p[0]), [1, 2, 3, 4, 5, 6].map(d => dag(`2026-03-0${d}`)));
|
||||
assert.deepEqual(points.map(p => p[1]), [0, 1, 0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
it('zet een streepje van vandaag niet op de rand, waar de stip half wegviel', () => {
|
||||
const { points } = heartbeat([item('2026-03-05', 0)], '2026-03-05T12:00:00Z');
|
||||
assert.notEqual(points[points.length - 1][0], dag('2026-03-05'), 'vandaag is niet het laatste punt');
|
||||
});
|
||||
|
||||
it('wijst omhoog bij meer erin, omlaag bij meer eruit, en omhoog bij evenveel', () => {
|
||||
@@ -88,16 +93,16 @@ describe('heartbeat', () => {
|
||||
const { points, byDay } = heartbeat([item('1994-06-01', 1), item('2026-03-02', 1)],
|
||||
'2026-03-03T12:00:00Z', '2026-03-01T12:00:00Z');
|
||||
assert.equal(points[0][0], dag('2026-03-01'), 'de lijn begint op de startdag, niet op de dag ervoor');
|
||||
assert.equal(points.length, 3);
|
||||
assert.equal(points.length, 4);
|
||||
assert.equal(byDay.has(dag('1994-06-01')), false, 'de oude boeking is er niet');
|
||||
assert.equal(heartbeat([item('1994-06-01', 1)], '2026-03-03T12:00:00Z', '2026-03-01T12:00:00Z'), null,
|
||||
'niets ná de start: geen lijn');
|
||||
});
|
||||
|
||||
it('geeft niets zonder items, en twee punten als alles vandaag gebeurde', () => {
|
||||
it('geeft niets zonder items, en drie punten als alles vandaag gebeurde', () => {
|
||||
assert.equal(heartbeat([], '2026-03-05T12:00:00Z'), null);
|
||||
const { points } = heartbeat([item('2026-03-05', -1)], '2026-03-05T12:00:00Z');
|
||||
assert.equal(points.length, 2);
|
||||
assert.equal(points.length, 3);
|
||||
assert.equal(points[1][0] - points[0][0], DAG);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:45c8de8a885eb07b9f327fbd56b57d092c058152ae89dc307ade593f6035be32
|
||||
// Generated, do not edit. sha256:9ad90176a0537a5dae796d00139a843d9f56aa6cd2de67fc75215c16300b5802
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('een balans en een portefeuille op de kaart', () => {
|
||||
|
||||
it('toont een portefeuille-voorstel met de naam en wat het wél en niet doet', async () => {
|
||||
const html = await teken([portefeuille()]);
|
||||
assert.match(html, /Nieuwe portefeuille/);
|
||||
assert.match(html, /Nieuw portfolio/);
|
||||
assert.match(html, /AI-demonstratie/);
|
||||
assert.match(html, /Leeg/);
|
||||
assert.match(html, /wisselen|niet geopend/i, 'dat hij niet geopend wordt');
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Generated, do not edit. sha256:1928a498401313a668bc3ca99074cab18fed0b4d4c803c1532d4ea964f94743c
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installDom } from '../helpers/mini-dom.mjs';
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
|
||||
const dom = installDom();
|
||||
|
||||
const INSTELLING = { blur: 0, auto_blur_min: CONFIG.AUTO_BLUR.NEVER_MIN };
|
||||
mock.module('../../client/app-settings.js', {
|
||||
namedExports: {
|
||||
appSetting: k => INSTELLING[k],
|
||||
setAppSetting: async (k, v) => { INSTELLING[k] = v; },
|
||||
},
|
||||
});
|
||||
|
||||
const { initPrivacyBlur } = await import('../../client/ui/blur.js');
|
||||
initPrivacyBlur(dom.host());
|
||||
|
||||
const stand = () => dom.document.documentElement.dataset.blur;
|
||||
|
||||
beforeEach(() => {
|
||||
INSTELLING.blur = 0;
|
||||
dom.document.documentElement.dataset.blur = 'off';
|
||||
for (const el of dom.document.querySelectorAll('.nav-edit-toggle, button.focus')) el.remove();
|
||||
});
|
||||
|
||||
describe('de spatiebalk en de vervaging', () => {
|
||||
it('wisselt heen en terug als niets de focus heeft, en houdt de pagina stil', () => {
|
||||
const doorgelaten = dom.key(dom.document.body, ' ');
|
||||
assert.equal(stand(), 'on');
|
||||
assert.equal(doorgelaten, false, 'geen scrollsprong van de spatie');
|
||||
dom.key(dom.document.body, ' ');
|
||||
assert.equal(stand(), 'off');
|
||||
});
|
||||
|
||||
it('laat een knop met de focus zijn eigen spatie houden', () => {
|
||||
const knop = dom.document.createElement('button');
|
||||
knop.className = 'focus';
|
||||
dom.document.body.appendChild(knop);
|
||||
assert.equal(dom.key(knop, ' '), true);
|
||||
assert.equal(stand(), 'off');
|
||||
});
|
||||
|
||||
it('doet niets in bewerkmodus of met een modifier', () => {
|
||||
dom.key(dom.document.body, ' ', { shiftKey: true });
|
||||
assert.equal(stand(), 'off');
|
||||
const schakel = dom.document.createElement('span');
|
||||
schakel.className = 'nav-edit-toggle is-on';
|
||||
dom.document.body.appendChild(schakel);
|
||||
dom.key(dom.document.body, ' ');
|
||||
assert.equal(stand(), 'off');
|
||||
});
|
||||
});
|
||||
@@ -1,429 +0,0 @@
|
||||
// Generated, do not edit. sha256:43e27327deb8801c6e640676df136f116a9043c3e6d4e1f65b6469a9f90a6fa6
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const INSTELLING = { asset_show_name: 1, asset_show_symbol: 1 };
|
||||
mock.module('../../client/app-settings.js', {
|
||||
namedExports: { appSetting: (k) => INSTELLING[k], baseCurrency: () => 'EUR', setAppSetting: async () => {} },
|
||||
});
|
||||
|
||||
const { fiatAtPrice, fiatValue, fiatHistorical } = await import('../../client/ui/fiat.js');
|
||||
const { assetLabel, assetLabelHtml } = await import('../../client/ui/asset-label.js');
|
||||
const { RANGE_DAYS, SPOT_RANGE_DAYS, rangeSeries, intradayStart, rangeCycleButton, wireRangeCycle, rangePointLabel, rangePointSources, namedDaySources } = await import('../../client/ui/range-cycle.js');
|
||||
const { serviceOrder, matrixCardHtml } = await import('../../client/ui/source-matrix.js');
|
||||
|
||||
describe('fiatAtPrice: van kleinste eenheid naar een bedrag', () => {
|
||||
it('rekent met de decimalen van het asset', () => {
|
||||
assert.match(fiatAtPrice(100000, 8, 50000), /50,00/);
|
||||
});
|
||||
|
||||
it('gebruikt acht decimalen als er geen meegegeven zijn', () => {
|
||||
assert.equal(fiatAtPrice(100000000, undefined, 7), fiatAtPrice(100000000, 8, 7));
|
||||
});
|
||||
|
||||
it('geeft een lege string als er geen prijs bekend is', () => {
|
||||
assert.equal(fiatAtPrice(100, 8, null), '');
|
||||
assert.equal(fiatAtPrice(100, 8, undefined), '');
|
||||
});
|
||||
|
||||
it('rekent nul gewoon uit in plaats van hem als onbekend te behandelen', () => {
|
||||
assert.match(fiatAtPrice(0, 8, 50000), /0,00/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fiatValue en fiatHistorical: de twee wegen naar leeg', () => {
|
||||
it('geeft niets voor het reken-asset', () => {
|
||||
assert.equal(fiatValue(12345, { id: 1, symbol: 'EUR', decimals: 2 }, 'eur'), '');
|
||||
});
|
||||
|
||||
it('geeft niets zonder koers in de cache, en zonder asset', () => {
|
||||
assert.equal(fiatValue(100, { id: 999, symbol: 'BTC', decimals: 8 }, 'eur'), '');
|
||||
assert.equal(fiatValue(100, null, 'eur'), '');
|
||||
assert.equal(fiatHistorical(100, null, '2026-03-01'), '');
|
||||
assert.equal(fiatHistorical(100, { id: 999, decimals: 8 }, '2026-03-01'), '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('assetLabel: de twee weergaveschuiven', () => {
|
||||
const btc = { name: 'Bitcoin', symbol: 'BTC' };
|
||||
|
||||
it('toont naam en symbool samen', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 1 });
|
||||
assert.equal(assetLabel(btc), 'Bitcoin (BTC)');
|
||||
});
|
||||
|
||||
it('toont alleen de naam, of alleen het symbool', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 0 });
|
||||
assert.equal(assetLabel(btc), 'Bitcoin');
|
||||
Object.assign(INSTELLING, { asset_show_name: 0, asset_show_symbol: 1 });
|
||||
assert.equal(assetLabel(btc), 'BTC');
|
||||
});
|
||||
|
||||
it('valt terug op wat er wél is als de gekozen helft ontbreekt', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 0 });
|
||||
assert.equal(assetLabel({ symbol: 'XAU' }), 'XAU');
|
||||
Object.assign(INSTELLING, { asset_show_name: 0, asset_show_symbol: 1 });
|
||||
assert.equal(assetLabel({ name: 'Goud' }), 'Goud');
|
||||
});
|
||||
|
||||
it('leest de alias-velden van een JOIN-rij', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 1 });
|
||||
assert.equal(assetLabel({ asset_name: 'Goud', asset_symbol: 'XAU' }), 'Goud (XAU)');
|
||||
});
|
||||
|
||||
it('geeft voorrang aan het eigen veld boven de alias, ook als dat leeg is', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 0 });
|
||||
assert.equal(assetLabel({ name: null, asset_name: 'Bitcoin', symbol: 'BTC' }), 'BTC');
|
||||
});
|
||||
|
||||
it('geeft een lege string voor niets', () => {
|
||||
assert.equal(assetLabel(null), '');
|
||||
assert.equal(assetLabelHtml(null), '');
|
||||
});
|
||||
|
||||
it('laat met `withoutSymbol` het symbool weg, want de hoeveelheid droeg het al', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 1 });
|
||||
const met = assetLabelHtml({ asset_name: 'Proef zonder koers', asset_symbol: 'PRFN' });
|
||||
const zonder = assetLabelHtml({ asset_name: 'Proef zonder koers', asset_symbol: 'PRFN' }, { withoutSymbol: true });
|
||||
assert.match(met, /PRFN/, 'zonder de vlag blijft het gedrag ongewijzigd');
|
||||
assert.match(zonder, /Proef zonder koers/);
|
||||
assert.ok(!zonder.includes('PRFN'), zonder);
|
||||
});
|
||||
|
||||
it('toont een naam die gelijk is aan het symbool één keer, ook met beide schuiven aan', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 1 });
|
||||
assert.equal(assetLabel({ name: 'XRP', symbol: 'XRP' }), 'XRP');
|
||||
assert.equal(assetLabel({ name: 'Xrp', symbol: 'XRP' }), 'Xrp');
|
||||
assert.equal(assetLabel({ name: 'XRP Ledger', symbol: 'XRP' }), 'XRP Ledger (XRP)');
|
||||
const h = assetLabelHtml({ name: 'XRP', symbol: 'XRP' });
|
||||
assert.equal(h.match(/XRP/g).length, 1, h);
|
||||
assert.ok(!h.includes('t-muted-cell'), h);
|
||||
});
|
||||
|
||||
it('valt met `withoutSymbol` terug op het symbool als er geen naam te tonen is', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 1 });
|
||||
assert.match(assetLabelHtml({ asset_symbol: 'PRFN' }, { withoutSymbol: true }), /PRFN/);
|
||||
Object.assign(INSTELLING, { asset_show_name: 0, asset_show_symbol: 1 });
|
||||
assert.match(assetLabelHtml({ asset_name: 'Bitcoin', asset_symbol: 'BTC' }, { withoutSymbol: true }), /BTC/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assetLabelHtml: bouwt HTML en escapet zelf', () => {
|
||||
it('zet naam en symbool in hun eigen span', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 1 });
|
||||
const h = assetLabelHtml({ name: 'Bitcoin', symbol: 'BTC' });
|
||||
assert.match(h, /<span class="asset-name">Bitcoin<\/span>/);
|
||||
assert.match(h, /<span class="t-muted-cell">BTC<\/span>/);
|
||||
});
|
||||
|
||||
it('escapet een naam uit de database', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 0 });
|
||||
const h = assetLabelHtml({ name: '<script>x</script> & co' });
|
||||
|
||||
assert.equal(h, '<span class="asset-name"><script>x</script> & co</span>');
|
||||
assert.equal(h.includes('<script>'), false, 'de tag mag niet als tag overleven');
|
||||
});
|
||||
|
||||
it('escapet GEEN aanhalingsteken, en dat is een grens en geen fout', () => {
|
||||
Object.assign(INSTELLING, { asset_show_name: 1, asset_show_symbol: 0 });
|
||||
assert.match(assetLabelHtml({ name: 'zeg "hallo"' }), /zeg "hallo"/,
|
||||
'als dit ooit verandert: controleer of de aanroepers nog steeds alleen elementinhoud zijn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('range-cycle: de gedeelde tijdvakknop', () => {
|
||||
it('kent drie tijdvakken met hun dagen', () => {
|
||||
assert.deepEqual(RANGE_DAYS, { week: 7, month: 30, year: 365 });
|
||||
});
|
||||
|
||||
it('cyclet week naar maand naar jaar en weer terug', () => {
|
||||
const gezien = [];
|
||||
const wrap = {};
|
||||
let huidig = 'week';
|
||||
wireRangeCycle(wrap, () => huidig, (r) => { huidig = r; gezien.push(r); });
|
||||
|
||||
const klik = { target: { closest: (sel) => (sel === '.range-cycle-btn' ? {} : null) } };
|
||||
wrap.onclick(klik); wrap.onclick(klik); wrap.onclick(klik);
|
||||
|
||||
assert.deepEqual(gezien, ['month', 'year', 'week']);
|
||||
});
|
||||
|
||||
it('doet niets bij een klik naast de knop', () => {
|
||||
let geroepen = 0;
|
||||
const wrap = {};
|
||||
wireRangeCycle(wrap, () => 'week', () => { geroepen++; });
|
||||
wrap.onclick({ target: { closest: () => null } });
|
||||
assert.equal(geroepen, 0);
|
||||
});
|
||||
|
||||
it('levert een knop met het label van het huidige tijdvak', () => {
|
||||
const h = rangeCycleButton('month');
|
||||
assert.match(h, /range-cycle-btn/);
|
||||
assert.match(h, /aria-label="/, 'een icoonknop hoort een toegankelijke naam te hebben');
|
||||
});
|
||||
|
||||
it('de widgets met een live spot cyclen dag, week, maand, jaar', () => {
|
||||
const gezien = [];
|
||||
const wrap = {};
|
||||
let huidig = 'day';
|
||||
wireRangeCycle(wrap, () => huidig, (r) => { huidig = r; gezien.push(r); }, SPOT_RANGE_DAYS);
|
||||
const klik = { target: { closest: (sel) => (sel === '.range-cycle-btn' ? {} : null) } };
|
||||
for (let i = 0; i < 4; i++) wrap.onclick(klik);
|
||||
assert.deepEqual(gezien, ['week', 'month', 'year', 'day']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rangeSeries', () => {
|
||||
const DAG = 86400000;
|
||||
const nu = Date.parse('2026-10-09T13:00:00Z');
|
||||
const middernacht = Math.floor(nu / DAG) * DAG;
|
||||
const maand = Array.from({ length: 30 }, (_, i) => [middernacht - (30 - i) * DAG, 100 + i]);
|
||||
|
||||
it('dag: precies de slotkoers van gisteren en de spot', () => {
|
||||
assert.deepEqual(rangeSeries(maand, 'day', 200, nu), [[middernacht - DAG, 129], [middernacht, 200]]);
|
||||
});
|
||||
|
||||
it('dag zonder spot: de laatste twee slotkoersen', () => {
|
||||
assert.deepEqual(rangeSeries(maand, 'day', null, nu), maand.slice(-2));
|
||||
});
|
||||
|
||||
it('week: de dagen binnen zeven dagen, met de spot als vandaag', () => {
|
||||
const uit = rangeSeries(maand, 'week', 200, nu);
|
||||
assert.equal(uit.length, 7);
|
||||
assert.deepEqual(uit.at(-1), [middernacht, 200]);
|
||||
});
|
||||
|
||||
it('te weinig punten in het tijdvak: de hele reeks', () => {
|
||||
const kort = [[middernacht - 40 * DAG, 90], [middernacht - 35 * DAG, 95]];
|
||||
assert.equal(rangeSeries(kort, 'week', null, nu), kort);
|
||||
});
|
||||
|
||||
const binnen = [[middernacht + 3600_000, 130], [middernacht + 7200_000, 135]];
|
||||
|
||||
it('dag met reeks: slotkoers aan het eind van zijn dag, de reeks, de spot op nu', () => {
|
||||
assert.deepEqual(rangeSeries(maand, 'day', 200, nu, binnen),
|
||||
[[middernacht, 129], ...binnen, [nu, 200]]);
|
||||
});
|
||||
|
||||
it('dag met reeks: punten vóór het einde van de slotdag of na nu vallen weg', () => {
|
||||
const rommel = [[middernacht - 60_000, 1], ...binnen, [nu + 60_000, 2]];
|
||||
assert.deepEqual(rangeSeries(maand, 'day', 200, nu, rommel), [[middernacht, 129], ...binnen, [nu, 200]]);
|
||||
});
|
||||
|
||||
it('dag met een lege of onbruikbare reeks: de rechte lijn van nu', () => {
|
||||
const rechte = [[middernacht - DAG, 129], [middernacht, 200]];
|
||||
assert.deepEqual(rangeSeries(maand, 'day', 200, nu, []), rechte);
|
||||
assert.deepEqual(rangeSeries(maand, 'day', 200, nu, [[nu + 1, 5]]), rechte);
|
||||
});
|
||||
|
||||
it('dag met reeks maar zonder spot: geen reeks, want er is geen eindpunt', () => {
|
||||
assert.deepEqual(rangeSeries(maand, 'day', null, nu, binnen), maand.slice(-2));
|
||||
});
|
||||
|
||||
it('de reeks raakt de andere tijdvakken niet', () => {
|
||||
assert.deepEqual(rangeSeries(maand, 'week', 200, nu, binnen), rangeSeries(maand, 'week', 200, nu));
|
||||
});
|
||||
});
|
||||
|
||||
describe('rangePointLabel: de hint bij een punt', () => {
|
||||
const datum = { day: 'numeric', month: 'short', year: 'numeric' };
|
||||
|
||||
it('binnen de dag: de prijs met de lokale datum en tijd', () => {
|
||||
const ms = Date.UTC(2026, 9, 16, 13, 7);
|
||||
const lokaal = new Date(ms).toLocaleString('nl-NL', { ...datum, hour: '2-digit', minute: '2-digit' });
|
||||
assert.equal(rangePointLabel('day', '€ 72.386', ms), `€ 72.386\n${lokaal}`);
|
||||
assert.match(lokaal, /2026/);
|
||||
});
|
||||
|
||||
it('in een ander tijdvak: de kalenderdag van het punt, zonder tijd', () => {
|
||||
const middernacht = Date.UTC(2026, 9, 16);
|
||||
const dag = new Date(middernacht).toLocaleDateString('nl-NL', { ...datum, timeZone: 'UTC' });
|
||||
assert.match(dag, /^16 okt\.? 2026$/);
|
||||
for (const range of Object.keys(RANGE_DAYS)) assert.equal(rangePointLabel(range, '€ 72.386', middernacht), `€ 72.386\n${dag}`);
|
||||
});
|
||||
|
||||
it('ook ten westen van Greenwich, waar middernacht UTC nog de vorige dag is', () => {
|
||||
const oud = process.env.TZ;
|
||||
process.env.TZ = 'America/New_York';
|
||||
try {
|
||||
assert.match(rangePointLabel('week', '€ 1', Date.UTC(2026, 9, 16)), /16 okt\.? 2026$/);
|
||||
} finally {
|
||||
if (oud === undefined) delete process.env.TZ; else process.env.TZ = oud;
|
||||
}
|
||||
});
|
||||
|
||||
it('zonder moment: alleen de prijs', () => {
|
||||
assert.equal(rangePointLabel('day', '€ 72.386', undefined), '€ 72.386');
|
||||
assert.equal(rangePointLabel('week', '€ 72.386', NaN), '€ 72.386');
|
||||
});
|
||||
|
||||
it('met een bron: die komt als laatste, en zonder bron blijft de hint zoals hij was', () => {
|
||||
const middernacht = Date.UTC(2026, 9, 16);
|
||||
const dag = new Date(middernacht).toLocaleDateString('nl-NL', { ...datum, timeZone: 'UTC' });
|
||||
assert.equal(rangePointLabel('week', '€ 1', middernacht, 'CoinGecko'), `€ 1\n${dag}\nCoinGecko`);
|
||||
assert.equal(rangePointLabel('week', '€ 1', middernacht, null), `€ 1\n${dag}`);
|
||||
assert.equal(rangePointLabel('week', '€ 1', NaN, 'CoinGecko'), '€ 1\nCoinGecko');
|
||||
});
|
||||
|
||||
it('zet elk deel op een eigen regel, de prijs bovenaan', () => {
|
||||
const regels = rangePointLabel('week', '€ 1', Date.UTC(2026, 9, 16), 'CoinGecko').split('\n');
|
||||
assert.equal(regels.length, 3);
|
||||
assert.equal(regels[0], '€ 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rangePointSources: de bron per punt', () => {
|
||||
const DAG = 86400000;
|
||||
const d0 = Date.UTC(2026, 9, 12);
|
||||
const nu = Date.UTC(2026, 9, 16, 14, 30);
|
||||
const vandaag = Date.UTC(2026, 9, 16);
|
||||
const grid = [[d0, 10], [d0 + DAG, 11], [d0 + 2 * DAG, 11], [d0 + 3 * DAG, 11]];
|
||||
const days = new Map([[d0, 'CoinGecko'], [d0 + DAG, 'Atreides Services']]);
|
||||
const dagTekst = ms => new Date(ms).toLocaleDateString('nl-NL', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
|
||||
|
||||
it('een week: eigen dagen bij naam, doorgetrokken dagen met de dag van de koers, de spot als laatste', () => {
|
||||
const use = rangeSeries(grid, 'week', 12, nu);
|
||||
const bron = rangePointSources(use, { range: 'week', spot: 12, nowMs: nu, live: 'Atreides Services (Bitvavo)', days });
|
||||
assert.equal(use[use.length - 1][0], vandaag);
|
||||
assert.equal(bron.get(d0), 'CoinGecko');
|
||||
assert.equal(bron.get(d0 + DAG), 'Atreides Services');
|
||||
assert.equal(bron.get(d0 + 2 * DAG), `Atreides Services, koers van ${dagTekst(d0 + DAG)}`);
|
||||
assert.equal(bron.get(vandaag), 'Atreides Services (Bitvavo)');
|
||||
});
|
||||
|
||||
it('zonder spot is het laatste punt geen actuele koers', () => {
|
||||
const use = rangeSeries(grid, 'week', null, nu);
|
||||
const bron = rangePointSources(use, { range: 'week', spot: null, nowMs: nu, live: 'Bitvavo', days });
|
||||
assert.equal(bron.get(d0 + 3 * DAG), `Atreides Services, koers van ${dagTekst(d0 + DAG)}`);
|
||||
assert.ok(![...bron.values()].includes('Bitvavo'));
|
||||
});
|
||||
|
||||
it('binnen de dag: een minuutpunt met een eigen bron draagt die, de rest de bron van de reeks', () => {
|
||||
const intraday = [[vandaag + 3600000, 11.5], [vandaag + 7200000, 11.8]];
|
||||
const use = rangeSeries(grid, 'day', 12, nu, intraday);
|
||||
const intradayAt = new Map([[vandaag + 3600000, 'Atreides Services (bitvavo)']]);
|
||||
const bron = rangePointSources(use, { range: 'day', spot: 12, nowMs: nu, live: 'Spot', intraday: 'Minuten', intradayAt, days });
|
||||
assert.equal(bron.get(vandaag + 3600000), 'Atreides Services (bitvavo)');
|
||||
assert.equal(bron.get(vandaag + 7200000), 'Minuten');
|
||||
assert.equal(bron.get(nu), 'Spot', 'de spot blijft de spot, ook met een kaart per punt');
|
||||
});
|
||||
|
||||
it('binnen de dag: de minuutreeks tussen de slotkoers en de spot', () => {
|
||||
const intraday = [[vandaag + 3600000, 11.5], [vandaag + 7200000, 11.8]];
|
||||
const use = rangeSeries(grid, 'day', 12, nu, intraday);
|
||||
const bron = rangePointSources(use, { range: 'day', spot: 12, nowMs: nu, live: 'Spot', intraday: 'Minuten', days });
|
||||
assert.equal(use.length, 4);
|
||||
assert.equal(bron.get(use[0][0]), `Atreides Services, koers van ${dagTekst(d0 + DAG)}`);
|
||||
assert.equal(bron.get(use[1][0]), 'Minuten');
|
||||
assert.equal(bron.get(use[2][0]), 'Minuten');
|
||||
assert.equal(bron.get(nu), 'Spot');
|
||||
});
|
||||
|
||||
it('binnen de dag zonder reeks: twee punten, en geen van beide heet minuutreeks', () => {
|
||||
const use = rangeSeries(grid, 'day', 12, nu, null);
|
||||
const bron = rangePointSources(use, { range: 'day', spot: 12, nowMs: nu, live: 'Spot', intraday: 'Minuten', days });
|
||||
assert.equal(use.length, 2);
|
||||
assert.equal(bron.get(vandaag), 'Spot');
|
||||
assert.notEqual(bron.get(use[0][0]), 'Minuten');
|
||||
});
|
||||
|
||||
it('zonder iets te zeggen: geen bron, ook niet voor een dag vóór de eerste eigen rij', () => {
|
||||
assert.equal(rangePointSources([], { range: 'week' }).size, 0);
|
||||
assert.equal(rangePointSources(null, { range: 'week' }).size, 0);
|
||||
const bron = rangePointSources([[d0 - DAG, 9], [d0, 10]], { range: 'week', nowMs: nu, days });
|
||||
assert.equal(bron.has(d0 - DAG), false);
|
||||
assert.equal(bron.get(d0), 'CoinGecko');
|
||||
});
|
||||
|
||||
it('namedDaySources: ids naar namen, en een onbekende provider valt weg in plaats van verkeerd te heten', () => {
|
||||
const dagBron = (providerId, sourceName = null) => ({ providerId, sourceName });
|
||||
const byDay = new Map([[d0, dagBron(1)], [d0 + DAG, dagBron(2)], [d0 + 2 * DAG, dagBron(99)]]);
|
||||
const named = namedDaySources(byDay, [{ id: 1, name: 'CoinGecko' }, { id: 2, name: 'Atreides Services' }]);
|
||||
assert.deepEqual([...named], [[d0, 'CoinGecko'], [d0 + DAG, 'Atreides Services']]);
|
||||
assert.equal(namedDaySources(byDay, null).size, 0);
|
||||
});
|
||||
|
||||
it('namedDaySources: de bron achter een dagkoers van de dienst staat erbij, zoals bij elke koers van de dienst', () => {
|
||||
const byDay = new Map([[d0, { providerId: 2, sourceName: 'Bitvavo' }]]);
|
||||
const named = namedDaySources(byDay, [{ id: 2, name: 'Atreides Services' }]);
|
||||
assert.equal(named.get(d0), 'Atreides Services (Bitvavo)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('intradayStart: waar de reeks binnen de dag begint', () => {
|
||||
const DAG = 86400000;
|
||||
const nu = Date.parse('2026-10-09T13:00:00Z');
|
||||
const middernacht = Math.floor(nu / DAG) * DAG;
|
||||
|
||||
it('het eind van de laatste slotdag vóór vandaag', () => {
|
||||
assert.equal(intradayStart([[middernacht - 2 * DAG, 1], [middernacht - DAG, 2]], nu), middernacht);
|
||||
});
|
||||
|
||||
it('een opgeslagen waarde van vandaag telt niet als slotkoers', () => {
|
||||
assert.equal(intradayStart([[middernacht - DAG, 2], [middernacht, 3]], nu), middernacht);
|
||||
});
|
||||
|
||||
it('na een weekend zonder slot: vanaf het eind van de laatste', () => {
|
||||
assert.equal(intradayStart([[middernacht - 3 * DAG, 1]], nu), middernacht - 2 * DAG);
|
||||
});
|
||||
|
||||
it('zonder reeks: vandaag middernacht', () => {
|
||||
assert.equal(intradayStart([], nu), middernacht);
|
||||
assert.equal(intradayStart(null, nu), middernacht);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serviceOrder: de plek van de dienst in de keten', () => {
|
||||
const app = (providerId, rank) => ({ providerId, rank, own: false });
|
||||
const zelf = (providerId, rank) => ({ providerId, rank, own: true });
|
||||
|
||||
it('eerst: zelf gekozen, dan de dienst, dan de keuze van de app', () => {
|
||||
const { ranks, service } = serviceOrder([app(1, 1), zelf(2, 2), app(3, 3)], 'first');
|
||||
assert.deepEqual([...ranks], [[2, 1], [1, 3], [3, 4]]);
|
||||
assert.equal(service, 2);
|
||||
});
|
||||
|
||||
it('eerst zonder eigen keuze: de dienst bovenaan', () => {
|
||||
const { ranks, service } = serviceOrder([app(1, 1)], 'first');
|
||||
assert.equal(service, 1);
|
||||
assert.equal(ranks.get(1), 2);
|
||||
});
|
||||
|
||||
it('als terugval: alle gekozen bronnen, dan de dienst', () => {
|
||||
const { ranks, service } = serviceOrder([zelf(2, 2), app(1, 1)], 'fallback');
|
||||
assert.deepEqual([...ranks], [[1, 1], [2, 2]]);
|
||||
assert.equal(service, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matrixCardHtml: de dienst als vaste kolom', () => {
|
||||
const bron = { id: 7, name: 'Bitvavo', isActive: 1, coverage: ['BTC'], endpoints: [{ capability: 'spot' }] };
|
||||
const btc = { id: 1, symbol: 'BTC', kind: 'crypto', name: 'Bitcoin' };
|
||||
const ctx = (mode, own, covered = [1]) => ({
|
||||
alle: [bron], providers: [bron], rateSource: new Map(),
|
||||
cell: new Map([['1|spot|7', { rank: 1, proposed: false, own }]]),
|
||||
service: { mode, covered: new Set(covered) },
|
||||
});
|
||||
const nummers = html => [...html.matchAll(/class="mx-rank[^"]*">(\d+)</g)].map(m => Number(m[1]));
|
||||
|
||||
it('stand eerst, Bitvavo door de app gekozen: de dienst op 1, Bitvavo op 2', () => {
|
||||
const html = matrixCardHtml(btc, ctx('first', false));
|
||||
assert.match(html, /Atreides Services/);
|
||||
assert.deepEqual(nummers(html), [2, 1]);
|
||||
assert.match(html, /mx-svc/);
|
||||
});
|
||||
|
||||
it('stand eerst, Bitvavo zelf gekozen: Bitvavo op 1, de dienst op 2', () => {
|
||||
assert.deepEqual(nummers(matrixCardHtml(btc, ctx('first', true))), [1, 2]);
|
||||
});
|
||||
|
||||
it('de dienstkolom is niet klikbaar', () => {
|
||||
const html = matrixCardHtml(btc, ctx('first', false));
|
||||
assert.doesNotMatch(html, /class="mx-cell"[^>]*>[^<]*<span class="mx-rank[^"]*is-service/);
|
||||
});
|
||||
|
||||
it('stand uit, of een asset dat de dienst niet dekt: geen kolom', () => {
|
||||
assert.doesNotMatch(matrixCardHtml(btc, ctx('off', false)), /Atreides Services/);
|
||||
assert.doesNotMatch(matrixCardHtml(btc, ctx('first', false, [])), /Atreides Services/);
|
||||
});
|
||||
});
|
||||
+21
-1
@@ -1,4 +1,4 @@
|
||||
// Generated, do not edit. sha256:f04b67a8f0dbd9eb295b842585345e76f56ef4fd377d5010897bcf893dbd0395
|
||||
// Generated, do not edit. sha256:0196b96345c180a741780bad0241a094640e956c6972ad08f5b4f400181c36c5
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installDom } from '../helpers/mini-dom.mjs';
|
||||
@@ -98,6 +98,26 @@ describe('a panel that would scroll gets wider on opening', () => {
|
||||
assert.equal(panel.classList.contains('is-wide'), false);
|
||||
});
|
||||
|
||||
it('a click on the icon pins the panel, and a click outside, Escape or the icon again closes it', () => {
|
||||
configureHint();
|
||||
setPageHint(page, { title: 'x', rows: ['<a href="#">link</a>'] });
|
||||
const hint = () => mount.querySelector('.hint');
|
||||
const btn = () => mount.querySelector('.hint-btn');
|
||||
dom.click(btn());
|
||||
assert.ok(hint().classList.contains('is-open'));
|
||||
assert.equal(btn().getAttribute('aria-expanded'), 'true');
|
||||
dom.click(mount.querySelector('.hint-panel a'));
|
||||
assert.ok(hint().classList.contains('is-open'), 'a click inside the panel keeps it open');
|
||||
dom.click(page);
|
||||
assert.equal(hint().classList.contains('is-open'), false);
|
||||
dom.click(btn());
|
||||
dom.key(dom.document.body, 'Escape');
|
||||
assert.equal(hint().classList.contains('is-open'), false);
|
||||
dom.click(btn());
|
||||
dom.click(btn());
|
||||
assert.equal(hint().classList.contains('is-open'), false);
|
||||
});
|
||||
|
||||
it('binds its listeners once per mount, however often a page announces', () => {
|
||||
const before = mount.listeners.length;
|
||||
setPageHint(page, { title: 'a', rows: [] });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Generated, do not edit. sha256:3b34cf97cc5aa20ba08ae8a6246f0235b943e5e85fc0d31e21707e06b2c2c371
|
||||
// Generated, do not edit. sha256:d235c44eb19e911ab77974e75ddb595cbc8249ee72a4b4a543123c373e4e6b5f
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { periodChoices, startPeriod, returnLine, idsForSelection, contributorsPerDay } from '../../client/wealth/return-view.js';
|
||||
import { periodChoices, startPeriod, returnLine, idsForSelection, contributorsPerDay, flatLine } from '../../client/wealth/return-view.js';
|
||||
import { dagreeks, seriesFor, meetVenster } from '../../client/wealth/series.js';
|
||||
import { windowTwr } from '../../client/wealth/performance.js';
|
||||
|
||||
@@ -94,6 +94,22 @@ describe('returnLine', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('flatLine', () => {
|
||||
it('geeft nul over de periode, eindigend op vandaag, oudste eerst', () => {
|
||||
const f = flatLine(7, '2026-10-29');
|
||||
assert.equal(f.amount, 0);
|
||||
assert.equal(f.pct, 0);
|
||||
assert.equal(f.labels.length, 7);
|
||||
assert.equal(f.labels[0], '2026-10-23');
|
||||
assert.equal(f.labels.at(-1), '2026-10-29');
|
||||
assert.ok(f.points.every(p => p.cumulative === 0));
|
||||
});
|
||||
|
||||
it('heeft minstens twee punten, ook voor één dag', () => {
|
||||
assert.deepEqual(flatLine(1, '2026-10-29').labels, ['2026-10-28', '2026-10-29']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('seriesFor met een groep', () => {
|
||||
it('is met alle assets erin hetzelfde als het totaal', () => {
|
||||
const r = reeks();
|
||||
|
||||
Reference in new issue
Block a user