211 lines
10 KiB
JavaScript
211 lines
10 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 ? { t: c.anchor_t, p: c.anchor_p } : null;
|
|
}, TEXT);
|
|
assert.ok(anchor, 'the comment was not created');
|
|
|
|
await page.fill('input[aria-label="Filter drawings by text"]', TEXT);
|
|
await page.locator('.trendline-row').click();
|
|
await page.keyboard.press('ArrowUp');
|
|
await page.keyboard.press('Shift+ArrowRight');
|
|
await page.waitForFunction(([text, original]) => {
|
|
const c = window.__chart;
|
|
const comment = c.comments.find(value => value.note === text);
|
|
return comment?.anchor_p === original.p + 0.25
|
|
&& Math.round(c.indexAt(comment.anchor_t) - c.indexAt(original.t)) === 4;
|
|
}, [TEXT, anchor]);
|
|
|
|
// 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');
|
|
const controls = await row.locator('.drawing-controls>*').evaluateAll(nodes =>
|
|
nodes.map(node => node.classList.contains('color-picker') ? 'color' : node.className));
|
|
assert.deepEqual(controls, ['collapse-toggle', 'color'],
|
|
'the comment color picker is not the rightmost control');
|
|
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.fill('input[aria-label="Filter drawings by text"]', TEXT);
|
|
await page.locator('.trendline-row').click();
|
|
await page.keyboard.press('ArrowRight');
|
|
await page.waitForFunction(([text, left]) => {
|
|
const node = [...document.querySelectorAll('.chart-comment')].find(value => value.title === text);
|
|
return node && Math.round(node.getBoundingClientRect().left) === left + 4;
|
|
}, [TEXT, before.left]);
|
|
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 + 4, '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, 'Mark');
|
|
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-family-swatches>button[title="blue1 (#A3CCFF)"]').count(), 1,
|
|
'the symbol color palette does not expose named presets');
|
|
assert.equal(await page.locator('.symbol-head-color .color-family-swatches>button[aria-label^="Use "]').count(), 24,
|
|
'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);
|
|
});
|
|
});
|