chart/tests/e2e/chart.test.mjs
Chris Amow 3b3c06a1f3 - Drag a selected trendline body to reposition the entire line.
- Duplicate and delete from the line context menu.
     - Copies shift ten bars right.
     - Default names are up and down; copies become up 2, down 2, etc.
     - Exact local data receipt time including seconds.
     - Deployment timestamp removed.
     - Test cleanup no longer deletes drawings created from your browser.
     - JWT password session flow.
2026-08-11 05:46:09 -05:00

165 lines
6.4 KiB
JavaScript

/**
* The chart shows the right window, at the right density, without throwing.
*
* Every assertion here failed at some point on 2026-08-10 while all 96 Python
* tests passed, which is the whole argument for this file existing.
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
withChart, launch, openChart, chartState, selectTimeframe, assertNoPageErrors,
} from './helpers.mjs';
const TIMEFRAMES = ['1m', '5m', '15m', '30m', '1h', '1d'];
test('a first-time browser exchanges the friendly password for a session',
{ timeout: 180000 }, async () => {
const { browser, page } = await launch();
let statusRequests = 0;
let submitted = null;
try {
page.on('dialog', async dialog => {
assert.equal(dialog.message(), 'Password for this chart');
await dialog.accept('friendly passphrase');
});
await page.route('**/api/status', async route => {
statusRequests += 1;
if (statusRequests === 1) await route.fulfill({ status: 401, body: '{}' });
else await route.continue();
});
await page.route('**/api/login', async route => {
submitted = route.request().postDataJSON();
await route.fulfill({
status: 204,
headers: { 'set-cookie': 'chart-session=test; Path=/; HttpOnly; SameSite=Strict' },
});
});
await openChart(page);
assert.deepEqual(submitted, { password: 'friendly passphrase' });
assert.ok(statusRequests >= 2, 'status was not retried after login');
const unexpected = page.__errors.filter(error =>
!error.includes('server responded with a status of 401'));
assert.deepEqual(unexpected, [], `page errors: ${unexpected.join('; ')}`);
} finally {
await browser.close();
}
});
test('an existing browser token is migrated once and removed from local storage',
{ timeout: 180000 }, async () => {
const { browser, page } = await launch();
let presentedToken = null;
try {
await page.addInitScript(() => localStorage.setItem('chart-token', 'legacy-token'));
await page.route('**/api/login', async route => {
presentedToken = route.request().headers()['x-chart-token'];
await route.fulfill({
status: 204,
headers: { 'set-cookie': 'chart-session=test; Path=/; HttpOnly; SameSite=Strict' },
});
});
await openChart(page);
assert.equal(presentedToken, 'legacy-token');
assert.equal(await page.evaluate(() => localStorage.getItem('chart-token')), null);
assertNoPageErrors(page, assert);
} finally {
await browser.close();
}
});
test('the viewport opens on the live edge, not in the past', { timeout: 180000 }, async () => {
await withChart(async page => {
const state = await chartState(page);
assert.ok(state.bars > 0, 'the chart loaded no bars at all');
// The bug: setBars placed the viewport by logical index, and the daily MA
// series then prepended hundreds of points, sliding the view ~10 hours back
// while the data itself was perfectly current.
assert.equal(
state.visibleTo, state.lastTime,
'the newest bar is not the right-hand edge of the viewport',
);
assertNoPageErrors(page, assert);
});
});
test('the status row shows the exact local time data last arrived',
{ timeout: 180000 }, async () => {
await withChart(async page => {
await page.waitForFunction(() => {
const freshness = document.querySelector('.data-freshness b');
return freshness?.textContent.trim() !== '—';
});
const state = await page.evaluate(() => {
const freshness = document.querySelector('.data-freshness');
return {
freshness: freshness.textContent.trim(),
deployMetadata: document.querySelector('.app-meta'),
barHeight: document.querySelector('.statusbar').getBoundingClientRect().height,
};
});
assert.match(state.freshness, /^UPDATED\s+\d{1,2}:\d{2}:\d{2}\s*(AM|PM)?$/i);
assert.equal(state.deployMetadata, null);
assert.ok(state.barHeight <= 40, `status bar grew to ${state.barHeight}px`);
assertNoPageErrors(page, assert);
});
});
test('a daily crosshair shows the session date, not the previous evening',
{ timeout: 180000 }, async () => {
await withChart(async page => {
await selectTimeframe(page, '1d');
const state = await page.evaluate(() => {
const c = window.__chart;
const time = c.bars[c.bars.length - 1].t;
return {
actual: c.chart.options().localization.timeFormatter(time),
expected: new Date(time * 1000).toLocaleString(undefined, {
timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric',
}),
};
});
assert.equal(state.actual, state.expected);
assert.doesNotMatch(state.actual, /\d:\d/);
assertNoPageErrors(page, assert);
});
});
test('every timeframe gives one slot per candle', { timeout: 300000 }, async () => {
await withChart(async page => {
for (const tf of TIMEFRAMES) {
await selectTimeframe(page, tf);
const state = await chartState(page);
// The bug: a minute-resolution VWAP put its own points on the shared time
// scale, so an hourly chart spread 160 candles over 908 slots and drew
// them as slivers. Anything much above 1.0 means the scale is polluted.
const ratio = state.slots / Math.max(state.inView, 1);
assert.ok(
ratio > 0.8 && ratio < 1.3,
`${tf}: ${state.inView} candles spread over ${state.slots} slots (ratio ${ratio.toFixed(2)})`,
);
}
assertNoPageErrors(page, assert);
});
});
test('switching timeframes rapidly never throws', { timeout: 300000 }, async () => {
await withChart(async page => {
// The bug: bar events for the timeframe just left arrived after the new
// snapshot, and Lightweight Charts throws "Cannot update oldest data"
// rather than ignoring a stale bar, which took the whole app down.
for (let round = 0; round < 3; round += 1) {
for (const tf of ['1m', '1h', '15m', '1d', '5m']) {
await page.click(`.timeframes button:text-is("${tf}")`);
await page.waitForTimeout(700);
}
}
await page.waitForTimeout(2000);
const state = await chartState(page);
assert.ok(state.bars > 0, 'the chart lost its data while switching');
assertNoPageErrors(page, assert);
});
});