// @ts-nocheck import { describe, expect, it, beforeEach, 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): // // echarts.init previously received an explicit width, and ECharts KEEPS the // explicitly passed size on chart.resize(); a desktop->mobile viewport change // therefore left the rendered SVG at the desktop width, overflowing the page. // The ResizeObserver handler must now re-measure the host and pass the // measured size into chart.resize() so the SVG actually shrinks with the // viewport. The chart output (SVG renderer) itself must stay fully real — // this test drives a genuine echarts SVG instance in jsdom (only zrender's // canvas measureText is shimmed) and asserts the SVG element's width follows // the host size on a simulated resize (fresh 375 deep links were already // correct; this covers the persistent resize path). interface CapturedRO { callback: () => void; el: Element; } let captured: CapturedRO[] = []; function makeResizeObserverCtor() { return class FakeResizeObserver { cb: () => void; el: Element | null = null; constructor(cb: ResizeObserverCallback) { this.cb = cb; captured.push({ callback: () => cb([], this as unknown as ResizeObserver), el: null as never }); } observe(el: Element) { const capture = captured[captured.length - 1]; if (capture) capture.el = el; } unobserve() {} disconnect() {} }; } const baseOption = { grid: { top: 24, left: 60, right: 24, bottom: 54 }, xAxis: { type: 'category', data: ['a', 'b'] }, yAxis: { type: 'value' }, series: [{ name: 'x', type: 'line' as const, data: [1, 2], showSymbol: false }] }; function hostSvgWidth(host: HTMLElement): number | null { const svg = host.querySelector('svg'); if (!svg) return null; const attr = svg.getAttribute('width'); if (attr) return parseFloat(attr); const style = svg.getAttribute('style') ?? ''; const m = /(?:^|;)\s*width:\s*(\d+(?:\.\d+)?)px/.exec(style); return m ? parseFloat(m[1]) : null; } describe('Chart resize follows host width (desktop→mobile overflow root cause)', () => { beforeEach(() => { captured = []; vi.stubGlobal('ResizeObserver', makeResizeObserverCtor()); }); it('init uses measured/fallback size, then resizes the SVG when the host shrinks', async () => { const host = document.createElement('div'); host.style.width = '900px'; document.body.appendChild(host); const originalW = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth'); Object.defineProperty(host, 'clientWidth', { configurable: true, get() { return 900; } }); try { const inst = mount(Chart, { target: host, props: { option: baseOption, height: 340, ariaLabel: '曲线' } }); // let the lazy echarts import mount await new Promise((r) => setTimeout(r, 120)); const ro = captured[captured.length - 1]; expect(ro).toBeTruthy(); expect(ro.el).toBe(host.firstElementChild); // observer watches the bind host const inner = ro.el as HTMLElement; const svg = inner.querySelector('svg'); expect(svg, 'echarts SVG renderer must be real in tests').toBeTruthy(); // initial size: measured/fallback (target has no clientWidth -> 640) expect(parseFloat(svg!.getAttribute('width')!)).toBe(640); // seed the chart at a "desktop" width, then shrink the host (viewport // resize) and fire the observer: svg must follow, not stay sticky const setW = (v: number) => Object.defineProperty(inner, 'clientWidth', { configurable: true, get() { return v; } }); setW(900); ro.callback(); await new Promise((r) => setTimeout(r, 40)); expect(parseFloat(inner.querySelector('svg')!.getAttribute('width')!)).toBe(900); // now the actual overflow scenario: shrink to 375-device width setW(340); ro.callback(); await new Promise((r) => setTimeout(r, 40)); const svgAfter = inner.querySelector('svg')!; const wAfter = svgAfter!.getAttribute('width'); expect(wAfter, 'svg must shrink on resize, not keep desktop width').toBeTruthy(); expect(parseFloat(wAfter!)).toBe(340); unmount(inst); host.remove(); vi.unstubAllGlobals(); } finally { Object.defineProperty(host, 'clientWidth', { configurable: true, get: () => 0 }); } }); it('does not pass undefined sizes to resize when host has no layout (avoid degenerate resize)', async () => { const host = document.createElement('div'); document.body.appendChild(host); const inst = mount(Chart, { target: host, props: { option: baseOption, height: 340, ariaLabel: '曲线' } }); await new Promise((r) => setTimeout(r, 120)); const ro = captured[captured.length - 1]; expect(ro?.el).toBeTruthy(); // host with zero layout: callback must be a no-op (no degenerate 0-width resize) expect(() => ro.callback()).not.toThrow(); unmount(inst); host.remove(); vi.unstubAllGlobals(); }); });