// Headless guard for the trace-chart viewer (chart-viewer.js) ordinal-label honesty. // // Property protected: the ORDINAL x spine collapses gaps to indices [0..n-1], but the // axis tick labels and the legend value-readout must still report the REAL timestamp // xs[i] for each collapsed index — not the index itself. The gap-collapse is only // honest if the axis tells the truth about which timestamp a collapsed position is. // A regression that returned the index (String(pos)) instead of xs[round(pos)] would // still pass "the formatter is a function" but silently mislabel every tick after a // gap. Continuous mode (no collapse) must carry NO index->ts formatter, so its tick is // the raw timestamp, and its legend echoes the timestamp verbatim. // // The fixture's timestamps [100, 200, 9000] diverge sharply from the index spine // [0, 1, 2] across a large non-trading gap, so an index-vs-timestamp confusion fails // loudly. Drives the exported pure buildCharts; uPlot/DOM never instantiated. import { createRequire } from "node:module"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const require = createRequire(import.meta.url); const here = dirname(fileURLToPath(import.meta.url)); const traces = JSON.parse(readFileSync(join(here, "fixtures", "gappy-traces.json"), "utf8")); const fail = (msg) => { console.error("FAIL: " + msg); process.exit(1); }; const eq = (a, b, msg) => { if (a !== b) fail(`${msg} (got ${JSON.stringify(a)}, want ${JSON.stringify(b)})`); }; const sameArr = (a, b, msg) => { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || a.some((v, i) => v !== b[i])) fail(msg); }; // No `document` global -> the browser mount is skipped; we get only the pure export. global.window = { AURA_TRACES: traces }; const { buildCharts } = require(join(here, "..", "assets", "chart-viewer.js")); const xs = traces.xs; // [100, 200, 9000] const idx = xs.map((_, i) => i); // [0, 1, 2] // ordinal (default): the x AXIS formatter maps each index split back to its real ts. const ov = buildCharts(traces, "overlay")[0]; const axisValues = ov.opts.axes[0].values; if (typeof axisValues !== "function") fail("ordinal x-axis lacks a values formatter"); const labels = axisValues(null, idx); // uPlot passes the tick positions (indices) sameArr(labels, xs.map(String), "ordinal axis must label each index with its real xs[i], not the index"); // ordinal: the LEGEND x value-readout maps a cursor index back to its real ts; a null // cursor reads as the gap placeholder "--", never as a coerced number. const legend = ov.opts.series[0].value; if (typeof legend !== "function") fail("ordinal x series lacks a value-readout formatter"); eq(legend(null, 0), String(xs[0]), "legend readout at index 0 must be the real ts xs[0]"); eq(legend(null, 2), String(xs[2]), "legend readout at index 2 must be the real ts xs[2] (across the gap)"); eq(legend(null, null), "--", "legend readout at a null cursor must be the gap placeholder --"); // continuous: NO index->ts formatter on the axis (uPlot's default tick = the raw ts), // and the legend echoes the timestamp verbatim. const ovc = buildCharts(traces, "overlay", "continuous")[0]; eq(typeof ovc.opts.axes[0].values, "undefined", "continuous axis must carry no index->ts formatter"); eq(ovc.opts.series[0].value(null, xs[2]), String(xs[2]), "continuous legend echoes the raw timestamp"); console.log("OK — ordinal labels report real timestamps (not indices); continuous echoes raw ts."); process.exit(0);