test: increase coverage and add codecov integration

This commit is contained in:
Justineo
2025-09-20 22:47:33 +08:00
committed by GU Yiling
parent e0cf2a4e7e
commit 373fe19d59
31 changed files with 2813 additions and 169 deletions

View File

@ -103,31 +103,29 @@ export default defineComponent({
let lastSignature: Signature | undefined;
function resolveUpdateOptions(
plan?: UpdatePlan,
override?: UpdateOptions,
): UpdateOptions {
const base = realUpdateOptions.value;
const result: UpdateOptions = { ...override };
// Note: This resolver is only used in the default "smart-update" path when
// no `updateOptions` are provided via props/injection (i.e., `realUpdateOptions`
// is falsy) and manual mode is off. Historically it attempted to merge
// caller overrides and base options, but those code paths are not reachable
// given current call sites (we never pass an override in non-manual paths,
// and when base options exist we short-circuit before calling this).
// To avoid dead branches and keep behavior clear, we only materialize flags
// derived from the computed update plan.
function resolveUpdateOptions(plan?: UpdatePlan): UpdateOptions {
const result: UpdateOptions = {};
const replacements = [
...(plan?.replaceMerge ?? []),
...(override?.replaceMerge ?? []),
].filter((key): key is string => key != null);
const replacements = (plan?.replaceMerge ?? []).filter(
(key): key is string => key != null,
);
if (replacements.length > 0) {
result.replaceMerge = [...new Set(replacements)];
} else {
delete result.replaceMerge;
}
const notMerge = override?.notMerge ?? plan?.notMerge;
if (notMerge !== undefined) {
result.notMerge = notMerge;
} else {
delete result.notMerge;
if (plan?.notMerge !== undefined) {
result.notMerge = plan.notMerge;
}
return base ? { ...base, ...result } : result;
return result;
}
function applyOption(
@ -156,7 +154,7 @@ export default defineComponent({
patched as unknown as EChartsOption,
);
const updateOptions = resolveUpdateOptions(planned.plan, override);
const updateOptions = resolveUpdateOptions(planned.plan);
instance.setOption(planned.option, updateOptions);
lastSignature = planned.signature;
}
@ -220,8 +218,13 @@ export default defineComponent({
if (once) {
const raw = handler;
let called = false;
handler = (...args: any[]) => {
if (called) {
return;
}
called = true;
raw(...args);
target.off(event, handler);
};
@ -274,10 +277,10 @@ export default defineComponent({
typeof notMerge === "boolean" ? { notMerge, lazyUpdate } : notMerge;
if (!chart.value) {
init(option, true, updateOptions ?? undefined);
} else {
applyOption(chart.value, option, updateOptions ?? undefined, true);
return;
}
applyOption(chart.value, option, updateOptions ?? undefined, true);
};
function cleanup() {
@ -306,10 +309,10 @@ export default defineComponent({
return;
}
if (!chart.value) {
init();
} else {
applyOption(chart.value, option);
return;
}
applyOption(chart.value, option);
},
{ deep: true },
);

View File

@ -10,7 +10,7 @@ import {
} from "vue";
import type { Slots, SlotsType } from "vue";
import type { Option } from "../types";
import { isValidArrayIndex, isSameSet } from "../utils";
import { isBrowser, isValidArrayIndex, isSameSet } from "../utils";
import type { TooltipComponentFormatterCallbackParams } from "echarts";
const SLOT_OPTION_PATHS = {
@ -29,8 +29,7 @@ function isValidSlotName(key: string): key is SlotName {
}
export function useSlotOption(slots: Slots, onSlotsChange: () => void) {
const detachedRoot =
typeof window !== "undefined" ? document.createElement("div") : undefined;
const detachedRoot = isBrowser() ? document.createElement("div") : undefined;
const containers = shallowReactive<SlotRecord<HTMLElement>>({});
const initialized = shallowReactive<SlotRecord<boolean>>({});
const params = shallowReactive<SlotRecord<unknown>>({});
@ -39,7 +38,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) {
// Teleport the slots to a detached root
const teleportedSlots = () => {
// Make slots client-side only to avoid SSR hydration mismatch
return isMounted.value
return isMounted.value && detachedRoot
? h(
Teleport,
{ to: detachedRoot },

View File

@ -1,4 +1,4 @@
import type { EChartsOption } from "echarts";
import type { Option } from "./types";
import { isPlainObject } from "./utils";
export interface UpdatePlan {
@ -49,7 +49,7 @@ function readId(item: unknown): string | undefined {
* Build a minimal signature from a full ECharts option.
* Only top-level keys are inspected.
*/
export function buildSignature(option: EChartsOption): Signature {
export function buildSignature(option: Option): Signature {
const opt = option as Record<string, unknown>;
const optionsLength = Array.isArray(opt.options)
@ -152,7 +152,7 @@ function hasMissingIds(
}
export interface PlannedUpdate {
option: EChartsOption;
option: Option;
signature: Signature;
plan: UpdatePlan;
}
@ -163,7 +163,7 @@ export interface PlannedUpdate {
*/
export function planUpdate(
prev: Signature | undefined,
option: EChartsOption,
option: Option,
): PlannedUpdate {
const next = buildSignature(option);
@ -224,7 +224,7 @@ export function planUpdate(
overrides.forEach((value, key) => {
clone[key] = value;
});
normalizedOption = clone as EChartsOption;
normalizedOption = clone as Option;
signature = buildSignature(normalizedOption);
}

View File

@ -1,5 +1,9 @@
type Attrs = Record<string, any>;
export function isBrowser(): boolean {
return typeof window !== "undefined" && typeof document !== "undefined";
}
// Copied from
// https://github.com/vuejs/vue-next/blob/5a7a1b8293822219283d6e267496bec02234b0bc/packages/shared/src/index.ts#L40-L41
const onRE = /^on[^a-z]/;

View File

@ -1,3 +1,5 @@
import { isBrowser } from "./utils";
let registered: boolean | null = null;
export const TAG_NAME = "x-vue-echarts";
@ -11,30 +13,33 @@ export function register(): boolean {
return registered;
}
if (
typeof HTMLElement === "undefined" ||
typeof customElements === "undefined"
) {
return (registered = false);
const registry = globalThis.customElements;
if (!isBrowser() || !registry?.get) {
registered = false;
return registered;
}
try {
class ECElement extends HTMLElement implements EChartsElement {
__dispose: (() => void) | null = null;
if (!registry.get(TAG_NAME)) {
try {
class ECElement extends HTMLElement implements EChartsElement {
__dispose: (() => void) | null = null;
disconnectedCallback() {
if (this.__dispose) {
this.__dispose();
this.__dispose = null;
disconnectedCallback(): void {
if (this.__dispose) {
this.__dispose();
this.__dispose = null;
}
}
}
registry.define(TAG_NAME, ECElement);
} catch {
registered = false;
return registered;
}
if (customElements.get(TAG_NAME) == null) {
customElements.define(TAG_NAME, ECElement);
}
} catch {
return (registered = false);
}
return (registered = true);
registered = true;
return registered;
}