diff options
Diffstat (limited to 'frontend/src/lib/chartResize.test.ts')
| -rw-r--r-- | frontend/src/lib/chartResize.test.ts | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/frontend/src/lib/chartResize.test.ts b/frontend/src/lib/chartResize.test.ts new file mode 100644 index 0000000..95beaad --- /dev/null +++ b/frontend/src/lib/chartResize.test.ts @@ -0,0 +1,131 @@ +// @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(); + }); +}); |
