chart/tests/e2e/chart.test.mjs
Chris Amow 501afb7792 Snap to the nearest extreme on screen, and add an e2e suite
Snapping took the bar sharing the cursor's time and then its nearer extreme,
which ignored how far away that extreme was. Pointing below a candle snapped to
that candle's low however distant, while the extreme genuinely under the cursor
was never considered. Zoomed out to some 360 bars at three pixels apart, that
made hitting the bar you meant a matter of several tries. snapPoint now scans
six bars either side and takes the extreme nearest in pixels.

Proven by probe: with the cursor sitting exactly on one bar's low but nudged two
pixels so coordinateToTime resolves to its neighbour, the snap takes the extreme
under the cursor rather than the neighbour's.

A report of the snap dot appearing "way above the bar" turned out to be the dot
landing correctly on the low while the cursor was 151 points below it: the right
price scale keeps a bottom margin of 0.1 and the volume overlay is drawn in it,
so the lower fifth of the pane sits below every candle.

bin/e2e runs tests/e2e against the dev stack inside the playwright service —
Node's own test runner, no dependency added here, since Playwright is global in
that container. Eleven cases, each one a bug that shipped: the viewport parked
ten hours in the past, hourly candles drawn as slivers, stale bar events
throwing, comments drifting across a timeframe switch, and three ways a
trendline anchor could disagree with its preview. Not one was reachable from
pytest. Tests delete any drawing they create, because the dev store is shared
with whoever is looking at the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:16:09 -05:00

62 lines
2.6 KiB
JavaScript

/**
* The chart shows the right window, at the right density, without throwing.
*
* Every assertion here failed at some point on 2026-08-10 while all 96 Python
* tests passed, which is the whole argument for this file existing.
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { withChart, chartState, selectTimeframe, assertNoPageErrors } from './helpers.mjs';
const TIMEFRAMES = ['1m', '5m', '15m', '30m', '1h', '1d'];
test('the viewport opens on the live edge, not in the past', { timeout: 180000 }, async () => {
await withChart(async page => {
const state = await chartState(page);
assert.ok(state.bars > 0, 'the chart loaded no bars at all');
// The bug: setBars placed the viewport by logical index, and the daily MA
// series then prepended hundreds of points, sliding the view ~10 hours back
// while the data itself was perfectly current.
assert.equal(
state.visibleTo, state.lastTime,
'the newest bar is not the right-hand edge of the viewport',
);
assertNoPageErrors(page, assert);
});
});
test('every timeframe gives one slot per candle', { timeout: 300000 }, async () => {
await withChart(async page => {
for (const tf of TIMEFRAMES) {
await selectTimeframe(page, tf);
const state = await chartState(page);
// The bug: a minute-resolution VWAP put its own points on the shared time
// scale, so an hourly chart spread 160 candles over 908 slots and drew
// them as slivers. Anything much above 1.0 means the scale is polluted.
const ratio = state.slots / Math.max(state.inView, 1);
assert.ok(
ratio > 0.8 && ratio < 1.3,
`${tf}: ${state.inView} candles spread over ${state.slots} slots (ratio ${ratio.toFixed(2)})`,
);
}
assertNoPageErrors(page, assert);
});
});
test('switching timeframes rapidly never throws', { timeout: 300000 }, async () => {
await withChart(async page => {
// The bug: bar events for the timeframe just left arrived after the new
// snapshot, and Lightweight Charts throws "Cannot update oldest data"
// rather than ignoring a stale bar, which took the whole app down.
for (let round = 0; round < 3; round += 1) {
for (const tf of ['1m', '1h', '15m', '1d', '5m']) {
await page.click(`.timeframes button:text-is("${tf}")`);
await page.waitForTimeout(700);
}
}
await page.waitForTimeout(2000);
const state = await chartState(page);
assert.ok(state.bars > 0, 'the chart lost its data while switching');
assertNoPageErrors(page, assert);
});
});