Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 290 additions & 23 deletions chartlets.js/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion chartlets.js/packages/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"@vitest/coverage-v8": "^2.1.1",
"@vitest/coverage-istanbul": "^2.1.8",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.12",
Expand Down
2 changes: 1 addition & 1 deletion chartlets.js/packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"@vitest/coverage-v8": "^2.1.1",
"@vitest/coverage-istanbul": "^2.1.8",
"canvas": "^2.11.2",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
Expand Down
63 changes: 63 additions & 0 deletions chartlets.js/packages/lib/src/component/Children.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { FC } from "react";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { registry } from "@/component/Registry";
import { type ComponentProps } from "./Component";
import { Children } from "./Children";

describe("Children", () => {
beforeEach(() => {
registry.clear();
});

afterEach(() => {
registry.clear();
});

function expectDocumentIsEmpty() {
// Note, the following 3 lines test that document is empty
// but there is always one empty "div" in it.
expect(document.body.firstElementChild).not.toBe(null);
expect(document.body.firstElementChild!.tagName).toBe("DIV");
expect(document.body.firstElementChild!.firstElementChild).toBe(null);
}

it("should not render undefined nodes", () => {
render(<Children onChange={() => {}} />);
expectDocumentIsEmpty();
});

it("should not render empty nodes", () => {
render(<Children nodes={[]} onChange={() => {}} />);
expectDocumentIsEmpty();
});

it("should render all child types", () => {
interface DivProps extends ComponentProps {
text: string;
}
const Div: FC<DivProps> = ({ text }) => <div>{text}</div>;
registry.register("Div", Div as FC<ComponentProps>);
const divProps = {
type: "Div",
text: "Hello",
onChange: () => {},
};
render(
<Children
nodes={[
divProps, // ok, regular component
"World", // ok, text
null, // ok, not rendered
undefined, // ok, not rendered
<div />, // not ok, emits warning, not rendered
]}
onChange={() => {}}
/>,
);
// to inspect rendered component, do:
// expect(document.body).toEqual({});
expect(screen.getByText("Hello")).not.toBeUndefined();
expect(screen.getByText("World")).not.toBeUndefined();
});
});
46 changes: 46 additions & 0 deletions chartlets.js/packages/lib/src/component/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { Component, type ComponentProps } from "./Component";
import { registry } from "@/component/Registry";
import type { FC } from "react";

describe("Component", () => {
beforeEach(() => {
registry.clear();
});

afterEach(() => {
registry.clear();
});

it("should not render unknown Component types", () => {
const boxProps = {
type: "Box",
id: "bx",
children: ["Hello!", "World!"],
onChange: () => {},
};
render(<Component {...boxProps} />);
// to inspect rendered component, do:
// expect(document.body).toEqual({});
expect(document.querySelector("#bx")).toBe(null);
});

it("should render a known component", () => {
interface DivProps extends ComponentProps {
text: string;
}
const Div: FC<DivProps> = ({ text }) => <div>{text}</div>;
registry.register("Div", Div as FC<ComponentProps>);
const divProps = {
type: "Div",
id: "root",
text: "Hello!",
onChange: () => {},
};
render(<Component {...divProps} />);
// to inspect rendered component, do:
// expect(document.body).toEqual({});
expect(screen.getByText("Hello!")).not.toBeUndefined();
});
});
13 changes: 13 additions & 0 deletions chartlets.js/packages/lib/src/component/Registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,17 @@ describe("Test that RegistryImpl", () => {
expect(registry.lookup("C")).toBeUndefined();
expect(registry.types).toEqual([]);
});

it("clears", () => {
const registry = new RegistryImpl();
const A = () => void 0;
const B = () => void 0;
const C = () => void 0;
registry.register("A", A);
registry.register("B", B);
registry.register("C", C);
expect(registry.types.length).toBe(3);
registry.clear();
expect(registry.types).toEqual([]);
});
});
10 changes: 10 additions & 0 deletions chartlets.js/packages/lib/src/component/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export interface Registry {
*/
lookup(type: string): ComponentType<ComponentProps> | undefined;

/**
* Clears the registry.
* For testing only.
*/
clear(): void;

/**
* Get the type names of all registered components.
*/
Expand All @@ -58,6 +64,10 @@ export class RegistryImpl implements Registry {
return this.components.get(type);
}

clear() {
this.components.clear();
}

get types(): string[] {
return Array.from(this.components.keys());
}
Expand Down
2 changes: 1 addition & 1 deletion chartlets.js/packages/lib/src/plugins/mui/Box.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { Box } from "./Box";
import type { ComponentChangeHandler } from "@/types/state/event";
import { Box } from "./Box";

describe("Box", () => {
it("should render the Box component", () => {
Expand Down
17 changes: 17 additions & 0 deletions chartlets.js/packages/lib/src/plugins/mui/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect } from "vitest";
import mui from "./index";

describe("mui Plugin", () => {
it("registers components", () => {
const plugin = mui();
expect(plugin).toBeTypeOf("object");
expect(Array.isArray(plugin.components)).toBe(true);
expect(plugin.components?.length).toBeGreaterThan(0);
plugin.components?.forEach((componentRegistration) => {
expect(componentRegistration).toHaveLength(2);
const [name, component] = componentRegistration;
expect(name).toBeTypeOf("string");
expect(component).toBeTypeOf("function");
});
});
});
17 changes: 17 additions & 0 deletions chartlets.js/packages/lib/src/plugins/vega/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect } from "vitest";
import vega from "./index";

describe("vega Plugin", () => {
it("registers components", () => {
const plugin = vega();
expect(plugin).toBeTypeOf("object");
expect(Array.isArray(plugin.components)).toBe(true);
expect(plugin.components?.length).toBeGreaterThan(0);
plugin.components?.forEach((componentRegistration) => {
expect(componentRegistration).toHaveLength(2);
const [name, component] = componentRegistration;
expect(name).toBeTypeOf("string");
expect(component).toBeTypeOf("function");
});
});
});
46 changes: 46 additions & 0 deletions chartlets.js/packages/lib/src/types/model/channel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, expect } from "vitest";
import {
isComponentChannel,
isContainerChannel,
isHostChannel,
} from "./channel";

describe("Test that isComponentChannel()", () => {
it("works", () => {
expect(isComponentChannel({ id: "ok_btn", property: "checked" })).toBe(
true,
);
expect(isComponentChannel({ id: "@container", property: "visible" })).toBe(
false,
);
expect(
isComponentChannel({ id: "@app", property: "selectedDatasetId" }),
).toBe(false);
});
});

describe("Test that isContainerChannel()", () => {
it("works", () => {
expect(isContainerChannel({ id: "ok_btn", property: "checked" })).toBe(
false,
);
expect(isContainerChannel({ id: "@container", property: "visible" })).toBe(
true,
);
expect(
isContainerChannel({ id: "@app", property: "selectedDatasetId" }),
).toBe(false);
});
});

describe("Test that isHostChannel()", () => {
it("works", () => {
expect(isHostChannel({ id: "ok_btn", property: "checked" })).toBe(false);
expect(isHostChannel({ id: "@container", property: "visible" })).toBe(
false,
);
expect(isHostChannel({ id: "@app", property: "selectedDatasetId" })).toBe(
true,
);
});
});
26 changes: 26 additions & 0 deletions chartlets.js/packages/lib/src/types/state/component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest";
import { isComponentState, isContainerState } from "./component";

describe("isComponentState", () => {
it("should work", () => {
expect(isComponentState({ type: "Button" })).toBe(true);
expect(isComponentState({ type: "Button", children: [] })).toBe(true);
expect(isComponentState({})).toBe(false);
expect(isComponentState("Button")).toBe(false);
expect(isComponentState({ type: 2 })).toBe(false);
expect(isComponentState(new Event("click"))).toBe(false);
expect(isComponentState(<span />)).toBe(false);
});
});

describe("isContainerState", () => {
it("should work", () => {
expect(isContainerState({ type: "Button" })).toBe(false);
expect(isContainerState({ type: "Button", children: [] })).toBe(true);
expect(isContainerState({})).toBe(false);
expect(isContainerState("Button")).toBe(false);
expect(isContainerState({ type: 2 })).toBe(false);
expect(isComponentState(new Event("click"))).toBe(false);
expect(isComponentState(<span />)).toBe(false);
});
});
17 changes: 8 additions & 9 deletions chartlets.js/packages/lib/src/types/state/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import { type CSSProperties } from "react";
import { isObject } from "@/utils/isObject";
import { isString } from "@/utils/isString";

export type ComponentType =
| "Box"
| "Button"
| "Checkbox"
| "Plot"
| "Select"
| "Typography";

export type ComponentNode =
| ComponentState
| string
Expand Down Expand Up @@ -38,7 +30,14 @@ export interface ContainerState extends ComponentState {
}

export function isComponentState(object: unknown): object is ComponentState {
return isObject(object) && isString(object.type);
return (
isObject(object) &&
isString(object.type) &&
// objects that are not created from classes
object.constructor.name === "Object" &&
// not React elements
!object["$$typeof"]
);
}

export function isContainerState(object: unknown): object is ContainerState {
Expand Down
33 changes: 33 additions & 0 deletions chartlets.js/packages/lib/src/types/state/options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, it, expect } from "vitest";
import {
type HostStore,
type MutableHostStore,
isHostStore,
isMutableHostStore,
} from "./options";

const hostStore: HostStore = {
get: (name: string) => name,
subscribe: (_cb: () => void) => {},
};

const mutableHostStore: MutableHostStore = {
...hostStore,
set: (_name: string, _value: unknown) => {},
};

describe("isHostStore", () => {
it("should work", () => {
expect(isHostStore({})).toBe(false);
expect(isHostStore(hostStore)).toBe(true);
expect(isHostStore(mutableHostStore)).toBe(true);
});
});

describe("isMutableHostStore", () => {
it("should work", () => {
expect(isMutableHostStore({})).toBe(false);
expect(isMutableHostStore(hostStore)).toBe(false);
expect(isMutableHostStore(mutableHostStore)).toBe(true);
});
});
14 changes: 10 additions & 4 deletions chartlets.js/packages/lib/src/types/state/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ComponentType } from "react";
import type { ApiOptions } from "@/types/api";
import type { LoggingOptions } from "@/actions/helpers/configureLogging";
import type { ComponentProps } from "@/component/Component";
import { isObject } from "@/utils/isObject";
import { isFunction } from "@/utils/isFunction";

/**
* The host store represents an interface to the state of
Expand Down Expand Up @@ -52,10 +54,14 @@ export interface MutableHostStore extends HostStore {
set: (property: string, value: unknown) => void;
}

export function isMutableHostStore(
hostStore: HostStore | undefined,
): hostStore is MutableHostStore {
return !!hostStore && typeof hostStore.set === "function";
export function isHostStore(value: unknown): value is HostStore {
return (
isObject(value) && isFunction(value.get) && isFunction(value.subscribe)
);
}

export function isMutableHostStore(value: unknown): value is MutableHostStore {
return isHostStore(value) && isFunction(value.set);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions chartlets.js/packages/lib/src/utils/hasOwnProperty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, it, expect } from "vitest";
import { hasOwnProperty } from "@/utils/hasOwnProperty";

describe("Test that hasOwnProperty()", () => {
it("works", () => {
expect(hasOwnProperty({ test: 13 }, "test")).toBe(true);
expect(hasOwnProperty({ test: 13 }, "test2")).toBe(false);
});
});
Loading
Loading