// @ts-nocheck import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; import { mount, unmount } from 'svelte'; import Chart from '../components/Chart.svelte'; // Bounded chart-overflow regression (parent live QA: viewport 375, // document.scrollWidth 1133 after desktop->mobile resize), reimplemented for // the layerchart/shadcn migration: // // The old echarts wrapper kept an explicitly-passed width across resize() // calls, leaving the SVG stuck at desktop width after a viewport shrink. // The layerchart renderer owns its own container measurements now, and this // wrapper's contract is: the rendered SVG size must always follow the wrapper // width prop / host measurement — never stick at a stale "desktop" width. // // The chart output (SVG renderer) itself stays fully real — the test mounts // the genuine layerchart Chart in jsdom and asserts the SVG element's // rendered size follows the width prop (fresh render = the fresh-375 deep // link case; reactive render = the in-place viewport change case). const data = [ { i: 0, date: '2026-01-01', equity: 1.0 }, { i: 1, date: '2026-01-02', equity: 1.05 }, { i: 2, date: '2026-01-03', equity: 0.98 } ]; const series = [{ key: 'equity', label: '策略权益', color: '#0f766e', value: 'equity' }]; function svgSize(host: HTMLElement): number | null { const svg = host.querySelector('svg'); if (!svg) return null; const attrW = svg.getAttribute('width'); if (attrW) return parseFloat(attrW); const style = svg.getAttribute('style') ?? ''; const m = /(?:^|;)\s*width:\s*(\d+(?:\.\d+)?)px/.exec(style); if (m) return parseFloat(m[1]); const vb = (svg.getAttribute('viewBox') || '').split(/[\s,]+/); return vb.length >= 4 ? parseFloat(vb[2]) : null; } let captured = []; function makeResizeObserverCtor() { return class FakeResizeObserver { cb; constructor(cb) { this.cb = cb; captured.push(() => cb([], this)); } observe(el) { if (captured.length > 0) captured[captured.length - 1].el = el; } unobserve() {} disconnect() {} }; } async function mountChart(props) { const host = document.createElement('div'); host.style.width = '900px'; document.body.appendChild(host); const inst = mount(Chart, { target: host, props }); await new Promise((r) => setTimeout(r, 150)); return { host, inst }; } describe('Chart (layerchart) render + resize contract', () => { beforeEach(() => { captured = []; vi.stubGlobal('ResizeObserver', makeResizeObserverCtor()); }); afterEach(() => vi.unstubAllGlobals()); it('layerchart SVG must be real in jsdom (fallback width 640 when host has no layout)', async () => { const host = document.createElement('div'); document.body.appendChild(host); const inst = mount(Chart, { target: host, props: { data, series, height: 340, ariaLabel: '曲线' } }); await new Promise((r) => setTimeout(r, 150)); const svg = host.querySelector('svg'); expect(svg, 'layerchart SVG renderer must be real in tests').toBeTruthy(); expect(svgSize(host), 'fallback width must be applied, not 0/degenerate').toBe(640); unmount(inst); host.remove(); }); it('svg width follows the wrapper width prop across a viewport change (900 → 340)', async () => { const first = await mountChart({ data, series, height: 340, width: 900, ariaLabel: '曲线' }); expect(svgSize(first.host), 'desktop width must be rendered').toBe(900); unmount(first.inst); first.host.remove(); // the desktop->mobile in-place resize case: renderer must not stick to the // previously rendered width const second = await mountChart({ data, series, height: 340, width: 340, ariaLabel: '曲线' }); expect(svgSize(second.host), 'mobile width must be rendered, not the desktop width').toBe(340); unmount(second.inst); second.host.remove(); }); // In-place viewport change without remount: resize the host and fire the // ResizeObserver callback. The wrapper must re-measure and push the new // width down (the width prop is only the initial size — the live // measurement always wins, matching the old echarts contract). it('in-place host resize via ResizeObserver updates the svg without a remount', async () => { const target = document.createElement('div'); target.style.width = '900px'; document.body.appendChild(target); const inst = mount(Chart, { target, props: { data, series, height: 340, ariaLabel: '曲线' } }); await new Promise((r) => setTimeout(r, 150)); // emulate a measurable wrapper host (jsdom has no layout engine): // the wrapper's internal role="img" div is the observed element const inner = target.querySelector('div[role="img"]'); expect(inner).toBeTruthy(); let hostWidth = 900; Object.defineProperty(inner, 'clientWidth', { get: () => hostWidth, configurable: true }); for (const fire of captured) fire(); await new Promise((r) => setTimeout(r, 120)); expect(svgSize(target), 'desktop width must be rendered first').toBe(900); // viewport shrink in place: narrowed host + RO callback (no remount) hostWidth = 280; for (const fire of captured) fire(); await new Promise((r) => setTimeout(r, 120)); expect(svgSize(target), 'mobile width must be rendered, not the desktop width').toBe(280); unmount(inst); target.remove(); }); it('zero-layout host: fake resize callbacks must not throw and svg stays alive', async () => { const host = document.createElement('div'); document.body.appendChild(host); const inst = mount(Chart, { target: host, props: { data, series, height: 340, ariaLabel: '曲线' } }); await new Promise((r) => setTimeout(r, 150)); expect(captured.length).toBeGreaterThan(0); expect(() => { for (const fire of captured) fire(); }).not.toThrow(); expect(host.querySelector('svg')).toBeTruthy(); unmount(inst); host.remove(); }); });