From 2d6b95ff6879cab799c3fe4b8e510a1c934002c0 Mon Sep 17 00:00:00 2001 From: Justineo Date: Tue, 25 Nov 2025 13:48:29 +0800 Subject: [PATCH] test: clean up code and improve test coverage --- src/ECharts.ts | 5 +---- src/composables/slot.ts | 7 ------ src/wc.ts | 5 +++++ tests/autoresize.test.ts | 43 +++++++++++++++++++++++++++++++++++++ tests/echarts.test.ts | 38 ++++++++++++++++++++++++++++++++- tests/update.test.ts | 46 ++++++++++++++++++++++++++++++++++++++++ tests/wc.test.ts | 31 ++++++++++++--------------- 7 files changed, 146 insertions(+), 29 deletions(-) diff --git a/src/ECharts.ts b/src/ECharts.ts index 31c6490..f83b5ca 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -185,10 +185,6 @@ export default defineComponent({ }); function init() { - if (!root.value) { - return; - } - const instance = (chart.value = initChart( root.value, realTheme.value, @@ -355,6 +351,7 @@ export default defineComponent({ // transition. root.value.__dispose = cleanup; } else { + /* c8 ignore next */ cleanup(); } }); diff --git a/src/composables/slot.ts b/src/composables/slot.ts index e8a169d..cc29599 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -109,14 +109,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { .forEach((key) => { const [prefix, ...rest] = key.split("-") as [SlotPrefix, ...string[]]; const tail = SLOT_OPTION_PATHS[prefix]; - if (!tail) { - return; - } - const path = [...rest, ...tail]; - if (path.length === 0) { - return; - } // Traverse to the parent of the leaf, cloning or creating along the way let cur: Record | undefined = root; diff --git a/src/wc.ts b/src/wc.ts index 738326a..1b673ea 100644 --- a/src/wc.ts +++ b/src/wc.ts @@ -43,3 +43,8 @@ export function register(): boolean { registered = true; return registered; } + +// Test helper to reset cached registration state. +export function __resetRegisterState(): void { + registered = null; +} diff --git a/tests/autoresize.test.ts b/tests/autoresize.test.ts index 05be095..bec516a 100644 --- a/tests/autoresize.test.ts +++ b/tests/autoresize.test.ts @@ -181,4 +181,47 @@ describe("useAutoresize", () => { scope.stop(); }); + + it("skips the initial resize callback when dimensions are unchanged", async () => { + const resize = vi.fn(); + const chart = ref(); + const autoresize = ref(true); + const root = ref(); + const container = createSizedContainer(160, 90); + + const originalRO = globalThis.ResizeObserver; + const callbacks: Array<() => void> = []; + + class StubResizeObserver { + callback: ResizeObserverCallback; + observe = vi.fn(); + disconnect = vi.fn(); + + constructor(cb: ResizeObserverCallback) { + this.callback = cb; + callbacks.push(() => cb([], this as unknown as ResizeObserver)); + } + } + + (globalThis as any).ResizeObserver = StubResizeObserver as any; + + const scope = effectScope(); + scope.run(() => { + useAutoresize( + chart as Ref, + autoresize as Ref, + root as Ref, + ); + }); + + chart.value = { resize } as unknown as EChartsType; + root.value = container; + await nextTick(); + + callbacks[0]?.(); + expect(resize).not.toHaveBeenCalled(); + + scope.stop(); + (globalThis as any).ResizeObserver = originalRO; + }); }); diff --git a/tests/echarts.test.ts b/tests/echarts.test.ts index 32a645a..adb3578 100644 --- a/tests/echarts.test.ts +++ b/tests/echarts.test.ts @@ -413,7 +413,13 @@ describe("ECharts component", () => { await nextTick(); chartStub.dispose.mockClear(); - exposed.value.root.value = undefined; + Object.defineProperty(exposed.value.root, "value", { + configurable: true, + get: () => undefined, + set: () => { + /* ignore */ + }, + }); screen.unmount(); await nextTick(); @@ -842,4 +848,34 @@ describe("ECharts component", () => { warnSpy.mockRestore(); }); + + it("ignores falsy listeners during event binding", async () => { + const option = ref({}); + const exposed = shallowRef(); + + renderChart( + () => ({ option: option.value, onClick: undefined as unknown as () => {} }), + exposed, + ); + await nextTick(); + + expect(chartStub.on).not.toHaveBeenCalled(); + }); + + it("skips option watcher when chart instance is missing", async () => { + const option = ref(null); + const exposed = shallowRef(); + + init.mockImplementation(() => undefined as any); + + renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + + chartStub.setOption.mockClear(); + + option.value = { title: { text: "later" } }; + await nextTick(); + + expect(chartStub.setOption).not.toHaveBeenCalled(); + }); }); diff --git a/tests/update.test.ts b/tests/update.test.ts index a30f911..95e53cc 100644 --- a/tests/update.test.ts +++ b/tests/update.test.ts @@ -43,6 +43,20 @@ describe("smart-update", () => { expect(summary?.idsSorted).toEqual(["1", "2"]); expect(summary?.noIdCount).toBe(3); }); + + it("counts primitive array items and sorts scalar keys", () => { + const option: EChartsOption = { + dataset: ["raw", { id: "has-id" }], + backgroundColor: "#000", + color: "#fff", + } as unknown as EChartsOption; + + const signature = buildSignature(option); + + expect(signature.arrays.dataset?.noIdCount).toBe(1); + expect(signature.arrays.dataset?.idsSorted).toEqual(["has-id"]); + expect(signature.scalars).toEqual(["backgroundColor", "color"]); + }); }); describe("planUpdate", () => { @@ -364,6 +378,38 @@ describe("smart-update", () => { expect(result.plan.notMerge).toBe(false); expect(result.option.series).not.toEqual(base.series); }); + + it("adds replaceMerge when ids disappear entirely", () => { + const base: EChartsOption = { + series: [ + { id: "espresso", type: "bar" }, + { id: "mocha", type: "line" }, + ], + }; + + const update: EChartsOption = { + series: [{ type: "bar" }, { type: "line" }], + }; + + const result = planUpdate(buildSignature(base), update); + + expect(result.plan.replaceMerge).toEqual(["series"]); + expect(result.option.series).toEqual(update.series); + }); + + it("ignores undefined array summaries carried over in previous signatures", () => { + const base: EChartsOption = { + series: [{ id: "flat-white", type: "bar" }], + }; + + const prev = buildSignature(base); + (prev.arrays as Record).phantom = undefined; + + const result = planUpdate(prev, base); + + expect(result.plan.notMerge).toBe(false); + expect(result.plan.replaceMerge).toBeUndefined(); + }); }); }); }); diff --git a/tests/wc.test.ts b/tests/wc.test.ts index 6a9ad66..ae52227 100644 --- a/tests/wc.test.ts +++ b/tests/wc.test.ts @@ -6,15 +6,11 @@ declare global { } } -type LoadOptions = { suffix?: string }; - -const loadModule = (() => { - let counter = 0; - return async (mode: "stub" | "native", options?: LoadOptions) => { - const suffix = options?.suffix ?? `${mode}-${++counter}`; - return import(/* @vite-ignore */ `../src/wc?${suffix}`); - }; -})(); +const loadModule = async () => { + const mod = await import("../src/wc"); + mod.__resetRegisterState(); + return mod; +}; describe("register", () => { describe("with stubbed customElements", () => { @@ -58,7 +54,7 @@ describe("register", () => { undefined as unknown as CustomElementRegistry, ); - const { register } = await loadModule("stub"); + const { register } = await loadModule(); expect(register()).toBe(false); expect(register()).toBe(false); @@ -71,7 +67,7 @@ describe("register", () => { define() {}, } as unknown as CustomElementRegistry); - const { register } = await loadModule("stub", { suffix: "no-get" }); + const { register } = await loadModule(); expect(register()).toBe(false); expect(register()).toBe(false); }); @@ -79,7 +75,7 @@ describe("register", () => { it("registers the custom element once", async () => { const defineSpy = vi.spyOn(registry, "define"); - const { register, TAG_NAME } = await loadModule("stub"); + const { register, TAG_NAME } = await loadModule(); expect(register()).toBe(true); expect(defineSpy).toHaveBeenCalledTimes(1); @@ -95,17 +91,17 @@ describe("register", () => { throw new Error("boom"); }); - const { register, TAG_NAME } = await loadModule("stub"); + const { register, TAG_NAME } = await loadModule(); expect(register()).toBe(false); - expect(register()).toBe(false); + expect(register()).toBe(false) expect(defineSpy).toHaveBeenCalledTimes(1); expect(registry.get(TAG_NAME)).toBeUndefined(); }); it("skips redefinition when element already registered", async () => { const existing = class extends HTMLElement {}; - const { register, TAG_NAME } = await loadModule("stub"); + const { register, TAG_NAME } = await loadModule(); registry.define(TAG_NAME, existing); const defineSpy = vi.spyOn(registry, "define"); @@ -116,7 +112,7 @@ describe("register", () => { }); it("exposes a constructor with disconnect hook", async () => { - const { register, TAG_NAME } = await loadModule("stub"); + const { register, TAG_NAME } = await loadModule(); expect(register()).toBe(true); @@ -130,6 +126,7 @@ describe("register", () => { let original: CustomElementConstructor | undefined; beforeEach(() => { + vi.resetModules(); vi.restoreAllMocks(); vi.unstubAllGlobals(); original = customElements.get("x-vue-echarts"); @@ -144,7 +141,7 @@ describe("register", () => { }); it("disposes chart when element is removed from DOM", async () => { - const { register, TAG_NAME } = await loadModule("native"); + const { register, TAG_NAME } = await loadModule(); expect(register()).toBe(true);