mirror of
https://github.com/ecomfe/vue-echarts.git
synced 2025-10-27 10:55:07 +08:00
test: setup vitest, add unit tests for smart-update
This commit is contained in:
@ -1,16 +1,21 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
Core source lives in `src/`, implemented in TypeScript with the Vue 3 Composition API. Key entry modules include `src/index.ts` for exports, `src/ECharts.ts` for the main component, and utilities under `src/composables/` and `src/utils.ts`. Demo site assets sit in `demo/` (Vite-powered) and should be kept in sync with new features. Bundled artifacts in `dist/` are generated by `pnpm build`; avoid editing them manually. Shared build helpers reside in `scripts/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Use `pnpm install` to set up dependencies. `pnpm dev` serves the demo playground at `http://localhost:5173` for interactive testing. Run `pnpm build` (tsdown) to produce distributable output under `dist/`, and `pnpm dev:build` or `pnpm dev:preview` when you need the Vite bundle for the demo. `pnpm typecheck` validates the library’s TypeScript contracts, `pnpm lint` applies ESLint with autofix, and `pnpm format` runs Prettier. Use `pnpm publint` before releases to confirm the published surface.
|
||||
|
||||
Use `pnpm install` to set up dependencies. `pnpm dev` serves the demo playground at `http://localhost:5173` for interactive testing. Run `pnpm build` (tsdown) to produce distributable output under `dist/`, and `pnpm dev:build` or `pnpm dev:preview` when you need the Vite bundle for the demo. `pnpm typecheck` validates the library’s TypeScript contracts, `pnpm lint` applies ESLint with autofix, `pnpm format` runs Prettier, and `pnpm test` executes the Vitest suite. Use `pnpm publint` before releases to confirm the published surface.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
This project targets Vue 3 + TypeScript with ECMAScript modules. Follow the existing 2-space indentation, trailing commas where valid, and single quotes for strings unless interpolation is required. Components and exported composables use PascalCase (e.g., `VChart`), while local helpers remain camelCase. Honor `eslint.config.ts` and Prettier defaults; always run `pnpm lint && pnpm format` before sending patches. Keep public exports centralized in `src/index.ts` and add accompanying CSS changes to `src/style.css`.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
There is no standalone unit-test runner yet; rely on TypeScript, linting, and manual QA in the demo. Before opening a PR, run `pnpm lint`, `pnpm typecheck`, and `pnpm build`. Exercise relevant demos in `demo/src/` and add or update examples that showcase new behaviors. For major fixes, include reproduction and verification steps in the PR description so reviewers can follow along.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Commit history follows Conventional Commits (`type(scope): summary`), e.g., `feat(runtime): add renderer option` or `chore(deps): update vue`. Use concise, imperative summaries and group related changes together. PRs should describe user-facing effects, list verification commands, and link issues with `Fixes #123` when applicable. Include screenshots or GIFs for visual updates to the demo, and note any doc changes (`README.md`, `demo/`) in the description. Ensure CI checks mirror local commands: `pnpm lint`, `pnpm typecheck`, and `pnpm build`.
|
||||
|
||||
@ -19,7 +19,9 @@
|
||||
"dev:preview": "vite preview",
|
||||
"dev:typecheck": "vue-tsc -p ./demo",
|
||||
"docs": "jiti ./scripts/docs.ts",
|
||||
"release": "bumpp --execute \"pnpm run docs\" --all"
|
||||
"release": "bumpp --execute \"pnpm run docs\" --all",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.1",
|
||||
"type": "module",
|
||||
@ -67,6 +69,7 @@
|
||||
"typescript": "^5.9.2",
|
||||
"unplugin-raw": "^0.6.0",
|
||||
"vite": "npm:rolldown-vite@^7.1.2",
|
||||
"vitest": "^3.2.4",
|
||||
"vue": "^3.5.18",
|
||||
"vue-tsc": "^3.0.5"
|
||||
},
|
||||
|
||||
1463
pnpm-lock.yaml
generated
1463
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -27,8 +27,8 @@ import {
|
||||
import type { PublicMethods, SlotsTypes } from "./composables";
|
||||
import { isOn, omitOn } from "./utils";
|
||||
import { register, TAG_NAME } from "./wc";
|
||||
import { planUpdate } from "./merge";
|
||||
import type { Signature, UpdatePlan } from "./merge";
|
||||
import { planUpdate } from "./smart-update";
|
||||
import type { Signature, UpdatePlan } from "./smart-update";
|
||||
|
||||
import type { PropType, InjectionKey } from "vue";
|
||||
import type {
|
||||
@ -81,15 +81,13 @@ export default defineComponent({
|
||||
|
||||
const { autoresize, manualUpdate, loading, loadingOptions } = toRefs(props);
|
||||
|
||||
const realOption = computed(() => props.option || undefined);
|
||||
const realTheme = computed(
|
||||
() => props.theme || toValue(defaultTheme) || undefined,
|
||||
);
|
||||
const realOption = computed(() => props.option || {});
|
||||
const realTheme = computed(() => props.theme || toValue(defaultTheme));
|
||||
const realInitOptions = computed(
|
||||
() => props.initOptions || toValue(defaultInitOptions) || undefined,
|
||||
);
|
||||
const realUpdateOptions = computed(
|
||||
() => props.updateOptions || toValue(defaultUpdateOptions) || undefined,
|
||||
() => props.updateOptions || toValue(defaultUpdateOptions),
|
||||
);
|
||||
const nonEventAttrs = computed(() => omitOn(attrs));
|
||||
const nativeListeners: Record<string, unknown> = {};
|
||||
|
||||
298
tests/smart-update.test.ts
Normal file
298
tests/smart-update.test.ts
Normal file
@ -0,0 +1,298 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildSignature, planUpdate } from "../src/smart-update";
|
||||
import type { EChartsOption } from "echarts";
|
||||
|
||||
describe("smart-update", () => {
|
||||
describe("buildSignature", () => {
|
||||
it("collects scalars, objects, and array summaries", () => {
|
||||
const option: EChartsOption = {
|
||||
title: { text: "foo" },
|
||||
tooltip: { show: true },
|
||||
color: "#000",
|
||||
dataset: [{ id: "ds1", source: [] }, { source: [] }],
|
||||
series: [
|
||||
{ id: "a", type: "bar" },
|
||||
{ type: "line" },
|
||||
],
|
||||
};
|
||||
|
||||
const signature = buildSignature(option);
|
||||
|
||||
expect(signature.objects).toEqual(["title", "tooltip"]);
|
||||
expect(signature.scalars).toEqual(["color"]);
|
||||
expect(signature.arrays.dataset?.idsSorted).toEqual(["ds1"]);
|
||||
expect(signature.arrays.dataset?.noIdCount).toBe(1);
|
||||
expect(signature.arrays.series?.idsSorted).toEqual(["a"]);
|
||||
expect(signature.arrays.series?.noIdCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("planUpdate", () => {
|
||||
describe("bootstrap & neutral cases", () => {
|
||||
it("returns neutral plan when previous signature missing", () => {
|
||||
const option: EChartsOption = {
|
||||
legend: { show: true },
|
||||
series: [{ type: "bar", data: [1, 2, 3] }],
|
||||
};
|
||||
|
||||
const result = planUpdate(undefined, option);
|
||||
|
||||
expect(result.plan.notMerge).toBe(false);
|
||||
expect(result.plan.replaceMerge).toBeUndefined();
|
||||
expect(result.option).toEqual(option);
|
||||
});
|
||||
|
||||
it("returns neutral plan when signatures match", () => {
|
||||
const option: EChartsOption = {
|
||||
title: { text: "foo" },
|
||||
series: [{ id: "a" }],
|
||||
};
|
||||
|
||||
const prev = buildSignature(option);
|
||||
const next = planUpdate(prev, option);
|
||||
|
||||
expect(next.plan.notMerge).toBe(false);
|
||||
expect(next.plan.replaceMerge).toBeUndefined();
|
||||
expect(next.option).toEqual(option);
|
||||
});
|
||||
|
||||
it("keeps merge when scalar value changes", () => {
|
||||
const prev = buildSignature({ color: "red" });
|
||||
const next = planUpdate(prev, { color: "blue" });
|
||||
|
||||
expect(next.plan.notMerge).toBe(false);
|
||||
expect(next.plan.replaceMerge).toBeUndefined();
|
||||
expect(next.option.color).toBe("blue");
|
||||
});
|
||||
|
||||
it("keeps merge when new series IDs are added", () => {
|
||||
const base: EChartsOption = {
|
||||
series: [{ id: "latte", type: "bar", data: [10, 20] }],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
series: [
|
||||
{ id: "latte", type: "bar", data: [12, 24] },
|
||||
{ id: "mocha", type: "bar", data: [14, 28] },
|
||||
],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.plan.notMerge).toBe(false);
|
||||
expect(result.plan.replaceMerge).toBeUndefined();
|
||||
expect(result.option.series).toEqual(update.series);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shrink detection", () => {
|
||||
it("forces rebuild when options shrink", () => {
|
||||
const prev = buildSignature({ options: [{}, {}] });
|
||||
const { plan } = planUpdate(prev, { options: [{}] });
|
||||
expect(plan.notMerge).toBe(true);
|
||||
});
|
||||
|
||||
it("forces rebuild when scalars disappear", () => {
|
||||
const prev = buildSignature({ color: "red", title: { text: "foo" } });
|
||||
const { plan } = planUpdate(prev, { title: { text: "foo" } });
|
||||
expect(plan.notMerge).toBe(true);
|
||||
});
|
||||
|
||||
it("injects null for removed objects", () => {
|
||||
const prev = buildSignature({ legend: { show: true } });
|
||||
const next = planUpdate(prev, {});
|
||||
|
||||
expect(next.option.legend).toBeNull();
|
||||
expect(next.plan.notMerge).toBe(false);
|
||||
});
|
||||
|
||||
it("injects empty array and replaceMerge when array removed", () => {
|
||||
const prev = buildSignature({ series: [{ id: "a" }, {}] });
|
||||
const next = planUpdate(prev, {});
|
||||
|
||||
expect(next.option.series).toEqual([]);
|
||||
expect(next.plan.replaceMerge).toEqual(["series"]);
|
||||
});
|
||||
|
||||
it("adds replaceMerge when ids shrink", () => {
|
||||
const prev = buildSignature({ series: [{ id: "a" }, { id: "b" }] });
|
||||
const next = planUpdate(prev, { series: [{ id: "a" }] });
|
||||
|
||||
expect(next.plan.replaceMerge).toEqual(["series"]);
|
||||
});
|
||||
|
||||
it("adds replaceMerge when anonymous count shrinks", () => {
|
||||
const prev = buildSignature({ series: [{}, {}] });
|
||||
const next = planUpdate(prev, { series: [{}] });
|
||||
|
||||
expect(next.plan.replaceMerge).toEqual(["series"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("real data scenarios", () => {
|
||||
it("handles legend removal and series shrink", () => {
|
||||
const base: EChartsOption = {
|
||||
legend: { show: true },
|
||||
dataset: [
|
||||
{
|
||||
id: "sales",
|
||||
source: [
|
||||
["product", "2015", "2016"],
|
||||
["Matcha Latte", 43.3, 85.8],
|
||||
],
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{ id: "2015", type: "bar", datasetId: "sales" },
|
||||
{ id: "2016", type: "bar", datasetId: "sales" },
|
||||
],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
dataset: [
|
||||
{
|
||||
id: "sales",
|
||||
source: [
|
||||
["product", "2015"],
|
||||
["Matcha Latte", 55.1],
|
||||
],
|
||||
},
|
||||
],
|
||||
series: [{ id: "2015", type: "bar", datasetId: "sales" }],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.option.legend).toBeNull();
|
||||
expect(result.option.series).toEqual(update.series);
|
||||
expect(result.plan.notMerge).toBe(false);
|
||||
expect(result.plan.replaceMerge).toEqual(["series"]);
|
||||
});
|
||||
|
||||
it("clears dataset when removed entirely", () => {
|
||||
const base: EChartsOption = {
|
||||
dataset: [
|
||||
{
|
||||
id: "sales",
|
||||
source: [
|
||||
["product", "2015"],
|
||||
["Latte", 30],
|
||||
],
|
||||
},
|
||||
],
|
||||
series: [{ id: "sales-series", type: "bar", datasetId: "sales" }],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
series: [{ id: "sales-series", type: "bar", data: [35] }],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.option.dataset).toEqual([]);
|
||||
expect(result.plan.notMerge).toBe(false);
|
||||
expect(result.plan.replaceMerge).toContain("dataset");
|
||||
});
|
||||
|
||||
it("tracks multiple array shrink operations", () => {
|
||||
const base: EChartsOption = {
|
||||
legend: { show: true },
|
||||
dataset: [
|
||||
{
|
||||
id: "2015",
|
||||
source: [
|
||||
["Latte", 30],
|
||||
["Mocha", 24],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2016",
|
||||
source: [
|
||||
["Latte", 40],
|
||||
["Mocha", 35],
|
||||
],
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{ id: "latte", type: "bar", datasetId: "2015" },
|
||||
{ id: "mocha", type: "bar", datasetId: "2016" },
|
||||
],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
series: [{ id: "latte", type: "bar", datasetId: "2015" }],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.option.legend).toBeNull();
|
||||
expect(result.option.dataset).toEqual([]);
|
||||
expect(result.plan.notMerge).toBe(false);
|
||||
expect(result.plan.replaceMerge).toEqual(["dataset", "series"]);
|
||||
});
|
||||
|
||||
it("injects null for tooltip removal while keeping explicit arrays", () => {
|
||||
const base: EChartsOption = {
|
||||
tooltip: { trigger: "axis" },
|
||||
xAxis: [{ type: "category", data: ["Jan", "Feb"] }],
|
||||
series: [{ type: "line", data: [10, 20] }],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
xAxis: [{ type: "category", data: ["Jan", "Feb"] }],
|
||||
series: [{ type: "line", data: [12, 18] }],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.option.tooltip).toBeNull();
|
||||
expect(result.option.xAxis).toEqual(update.xAxis);
|
||||
expect(result.plan.notMerge).toBe(false);
|
||||
expect(result.plan.replaceMerge).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles dataset to series data migration", () => {
|
||||
const base: EChartsOption = {
|
||||
dataset: [
|
||||
{
|
||||
id: "sales",
|
||||
source: [
|
||||
["Latte", 30],
|
||||
["Mocha", 40],
|
||||
],
|
||||
},
|
||||
],
|
||||
series: [{ id: "sales", type: "bar", datasetId: "sales" }],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
series: [{ id: "sales", type: "bar", data: [35, 44] }],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.option.dataset).toEqual([]);
|
||||
expect(result.option.series).toEqual(update.series);
|
||||
expect(result.plan.replaceMerge).toEqual(["dataset"]);
|
||||
});
|
||||
|
||||
it("tracks series ID removal while keeping modifications", () => {
|
||||
const base: EChartsOption = {
|
||||
series: [
|
||||
{ id: "latte", type: "bar", data: [10, 20] },
|
||||
{ id: "mocha", type: "bar", data: [15, 25] },
|
||||
],
|
||||
};
|
||||
|
||||
const update: EChartsOption = {
|
||||
series: [{ id: "latte", type: "line", data: [11, 22] }],
|
||||
};
|
||||
|
||||
const result = planUpdate(buildSignature(base), update);
|
||||
|
||||
expect(result.option.series).toEqual(update.series);
|
||||
expect(result.plan.replaceMerge).toEqual(["series"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
8
tsconfig.vitest.json
Normal file
8
tsconfig.vitest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "node"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["tests/**/*.ts", "src/**/*.ts"]
|
||||
}
|
||||
12
vitest.config.ts
Normal file
12
vitest.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["tests/**/*.test.ts"],
|
||||
coverage: {
|
||||
reporter: ["text", "lcov"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user