test: clean up code and improve test coverage

This commit is contained in:
Justineo
2025-11-25 13:48:29 +08:00
parent 519e9ca382
commit 2d6b95ff68
7 changed files with 146 additions and 29 deletions

View File

@@ -185,10 +185,6 @@ export default defineComponent({
}); });
function init() { function init() {
if (!root.value) {
return;
}
const instance = (chart.value = initChart( const instance = (chart.value = initChart(
root.value, root.value,
realTheme.value, realTheme.value,
@@ -355,6 +351,7 @@ export default defineComponent({
// transition. // transition.
root.value.__dispose = cleanup; root.value.__dispose = cleanup;
} else { } else {
/* c8 ignore next */
cleanup(); cleanup();
} }
}); });

View File

@@ -109,14 +109,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) {
.forEach((key) => { .forEach((key) => {
const [prefix, ...rest] = key.split("-") as [SlotPrefix, ...string[]]; const [prefix, ...rest] = key.split("-") as [SlotPrefix, ...string[]];
const tail = SLOT_OPTION_PATHS[prefix]; const tail = SLOT_OPTION_PATHS[prefix];
if (!tail) {
return;
}
const path = [...rest, ...tail]; const path = [...rest, ...tail];
if (path.length === 0) {
return;
}
// Traverse to the parent of the leaf, cloning or creating along the way // Traverse to the parent of the leaf, cloning or creating along the way
let cur: Record<string, unknown> | undefined = root; let cur: Record<string, unknown> | undefined = root;

View File

@@ -43,3 +43,8 @@ export function register(): boolean {
registered = true; registered = true;
return registered; return registered;
} }
// Test helper to reset cached registration state.
export function __resetRegisterState(): void {
registered = null;
}

View File

@@ -181,4 +181,47 @@ describe("useAutoresize", () => {
scope.stop(); scope.stop();
}); });
it("skips the initial resize callback when dimensions are unchanged", async () => {
const resize = vi.fn();
const chart = ref<EChartsType | undefined>();
const autoresize = ref<AutoResize | undefined>(true);
const root = ref<HTMLElement | undefined>();
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<EChartsType | undefined>,
autoresize as Ref<AutoResize | undefined>,
root as Ref<HTMLElement | undefined>,
);
});
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;
});
}); });

View File

@@ -413,7 +413,13 @@ describe("ECharts component", () => {
await nextTick(); await nextTick();
chartStub.dispose.mockClear(); chartStub.dispose.mockClear();
exposed.value.root.value = undefined; Object.defineProperty(exposed.value.root, "value", {
configurable: true,
get: () => undefined,
set: () => {
/* ignore */
},
});
screen.unmount(); screen.unmount();
await nextTick(); await nextTick();
@@ -842,4 +848,34 @@ describe("ECharts component", () => {
warnSpy.mockRestore(); warnSpy.mockRestore();
}); });
it("ignores falsy listeners during event binding", async () => {
const option = ref({});
const exposed = shallowRef<any>();
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<any>(null);
const exposed = shallowRef<any>();
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();
});
}); });

View File

@@ -43,6 +43,20 @@ describe("smart-update", () => {
expect(summary?.idsSorted).toEqual(["1", "2"]); expect(summary?.idsSorted).toEqual(["1", "2"]);
expect(summary?.noIdCount).toBe(3); 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", () => { describe("planUpdate", () => {
@@ -364,6 +378,38 @@ describe("smart-update", () => {
expect(result.plan.notMerge).toBe(false); expect(result.plan.notMerge).toBe(false);
expect(result.option.series).not.toEqual(base.series); 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<string, unknown>).phantom = undefined;
const result = planUpdate(prev, base);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toBeUndefined();
});
}); });
}); });
}); });

View File

@@ -6,15 +6,11 @@ declare global {
} }
} }
type LoadOptions = { suffix?: string }; const loadModule = async () => {
const mod = await import("../src/wc");
const loadModule = (() => { mod.__resetRegisterState();
let counter = 0; return mod;
return async (mode: "stub" | "native", options?: LoadOptions) => { };
const suffix = options?.suffix ?? `${mode}-${++counter}`;
return import(/* @vite-ignore */ `../src/wc?${suffix}`);
};
})();
describe("register", () => { describe("register", () => {
describe("with stubbed customElements", () => { describe("with stubbed customElements", () => {
@@ -58,7 +54,7 @@ describe("register", () => {
undefined as unknown as CustomElementRegistry, undefined as unknown as CustomElementRegistry,
); );
const { register } = await loadModule("stub"); const { register } = await loadModule();
expect(register()).toBe(false); expect(register()).toBe(false);
expect(register()).toBe(false); expect(register()).toBe(false);
@@ -71,7 +67,7 @@ describe("register", () => {
define() {}, define() {},
} as unknown as CustomElementRegistry); } as unknown as CustomElementRegistry);
const { register } = await loadModule("stub", { suffix: "no-get" }); const { register } = await loadModule();
expect(register()).toBe(false); expect(register()).toBe(false);
expect(register()).toBe(false); expect(register()).toBe(false);
}); });
@@ -79,7 +75,7 @@ describe("register", () => {
it("registers the custom element once", async () => { it("registers the custom element once", async () => {
const defineSpy = vi.spyOn(registry, "define"); const defineSpy = vi.spyOn(registry, "define");
const { register, TAG_NAME } = await loadModule("stub"); const { register, TAG_NAME } = await loadModule();
expect(register()).toBe(true); expect(register()).toBe(true);
expect(defineSpy).toHaveBeenCalledTimes(1); expect(defineSpy).toHaveBeenCalledTimes(1);
@@ -95,17 +91,17 @@ describe("register", () => {
throw new Error("boom"); 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(register()).toBe(false)
expect(defineSpy).toHaveBeenCalledTimes(1); expect(defineSpy).toHaveBeenCalledTimes(1);
expect(registry.get(TAG_NAME)).toBeUndefined(); expect(registry.get(TAG_NAME)).toBeUndefined();
}); });
it("skips redefinition when element already registered", async () => { it("skips redefinition when element already registered", async () => {
const existing = class extends HTMLElement {}; const existing = class extends HTMLElement {};
const { register, TAG_NAME } = await loadModule("stub"); const { register, TAG_NAME } = await loadModule();
registry.define(TAG_NAME, existing); registry.define(TAG_NAME, existing);
const defineSpy = vi.spyOn(registry, "define"); const defineSpy = vi.spyOn(registry, "define");
@@ -116,7 +112,7 @@ describe("register", () => {
}); });
it("exposes a constructor with disconnect hook", async () => { 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); expect(register()).toBe(true);
@@ -130,6 +126,7 @@ describe("register", () => {
let original: CustomElementConstructor | undefined; let original: CustomElementConstructor | undefined;
beforeEach(() => { beforeEach(() => {
vi.resetModules();
vi.restoreAllMocks(); vi.restoreAllMocks();
vi.unstubAllGlobals(); vi.unstubAllGlobals();
original = customElements.get("x-vue-echarts"); original = customElements.get("x-vue-echarts");
@@ -144,7 +141,7 @@ describe("register", () => {
}); });
it("disposes chart when element is removed from DOM", async () => { 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); expect(register()).toBe(true);