chart/tests/e2e/touch.test.mjs
2026-08-14 13:58:44 -05:00

159 lines
7.3 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { withChart, assertNoPageErrors } from './helpers.mjs';
test('touch tap selects a drawing and horizontal and vertical pinches scale only their own axes',
{ timeout: 180000 }, async () => {
await withChart(async page => {
const label = `e2e touch ${Date.now()}`;
const created = await page.evaluate(async name => {
const price = window.__chart.bars.at(-1).c;
const response = await fetch('/api/lines/price', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ price, note: name }),
});
return response.json();
}, label);
await page.waitForFunction(id =>
window.__chart.levels.some(level => level.id === id), created.id);
const cdp = await page.context().newCDPSession(page);
const geometry = await page.evaluate(id => {
const chart = window.__chart;
const level = chart.levels.find(value => value.id === id);
const plot = chart.plotCanvas().getBoundingClientRect();
return {
tap: { x: plot.left + plot.width * 0.5, y: plot.top + chart.candles.priceToCoordinate(level.anchor_p) },
horizontal: {
x1: plot.left + plot.width * 0.35, y1: plot.top + plot.height * 0.5,
x2: plot.left + plot.width * 0.65, y2: plot.top + plot.height * 0.5,
},
vertical: {
x1: plot.left + plot.width * 0.5, y1: plot.top + plot.height * 0.35,
x2: plot.left + plot.width * 0.5, y2: plot.top + plot.height * 0.65,
},
};
}, created.id);
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart', touchPoints: [{ ...geometry.tap, id: 1 }],
});
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await page.waitForFunction(id => window.__chart.selectedLineId === id, created.id);
assert.match(await page.locator('.chart-tooltip').textContent(), new RegExp(label));
const state = () => page.evaluate(() => {
const chart = window.__chart;
const range = chart.chart.timeScale().getVisibleLogicalRange();
return {
timeFrom: range.from,
timeTo: range.to,
timeSpan: range.to - range.from,
priceSpan: Math.abs(chart.candles.coordinateToPrice(80) - chart.candles.coordinateToPrice(500)),
pageScale: window.visualViewport?.scale || 1,
};
});
const pinch = async (points, axis) => {
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [
{ x: points.x1, y: points.y1, id: 1 },
{ x: points.x2, y: points.y2, id: 2 },
],
});
for (let step = 1; step <= 8; step += 1) {
const delta = step * 8;
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [
{ x: points.x1 - (axis === 'x' ? delta : 0), y: points.y1 - (axis === 'y' ? delta : 0), id: 1 },
{ x: points.x2 + (axis === 'x' ? delta : 0), y: points.y2 + (axis === 'y' ? delta : 0), id: 2 },
],
});
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await page.waitForTimeout(300);
};
const before = await state();
await pinch(geometry.horizontal, 'x');
const horizontal = await state();
assert.notEqual(Math.round(horizontal.timeSpan), Math.round(before.timeSpan),
'horizontal pinch did not change the time scale');
assert.ok(Math.abs(horizontal.priceSpan - before.priceSpan) < 0.1,
`horizontal pinch changed the price scale (${before.priceSpan} to ${horizontal.priceSpan})`);
await pinch(geometry.vertical, 'y');
const vertical = await state();
assert.ok(Math.abs(vertical.priceSpan - horizontal.priceSpan) > 0.1,
'vertical pinch did not change the price scale');
assert.ok(Math.abs(vertical.timeSpan - horizontal.timeSpan) < 0.1,
'vertical pinch changed the time scale');
assert.equal(vertical.pageScale, before.pageScale, 'pinch zoomed the browser page');
const plot = await page.evaluate(() => {
const rect = window.__chart.plotCanvas().getBoundingClientRect();
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
});
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: plot.left + plot.width * 0.5, y: plot.top + plot.height * 0.5, id: 1 }],
});
for (let step = 1; step <= 8; step += 1) {
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: plot.left + plot.width * 0.5 + step * 10, y: plot.top + plot.height * 0.5, id: 1 }],
});
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await page.waitForTimeout(300);
const panned = await state();
assert.ok(Math.abs(panned.timeSpan - vertical.timeSpan) < 0.1,
'one-finger pan changed the time zoom');
assert.ok(Math.abs(panned.timeFrom - vertical.timeFrom) > 0.5,
'one-finger horizontal drag did not pan the chart');
assertNoPageErrors(page, assert);
}, { viewport: { width: 1200, height: 800 }, hasTouch: true, isMobile: true });
});
test('live-price autoscroll restores autoscaling only when enabled',
{ timeout: 180000 }, async () => {
await withChart(async page => {
await page.locator('.config-section summary').click();
const autoscroll = page.locator('.config-section label:has-text("Autoscroll to live price") input');
await autoscroll.uncheck();
await page.waitForFunction(() => window.__chart.autoScrollLivePrice === false);
const offscreenPrice = await page.evaluate(() => {
const c = window.__chart;
const last = c.bars.at(-1);
// This is the same manual range created by a vertical or horizontal pinch.
c.manualPriceRange = { minValue: last.c - 1, maxValue: last.c + 1 };
c.candles.applyOptions({ autoscaleInfoProvider: c.candleAutoscaleProvider });
const price = last.c - 10;
c.updateBar({ ...last, o: price, h: price, l: price, c: price });
return price;
});
const disabledState = await page.evaluate(() => Boolean(window.__chart.manualPriceRange));
assert.equal(disabledState, true, 'disabled autoscroll reset the manual range');
await autoscroll.check();
await page.waitForFunction(() => window.__chart.autoScrollLivePrice === true);
await page.waitForTimeout(100);
const state = await page.evaluate(price => {
const c = window.__chart;
return {
manualPriceRange: c.manualPriceRange,
y: c.candles.priceToCoordinate(price),
plotHeight: c.plotCanvas().clientHeight,
};
}, offscreenPrice);
assert.equal(state.manualPriceRange, null, 'enabled autoscroll kept the manual range');
assert.ok(state.y >= 0 && state.y <= state.plotHeight,
`live price remained off-screen at y=${state.y}, plot=${state.plotHeight}`);
assertNoPageErrors(page, assert);
}, { viewport: { width: 1200, height: 800 }, hasTouch: true, isMobile: true });
});