summaryrefslogtreecommitdiff
path: root/frontend/src/lib/chartResize.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/chartResize.test.ts')
-rw-r--r--frontend/src/lib/chartResize.test.ts131
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();
+ });
+});