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>
110 lines
4.5 KiB
JavaScript
110 lines
4.5 KiB
JavaScript
/**
|
|
* Comments stay where they were put, across zoom, scroll and timeframe.
|
|
*
|
|
* A comment is stored as a drawing but must never behave like a level, so the
|
|
* last test here guards the invariant that matters most: a note on a chart can
|
|
* never end up in a confluence cluster or fire a push notification.
|
|
*/
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
withChart, chartBox, at, armTool, selectTimeframe, assertNoPageErrors,
|
|
} from './helpers.mjs';
|
|
|
|
const TEXT = 'e2e placement check';
|
|
|
|
/** Place a comment at a point on the chart. */
|
|
async function placeComment(page, box, fx, fy, { floating = false } = {}) {
|
|
await armTool(page, 'Comment');
|
|
await page.fill('textarea[aria-label="Comment text"]', TEXT);
|
|
if (floating) {
|
|
await page.evaluate(() => {
|
|
const body = [...document.querySelectorAll('.tool-body')].find(b => b.querySelector('textarea'));
|
|
const box_ = body.querySelector('input[type=checkbox]');
|
|
if (!box_.checked) box_.click();
|
|
});
|
|
await page.waitForTimeout(300);
|
|
}
|
|
const point = at(box, fx, fy);
|
|
await page.mouse.move(point.x, point.y);
|
|
await page.mouse.down(); await page.mouse.up();
|
|
await page.waitForTimeout(1800);
|
|
}
|
|
|
|
const commentNode = page => page.evaluate(text => {
|
|
const node = [...document.querySelectorAll('.chart-comment')].find(n => n.title === text);
|
|
if (!node) return null;
|
|
return {
|
|
left: Math.round(node.getBoundingClientRect().left),
|
|
collapsed: node.classList.contains('collapsed'),
|
|
parked: node.classList.contains('off-left') || node.classList.contains('off-right'),
|
|
};
|
|
}, TEXT);
|
|
|
|
test('a pinned comment keeps its bar across timeframes', { timeout: 300000 }, async () => {
|
|
await withChart(async page => {
|
|
const box = await chartBox(page);
|
|
await selectTimeframe(page, '30m');
|
|
await placeComment(page, box, 0.55, 0.40);
|
|
|
|
const anchor = await page.evaluate(t => {
|
|
const c = window.__chart.comments.find(x => x.note === t);
|
|
return c ? c.anchor_t : null;
|
|
}, TEXT);
|
|
assert.ok(anchor, 'the comment was not created');
|
|
|
|
// The bug: timeToCoordinate answers only for exact data points, so a 30m
|
|
// bucket returned null on 15m and null was read as "off the left edge".
|
|
for (const tf of ['15m', '1h', '30m']) {
|
|
await selectTimeframe(page, tf);
|
|
const node = await commentNode(page);
|
|
assert.ok(node, `the comment vanished on ${tf}`);
|
|
assert.equal(node.parked, false, `the comment was edge-parked on ${tf} despite being in view`);
|
|
}
|
|
assertNoPageErrors(page, assert);
|
|
});
|
|
});
|
|
|
|
test('clicking a comment collapses it', { timeout: 180000 }, async () => {
|
|
await withChart(async page => {
|
|
const box = await chartBox(page);
|
|
await placeComment(page, box, 0.5, 0.35);
|
|
assert.equal((await commentNode(page)).collapsed, false);
|
|
// By title, not by class: the dev store is shared, so other people's
|
|
// comments are on the chart too and `.chart-comment` matches them first.
|
|
await page.click(`.chart-comment[title="${TEXT}"]`);
|
|
await page.waitForTimeout(1200);
|
|
assert.equal((await commentNode(page)).collapsed, true, 'clicking did not collapse it');
|
|
assertNoPageErrors(page, assert);
|
|
});
|
|
});
|
|
|
|
test('a floating comment holds its position while the chart scrolls', { timeout: 180000 }, async () => {
|
|
await withChart(async page => {
|
|
const box = await chartBox(page);
|
|
await placeComment(page, box, 0.4, 0.3, { floating: true });
|
|
const before = await commentNode(page);
|
|
assert.ok(before, 'the floating comment was not created');
|
|
await page.evaluate(() => window.__chart.chart.timeScale().scrollToPosition(-300, false));
|
|
await page.waitForTimeout(1200);
|
|
const after = await commentNode(page);
|
|
assert.equal(after.left, before.left, 'a floating comment moved with the chart');
|
|
assertNoPageErrors(page, assert);
|
|
});
|
|
});
|
|
|
|
test('a comment is never a level', { timeout: 180000 }, async () => {
|
|
await withChart(async page => {
|
|
const box = await chartBox(page);
|
|
await placeComment(page, box, 0.5, 0.5);
|
|
// Levels cluster and alert. A comment reaching that list would push a
|
|
// phone notification about a piece of text.
|
|
const leaked = await page.evaluate(t => {
|
|
const note = window.__chart.comments.find(x => x.note === t);
|
|
if (!note) return 'comment missing';
|
|
return window.__chart.levels.some(level => level.id === note.id);
|
|
}, TEXT);
|
|
assert.equal(leaked, false, 'a comment appeared in the level list');
|
|
assertNoPageErrors(page, assert);
|
|
});
|
|
});
|