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() {
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();
}
});

View File

@@ -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<string, unknown> | undefined = root;

View File

@@ -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;
}

View File

@@ -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<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();
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<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?.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<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 = (() => {
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);