Files
vueecharts/tests/graphic.node.test.ts

473 lines
11 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { buildGraphicOption } from "../src/graphic/build";
import { createGraphicCollector } from "../src/graphic/collector";
const flushMicrotasks = () => new Promise<void>((resolve) => queueMicrotask(() => resolve()));
function getRootGraphicElement(option: unknown): any {
const root = (option as any).graphic?.elements?.[0] as any;
if (!root) {
throw new Error("Expected root graphic element to exist.");
}
return root;
}
describe("graphic", () => {
it("builds graphic option with ordered children and replace root", () => {
const nodes = [
{
id: "rect",
type: "rect",
parentId: null,
props: {
x: 10,
y: 20,
width: 30,
height: 40,
progressive: 8,
textContent: { type: "text", style: { text: "label" } },
textConfig: { position: "inside" },
fill: "#f00",
},
handlers: {},
order: 1,
sourceId: 1,
},
{
id: "text",
type: "text",
parentId: null,
props: {
x: 2,
y: 4,
width: 120,
overflow: "truncate",
ellipsis: "...",
text: "Hi",
textFill: "#000",
},
handlers: {},
order: 0,
sourceId: 2,
},
];
const option = buildGraphicOption(nodes, "root");
const root = getRootGraphicElement(option);
expect(root.id).toBe("root");
expect(root.$action).toBe("replace");
const [text, rect] = root.children as any[];
expect(text.type).toBe("text");
expect(text.x).toBe(2);
expect(text.y).toBe(4);
expect(text.style).toMatchObject({
text: "Hi",
textFill: "#000",
width: 120,
overflow: "truncate",
ellipsis: "...",
});
expect(text.width).toBeUndefined();
expect(rect.type).toBe("rect");
expect(rect.progressive).toBe(8);
expect(rect.textContent).toMatchObject({ type: "text" });
expect(rect.textConfig).toMatchObject({ position: "inside" });
expect(rect.shape).toMatchObject({ x: 10, y: 20, width: 30, height: 40 });
expect(rect.style).toMatchObject({ fill: "#f00" });
expect(root.children.some((child: any) => child.id === "rect")).toBe(true);
});
it("keeps user info as-is and maps handlers to graphic onxxx props", () => {
const nodes = [
{
id: "hit",
type: "circle",
parentId: null,
props: {
cx: 1,
cy: 2,
r: 3,
info: { name: "marker" },
},
handlers: { onClick: () => void 0 },
order: 0,
sourceId: 1,
},
];
const option = buildGraphicOption(nodes, "root");
const root = getRootGraphicElement(option);
const child = root.children?.[0] as Record<string, unknown> | undefined;
if (!child) {
throw new Error("Expected child graphic element to exist.");
}
const info = child.info as Record<string, unknown>;
expect(info).toMatchObject({ name: "marker" });
expect(typeof child.onclick).toBe("function");
});
it("builds image/group options and covers info fallback branches", () => {
const nodes = [
{
id: "group",
type: "group",
parentId: null,
props: {
info: "root",
},
handlers: {},
order: 0,
sourceId: 1,
},
{
id: "img",
type: "image",
parentId: "group",
props: {
x: 1,
y: 2,
width: 3,
height: 4,
image: "https://example.com/a.png",
styleTransition: "all",
},
handlers: {},
order: 0,
sourceId: 2,
},
{
id: "img-hit",
type: "image",
parentId: "group",
props: {
image: "https://example.com/b.png",
},
handlers: { onClick: () => void 0 },
order: 2,
sourceId: 7,
},
{
id: "line",
type: "line",
parentId: "group",
props: {
x1: 0,
y1: 0,
x2: 10,
y2: 10,
shapeTransition: "shape",
info: 42,
},
handlers: {},
order: 1,
sourceId: 3,
},
{
id: "custom",
type: "custom",
parentId: null,
props: { info: { level: "custom" } },
handlers: {},
order: 1,
sourceId: 4,
},
{
id: "txt",
type: "text",
parentId: null,
props: {
text: "hello",
},
handlers: { onMouseover: () => void 0 },
order: 4,
sourceId: 8,
},
{
id: "dup",
type: "rect",
parentId: null,
props: {
x: 0,
y: 0,
width: 1,
height: 1,
},
handlers: {},
order: 2,
sourceId: 5,
},
{
id: "dup",
type: "rect",
parentId: null,
props: {
x: 2,
y: 2,
width: 1,
height: 1,
},
handlers: {},
order: 3,
sourceId: 6,
},
];
const option = buildGraphicOption(nodes, "root");
const root = getRootGraphicElement(option);
const group = root.children.find((item: any) => item.id === "group");
if (!group) {
throw new Error("Expected group node to exist.");
}
const image = group.children.find((item: any) => item.id === "img");
const line = group.children.find((item: any) => item.id === "line");
const imageHit = group.children.find((item: any) => item.id === "img-hit");
const custom = root.children.find((item: any) => item.id === "custom");
const text = root.children.find((item: any) => item.id === "txt");
expect(group.info).toBe("root");
expect(image.style).toMatchObject({
image: "https://example.com/a.png",
transition: "all",
});
expect(line.shape).toMatchObject({
x1: 0,
y1: 0,
x2: 10,
y2: 10,
transition: "shape",
});
expect(line.info).toBe(42);
expect(imageHit.info).toBeUndefined();
expect(custom.info).toMatchObject({ level: "custom" });
expect(text.info).toBeUndefined();
expect(typeof imageHit.onclick).toBe("function");
expect(typeof text.onmouseover).toBe("function");
expect(custom.shape).toBeUndefined();
expect(root.children.filter((item: any) => item.id === "dup")).toHaveLength(2);
});
it("coalesces flushes and warns on duplicate ids", async () => {
const onFlush = vi.fn();
const warn = vi.fn();
const collector = createGraphicCollector({ onFlush, warn });
collector.register({
id: "dup",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 1,
});
collector.register({
id: "dup",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 2,
});
expect(warn).toHaveBeenCalledTimes(1);
expect(onFlush).toHaveBeenCalledTimes(0);
await flushMicrotasks();
expect(onFlush).toHaveBeenCalledTimes(1);
collector.unregister("dup");
await flushMicrotasks();
expect(onFlush).toHaveBeenCalledTimes(2);
});
it("exposes current collector nodes", () => {
const collector = createGraphicCollector({
onFlush: () => void 0,
warn: () => void 0,
});
collector.register({
id: "a",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 1,
});
collector.register({
id: "b",
type: "text",
parentId: "a",
props: {},
handlers: {},
sourceId: 2,
});
const nodes = Array.from(collector.getNodes());
expect(nodes.some((item) => item.id === "a")).toBe(true);
expect(nodes.find((item) => item.id === "b")?.parentId).toBe("a");
});
it("ignores unregister from mismatched source and removes with matched source", () => {
const collector = createGraphicCollector({
onFlush: () => void 0,
warn: () => void 0,
});
collector.register({
id: "x",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 1,
});
collector.unregister("x", 2);
expect(Array.from(collector.getNodes()).some((item) => item.id === "x")).toBe(true);
collector.unregister("missing", 1);
expect(Array.from(collector.getNodes()).some((item) => item.id === "x")).toBe(true);
collector.unregister("x", 1);
expect(Array.from(collector.getNodes()).some((item) => item.id === "x")).toBe(false);
});
it("does not mark duplicate when same id appears across different passes", () => {
const warn = vi.fn();
const collector = createGraphicCollector({
onFlush: () => void 0,
warn,
});
collector.beginPass();
collector.register({
id: "node",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 1,
});
collector.beginPass();
collector.register({
id: "node",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 2,
});
expect(warn).not.toHaveBeenCalled();
});
it("skips pending flush callback and ignores operations after dispose", async () => {
const onFlush = vi.fn();
const collector = createGraphicCollector({
onFlush,
warn: () => void 0,
});
collector.register({
id: "node",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 1,
});
collector.dispose();
await flushMicrotasks();
expect(onFlush).toHaveBeenCalledTimes(0);
collector.register({
id: "after-dispose",
type: "rect",
parentId: null,
props: {},
handlers: {},
sourceId: 2,
});
collector.unregister("node");
await flushMicrotasks();
expect(Array.from(collector.getNodes())).toEqual([]);
});
it("accepts null, bigint, and symbol values", async () => {
const onFlush = vi.fn();
const collector = createGraphicCollector({
onFlush,
warn: () => void 0,
});
const onClick = () => void 0;
const marker = Symbol("marker");
collector.register({
id: "typed-node",
type: "rect",
parentId: null,
props: {
nullable: null,
amount: 10n,
marker,
enabled: true,
archived: false,
nested: {
a: null,
b: 20n,
c: marker,
},
list: [null, 30n, marker],
},
handlers: {
onClick,
},
order: 0,
sourceId: 1,
});
await flushMicrotasks();
expect(onFlush).toHaveBeenCalledTimes(1);
collector.register({
id: "typed-node",
type: "rect",
parentId: null,
props: {
nullable: null,
amount: 10n,
marker,
enabled: true,
archived: false,
nested: {
a: null,
b: 20n,
c: marker,
},
list: [null, 30n, marker],
},
handlers: {
onClick,
},
order: 0,
sourceId: 1,
});
await flushMicrotasks();
expect(onFlush).toHaveBeenCalledTimes(2);
});
});