/** * 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); await page.fill('input[aria-label="Filter drawings by text"]', TEXT); const row = page.locator('.trendline-row'); await row.locator('.drawing-secondary>span').click(); assert.equal(await page.locator(`.chart-comment[title="${TEXT}"]`).evaluate(node => node.classList.contains('focus')), true, 'selecting the comment row did not point out the comment on the chart'); await page.keyboard.press('Escape'); assert.equal(await row.evaluate(node => node.classList.contains('selected')), false, 'Escape did not clear comment selection'); const chartComment = page.locator(`.chart-comment[title="${TEXT}"]`); // The body selects without collapsing; collapse is deliberately confined // to the narrow control on the left. await chartComment.locator('.chart-comment-text').click(); assert.equal(await row.evaluate(node => node.classList.contains('selected')), true, 'clicking the chart comment did not select its drawing row'); assert.equal((await commentNode(page)).collapsed, false, 'selecting the comment also collapsed it'); await chartComment.locator('.chart-comment-collapse').click(); 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); }); }); test('a symbol can be dropped at a price and dragged to a new one', { timeout: 180000, skip: 'quarantined: persisted off-screen symbols can overlap and intercept the test gesture', }, async () => { await withChart(async page => { const box = await chartBox(page); await armTool(page, 'Symbol'); assert.equal(await page.locator('.symbol-palette>button').count(), 12, 'the two-row symbol palette is incomplete'); assert.equal(await page.locator('.symbol-palette>button[title="Go"] .fa-play').count(), 1, 'the Go symbol is not represented by a play triangle'); await page.locator('.symbol-head-color>summary').click(); assert.equal(await page.locator('.symbol-head-color .color-popover>button[title="Cyan"]').count(), 1, 'the symbol color palette does not expose named presets'); assert.equal(await page.locator('.symbol-head-color .color-popover>button[aria-label^="Use "]').count(), 16, 'the symbol color palette does not contain all presets'); await page.locator('.symbol-head-color .palette-close').click(); await page.locator('button[aria-label="Skull"]').dragTo(page.locator('#chart'), { targetPosition: { x: box.w * 0.55, y: box.h * 0.4 }, }); await page.waitForFunction(() => window.__chart.comments.some(comment => comment.kind === 'symbol' && comment.icon === 'skull')); const symbolTool = page.locator('.tool:has(.symbol-head)'); assert.equal(await symbolTool.locator('.tool-body').isVisible(), true, 'placing a symbol closed its tool palette'); assert.equal(await symbolTool.evaluate(node => node.classList.contains('armed')), false, 'placing a symbol left the chart armed to create another one'); const created = await page.evaluate(() => { const symbol = window.__chart.comments.find(comment => comment.kind === 'symbol' && comment.icon === 'skull'); return { id: symbol.id, anchor_t: symbol.anchor_t, anchor_p: symbol.anchor_p }; }); assert.equal(created.anchor_p * 4, Math.round(created.anchor_p * 4), 'the dropped symbol price was not tick-snapped'); const node = page.locator(`.chart-comment.symbol[data-drawing-id="${created.id}"]`); assert.equal(await node.locator('.fa-skull').count(), 1, 'the symbol did not render with its approved Font Awesome icon'); assert.equal(await page.evaluate(id => window.__chart.levels.some(level => level.id === id), created.id), false, 'the symbol leaked into chart levels'); assert.equal(await node.locator('.chart-comment-grab').isVisible(), false, 'an unselected symbol exposed its grab handle'); await node.locator('.chart-comment-text').click(); assert.equal(await node.locator('.chart-comment-grab').isVisible(), true, 'a selected symbol did not expose its grab handle'); const before = await node.boundingBox(); const grip = await node.locator('.chart-comment-grab').boundingBox(); await page.mouse.move(grip.x + grip.width / 2, grip.y + grip.height / 2); await page.mouse.down(); await page.mouse.move(grip.x + grip.width / 2 + 70, grip.y + grip.height / 2 + 45, { steps: 10 }); await page.mouse.up(); await page.waitForFunction(previous => { const symbol = window.__chart.comments.find(comment => comment.id === previous.id); return symbol && (symbol.anchor_t !== previous.anchor_t || symbol.anchor_p !== previous.anchor_p); }, created); const movedPrice = await page.evaluate(id => window.__chart.comments.find(comment => comment.id === id).anchor_p, created.id); assert.equal(movedPrice * 4, Math.round(movedPrice * 4), 'the dragged symbol price was not tick-snapped'); assertNoPageErrors(page, assert); }); });