// node scripts/test-graphcols.mjs — the rule in src/graphcols.ts, on a curve scrolled one point at a time. // (No test runner in this project: esbuild, which vite already brings, turns the one file into something node can load.) import { buildSync } from 'esbuild' import { pathToFileURL } from 'node:url' import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' const out = join(mkdtempSync(join(tmpdir(), 'graphcols-')), 'graphcols.mjs') buildSync({ entryPoints: [new URL('../src/graphcols.ts', import.meta.url).pathname], outfile: out, format: 'esm', logLevel: 'error' }) const { columnsOf, columnsAt } = await import(pathToFileURL(out).href) const fails = [] const check = (ok, what, detail) => { if (!ok) fails.push(what + (detail !== undefined ? ' — ' + JSON.stringify(detail).slice(0, 200) : '')) } const CAP = 300, COLS = 90 // the net graph: 300 points of history in about 90 dot-columns const base = Array.from({ length: CAP }, (_, i) => (i === 100 ? 900 : i === 180 ? 500 : i === 251 ? 700 : 10)) const spikes = (cols) => cols.filter((v) => v > 100).sort((a, b) => b - a).join(',') // what Top drew before 2026-09-21: the nearest point for each column, counted from the right edge const nearest = (values, cap, cols) => { const n = Math.min(values.length, cap), seen = []; for (let c = 0; c < cols; c++) { const i = Math.floor((c * cap) / cols) - (cap - n); if (i >= 0) seen.push(values[values.length - n + i]) } return seen } let values = base.slice(), advance = CAP - 1, prev = null const oldShapes = new Set(), newShapes = new Set() for (let step = 0; step < 40; step++) { oldShapes.add(spikes(nearest(values, CAP, COLS))) const cols = columnsOf(values, advance, CAP, COLS) newShapes.add(spikes(cols)) if (prev) check([0, 1].some((sh) => cols.slice(0, -1).every((v, j) => prev[j + sh] === undefined || prev[j + sh] === v)), `frame ${step} is the one before, slid left by a column at most`) prev = cols values.push(10); values.shift(); advance++ } check(oldShapes.size > 1, 'the test reproduces the fault it guards against (spikes flickering under nearest-point drawing)', [...oldShapes]) check(newShapes.size === 1 && [...newShapes][0] === '900,700,500', 'every spike is drawn, at its height, on every frame', [...newShapes]) check(columnsOf(base, CAP - 1, CAP, COLS).length === COLS, 'a full curve fills the box') const young = columnsOf(base.slice(-30), 29, CAP, COLS).length check(young >= 8 && young <= 10, 'a tenth of a history takes a tenth of the box, from the right', young) const wide = columnsOf(base, CAP - 1, CAP, 390) check(wide.length === 390 && Math.max(...wide) === 900, 'a box wider than the history is filled, nothing invented', wide.length) check(columnsOf([], 0, CAP, COLS).length === 0 && columnsOf([5], 0, 2, 50).length >= 1, 'empty and single-point curves') // Sliding with the ticks: four ticks to a point (a 250 ms rate, a 1 s stride). Every tick moves // the graph left by the same fraction of a column, the newest column grows in from the right, and // a column, once finished, keeps its value wherever it has slid to. for (const cols of [COLS, 390]) { let v = base.slice(), adv = CAP - 1, prevAt = null, back = 0, jump = 0, held = 0 const was = new Map() // a finished column's value, by its number for (let t = 0; t < 4 * 30; t++) { const now = adv + ((t % 4) + 1) / 4 const { cols: seen, edge, last } = columnsAt(v, adv, now, CAP, cols) check(edge > 0 && edge <= 1, `edge in (0, 1] (${cols} cols)`, edge) check(seen.length >= cols - 2 && seen.length <= cols + 1, `the box is filled, give or take the sliver (${cols} cols)`, seen.length) const at = last + edge // how far the graph has slid, in columns if (prevAt !== null) { if (at < prevAt - 1e-9) back++; if (at > prevAt + 1 + 1e-9) jump++; if (Math.abs(at - prevAt) < 1e-9) held++ } prevAt = at seen.forEach((x, i) => { const k = last - (seen.length - 1 - i); if (k < last - 1) { if (was.has(k) && was.get(k) !== x) fails.push(`column ${k} changed after it was finished`); was.set(k, x) } }) if ((t % 4) === 3) { v.push(10); v.shift(); adv++ } } check(back === 0 && jump === 0, `the graph only slides left, less than a column a tick (${cols} cols)`, { back, jump }) check(held < 4 * 30 / 4, `the graph moves on almost every tick, not once a point (${cols} cols)`, held) } // a point per tick (stride == rate) is the old layout exactly check(JSON.stringify(columnsAt(base, CAP - 1, CAP, CAP, COLS).cols.slice(-COLS)) === JSON.stringify(columnsOf(base, CAP - 1, CAP, COLS)), 'columnsOf is columnsAt at a whole point') if (fails.length) { console.error('graphcols: FAIL\n ' + fails.join('\n ')); process.exit(1) } console.log('graphcols: ok')