/** * 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('diagnostic capture uploads a PNG and adds its capability ID to Events', { timeout: 180000, skip: 'quarantined: display-media mocks and async upload completion are not yet reliable', }, async () => { const { browser, page } = await launch(); let uploaded = null; try { await page.addInitScript(() => localStorage.setItem('chart-diag', '1')); await page.route('**/api/debug/captures', async route => { uploaded = { contentType: route.request().headers()['content-type'], metadata: JSON.parse(Buffer.from( route.request().headers()['x-capture-metadata'], 'base64').toString()), }; await route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ id: 'c-TESTCAPTURE1', url: '/api/debug/captures/c-TESTCAPTURE1', }), }); }); await openChart(page); await page.evaluate(() => { Object.defineProperty(navigator, 'mediaDevices', { configurable: true, value: { getDisplayMedia: async () => ({ getTracks: () => [{ stop() {} }] }) }, }); Object.defineProperty(HTMLMediaElement.prototype, 'srcObject', { configurable: true, set() { setTimeout(() => this.onloadedmetadata?.(), 0); }, }); Object.defineProperty(HTMLVideoElement.prototype, 'videoWidth', { configurable: true, get: () => 800, }); Object.defineProperty(HTMLVideoElement.prototype, 'videoHeight', { configurable: true, get: () => 600, }); HTMLMediaElement.prototype.play = async function play() {}; HTMLVideoElement.prototype.requestVideoFrameCallback = function callback(done) { done(); }; HTMLCanvasElement.prototype.getContext = () => ({ drawImage() {} }); HTMLCanvasElement.prototype.toBlob = function toBlob(done) { done(new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])], { type: 'image/png', })); }; }); await page.locator('.diag-capture').click(); await page.waitForFunction(() => [...document.querySelectorAll('.alert-entry')] .some(entry => entry.textContent.includes('c-TESTCAPTURE1'))); assert.equal(uploaded.contentType, 'image/png'); assert.equal(uploaded.metadata.timeframe, '1m'); assert.equal(uploaded.metadata.image_width, 800); assert.equal(uploaded.metadata.image_height, 600); const event = page.locator('.alert-entry.capture a'); assert.match(await event.textContent(), /CAPTURE c-TESTCAPTURE1 · 1m/); assert.match(await event.getAttribute('href'), /\/api\/debug\/captures\/c-TESTCAPTURE1$/); assertNoPageErrors(page, assert); } finally { await browser.close(); } }); test('the viewport opens on the live edge, not in the past', { timeout: 180000, skip: 'quarantined: chart data and viewport are sampled separately while the live feed advances', }, 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('the quote, current-price marker, and tool order retain their trading context', { timeout: 180000 }, async () => { await withChart(async page => { const state = await page.evaluate(() => ({ quote: document.querySelector('.quote-change').textContent.trim(), sections: [...document.querySelectorAll('aside > .sidebar-section > summary')] .map(node => node.textContent.trim().replace(/\s+/g, ' ')), tools: [...document.querySelectorAll('.tools-section > .tool')] .map(node => node.querySelector('.tool-head').textContent.trim().replace(/\s+/g, ' ')), candleOptions: window.__chart.candles.options(), markerOptions: window.__chart.currentPriceLine?.options(), pulseExists: Boolean(document.querySelector('.current-price-pulse')), configOpen: document.querySelector('.config-section').open, })); assert.match(state.quote, /^[+-]?\d+\.\d{2} \([+-]?\d+\.\d{2}%\)$/); assert.deepEqual(state.sections.slice(0, 3), ['Layers', 'Config', 'Tools']); assert.deepEqual(state.tools, ['Mark', 'Price level', 'Trendline', 'Comment']); assert.equal(state.candleOptions.lastValueVisible, false); assert.equal(state.candleOptions.priceLineVisible, false); assert.equal(state.markerOptions.lineWidth, 1); assert.equal(state.markerOptions.axisLabelVisible, true); assert.equal(state.pulseExists, true); assert.equal(state.configOpen, false); 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); }); });