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 oneFinger = await state(); assert.ok(Math.abs(oneFinger.timeFrom - vertical.timeFrom) < 0.5, 'one-finger drag panned the chart'); const midY = plot.top + plot.height * 0.5; await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [ { x: plot.left + plot.width * 0.4, y: midY, id: 1 }, { x: plot.left + plot.width * 0.6, y: midY, id: 2 }, ], }); for (let step = 1; step <= 8; step += 1) { await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [ { x: plot.left + plot.width * 0.4 + step * 10, y: midY, id: 1 }, { x: plot.left + plot.width * 0.6 + step * 10, y: midY, id: 2 }, ], }); } await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); await page.waitForTimeout(300); const panned = await state(); assert.ok(Math.abs(panned.timeSpan - oneFinger.timeSpan) < 0.1, 'two-finger pan changed the time zoom'); assert.ok(Math.abs(panned.timeFrom - oneFinger.timeFrom) > 0.5, 'two-finger 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 }); });