From 18f17fb6f4d77713196f39f3a709875ec14a0bac Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 11 Dec 2024 07:24:16 +0100 Subject: [PATCH 1/3] more actions tests --- .../src/actions/configureFramework.test.tsx | 105 ++++++++++++++++++ .../lib/src/actions/configureFramework.ts | 11 +- 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 chartlets.js/packages/lib/src/actions/configureFramework.test.tsx diff --git a/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx b/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx new file mode 100644 index 0000000..654e76e --- /dev/null +++ b/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx @@ -0,0 +1,105 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { configureFramework, resolvePlugin } from "./configureFramework"; +import { store } from "@/store"; +import { registry } from "@/components/registry"; +import { ComponentProps } from "@/components/Component"; +import { FC } from "react"; + +function getComponents() { + interface DivProps extends ComponentProps { + text: string; + } + const Div: FC = ({ text }) =>
{text}
; + return [ + ["A", Div as FC], + ["B", Div as FC], + ]; +} + +describe("configureFramework", () => { + it("should accept no arg", () => { + configureFramework(); + expect(store.getState().configuration).toEqual({}); + }); + + it("should accept empty arg", () => { + configureFramework({}); + expect(store.getState().configuration).toEqual({}); + }); + + it("should enable logging", () => { + configureFramework({ + logging: { + enabled: true, + }, + }); + expect(store.getState().configuration).toEqual({ + logging: { enabled: true }, + }); + }); + + it("should subscribe to host store", () => { + const listeners = []; + const hostStore = { + get: (_key: string) => null, + subscribe: (l: () => void) => { + listeners.push(l); + }, + }; + configureFramework({ + hostStore, + }); + expect(listeners.length).toBe(1); + }); + + it("should install plugins", () => { + expect(registry.types.length).toBe(0); + configureFramework({ + plugins: [{ components: getComponents() }], + }); + expect(registry.types.length).toBe(2); + }); +}); + +describe("resolvePlugin", () => { + beforeEach(() => { + registry.clear(); + }); + + afterEach(() => { + registry.clear(); + }); + + it("should resolve a object", async () => { + const pluginObj = { components: getComponents() }; + expect(registry.types.length).toBe(0); + const result = await resolvePlugin(pluginObj); + expect(result).toBe(pluginObj); + expect(registry.types.length).toBe(2); + }); + + it("should resolve a function", async () => { + const pluginObj = { components: getComponents() }; + const pluginFunction = () => pluginObj; + expect(registry.types.length).toBe(0); + const result = await resolvePlugin(pluginFunction); + expect(result).toBe(pluginObj); + expect(registry.types.length).toBe(2); + }); + + it("should resolve a promise", async () => { + const pluginObj = { components: getComponents() }; + const pluginPromise = Promise.resolve(pluginObj); + expect(registry.types.length).toBe(0); + const result = await resolvePlugin(pluginPromise); + expect(result).toBe(pluginObj); + expect(registry.types.length).toBe(2); + }); + + it("should resolve undefined", async () => { + expect(registry.types.length).toBe(0); + const result = await resolvePlugin(undefined); + expect(result).toBe(undefined); + expect(registry.types.length).toBe(0); + }); +}); diff --git a/chartlets.js/packages/lib/src/actions/configureFramework.ts b/chartlets.js/packages/lib/src/actions/configureFramework.ts index 1e34987..05a512e 100644 --- a/chartlets.js/packages/lib/src/actions/configureFramework.ts +++ b/chartlets.js/packages/lib/src/actions/configureFramework.ts @@ -11,7 +11,8 @@ import { isObject } from "@/utils/isObject"; import { handleHostStoreChange } from "./handleHostStoreChange"; import { configureLogging } from "./helpers/configureLogging"; -export function configureFramework(options: FrameworkOptions) { +export function configureFramework(options?: FrameworkOptions) { + options = options || {}; if (options.logging) { configureLogging(options.logging); } @@ -26,16 +27,18 @@ export function configureFramework(options: FrameworkOptions) { } } -function resolvePlugin(plugin: PluginLike) { +export function resolvePlugin(plugin: PluginLike): Promise { if (isPromise(plugin)) { - plugin.then(resolvePlugin); + return plugin.then(resolvePlugin); } else if (isFunction(plugin)) { - resolvePlugin(plugin()); + return resolvePlugin(plugin()); } else if (isObject(plugin) && plugin.components) { (plugin.components as ComponentRegistration[]).forEach( ([name, component]) => { registry.register(name, component); }, ); + return Promise.resolve(plugin); } + return Promise.resolve(undefined); } From 3ba54dc1f6b2bc16c911eff39e251bba4f54f1fa Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 11 Dec 2024 08:42:07 +0100 Subject: [PATCH 2/3] testing handleHostStoreChange() --- .../lib/src/actions/handleHostStoreChange.ts | 11 ++- .../actions/handleHostStoreChanges.test.tsx | 85 +++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index fa76991..3175811 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -11,6 +11,7 @@ import { formatObjPath } from "@/utils/objPath"; import { invokeCallbacks } from "@/actions/helpers/invokeCallbacks"; import type { ContributionState } from "@/types/state/contribution"; import type { HostStore } from "@/types/state/options"; +import type { store } from "@/store"; /** * A reference to a property of an input of a callback of a contribution. @@ -23,12 +24,16 @@ export interface PropertyRef extends ContribRef, CallbackRef, InputRef { export function handleHostStoreChange() { const { extensions, configuration, contributionsRecord } = store.getState(); const { hostStore } = configuration; - if (!hostStore || extensions.length === 0) { - // Exit if no host store configured or - // there are no extensions (yet) + if (!hostStore) { + // Exit if no host store configured. + // Actually, we should not come here. return; } synchronizeThemeMode(hostStore); + if (extensions.length === 0) { + // Exit if there are no extensions (yet) + return; + } const propertyRefs = getHostStorePropertyRefs(); if (!propertyRefs || propertyRefs.length === 0) { // Exit if there are is nothing to be changed diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx new file mode 100644 index 0000000..49877ac --- /dev/null +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { store } from "@/store"; +import { handleHostStoreChange } from "./handleHostStoreChange"; + +describe("handleHostStoreChange", () => { + let listeners: (() => void)[] = []; + let hostState: Record = {}; + const hostStore = { + get: (key: string) => hostState[key], + set: (key: string, value: unknown) => { + hostState = { ...hostState, [key]: value }; + listeners.forEach((l) => void l()); + }, + subscribe: (_l: () => void) => { + listeners.push(_l); + }, + }; + + beforeEach(() => { + listeners = []; + hostState = {}; + }); + + it("should do nothing without host store", () => { + store.setState({ configuration: {} }); + const oldState = store.getState(); + handleHostStoreChange(); + const newState = store.getState(); + expect(newState).toBe(oldState); + expect(newState).toEqual(oldState); + }); + + it("should synchronize theme mode", () => { + store.setState({ configuration: { hostStore } }); + expect(store.getState().themeMode).toBeUndefined(); + hostStore.set("themeMode", "light"); + handleHostStoreChange(); + expect(store.getState().themeMode).toEqual("light"); + }); + + it("should generate callback requests", () => { + const extensions = [{ name: "e0", version: "0", contributes: ["panels"] }]; + store.setState({ + configuration: { hostStore }, + extensions, + contributionsResult: { + status: "ok", + data: { + extensions, + contributions: { + panels: [ + { + name: "p0", + extension: "e0", + layout: { + function: { + name: "layout", + parameters: [], + return: {}, + }, + inputs: [], + outputs: [], + }, + callbacks: [ + { + function: { + name: "callback", + parameters: [], + return: {}, + }, + inputs: [{ id: "@app", property: "variableName" }], + outputs: [{ id: "select", property: "value" }], + }, + ], + initialState: {}, + }, + ], + }, + }, + }, + }); + hostStore.set("variableName", "CHL"); + handleHostStoreChange(); + }); +}); From d597c8e6142208d359c4e5e73fd6e1eee87e3e0d Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 11 Dec 2024 17:31:00 +0100 Subject: [PATCH 3/3] Fixed tests; split options type module --- .../src/actions/configureFramework.test.tsx | 14 ++-- .../lib/src/actions/configureFramework.ts | 7 +- .../lib/src/actions/handleHostStoreChange.ts | 5 +- .../helpers/applyStateChangeRequests.ts | 5 +- .../lib/src/actions/helpers/getInputValues.ts | 2 +- chartlets.js/packages/lib/src/index.ts | 10 +-- .../state/{options.test.ts => host.test.ts} | 2 +- .../packages/lib/src/types/state/host.ts | 60 ++++++++++++++ .../packages/lib/src/types/state/options.ts | 81 +------------------ .../packages/lib/src/types/state/plugin.ts | 28 +++++++ 10 files changed, 110 insertions(+), 104 deletions(-) rename chartlets.js/packages/lib/src/types/state/{options.test.ts => host.test.ts} (97%) create mode 100644 chartlets.js/packages/lib/src/types/state/host.ts create mode 100644 chartlets.js/packages/lib/src/types/state/plugin.ts diff --git a/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx b/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx index 654e76e..1dbb6e1 100644 --- a/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx +++ b/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx @@ -1,11 +1,13 @@ +import type { ComponentType, FC } from "react"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { configureFramework, resolvePlugin } from "./configureFramework"; import { store } from "@/store"; import { registry } from "@/components/registry"; -import { ComponentProps } from "@/components/Component"; -import { FC } from "react"; +import type { HostStore } from "@/types/state/host"; +import type { Plugin } from "@/types/state/plugin"; +import type { ComponentProps } from "@/components/Component"; -function getComponents() { +function getComponents(): [string, ComponentType][] { interface DivProps extends ComponentProps { text: string; } @@ -40,7 +42,7 @@ describe("configureFramework", () => { it("should subscribe to host store", () => { const listeners = []; - const hostStore = { + const hostStore: HostStore = { get: (_key: string) => null, subscribe: (l: () => void) => { listeners.push(l); @@ -71,7 +73,7 @@ describe("resolvePlugin", () => { }); it("should resolve a object", async () => { - const pluginObj = { components: getComponents() }; + const pluginObj: Plugin = { components: getComponents() }; expect(registry.types.length).toBe(0); const result = await resolvePlugin(pluginObj); expect(result).toBe(pluginObj); @@ -98,7 +100,7 @@ describe("resolvePlugin", () => { it("should resolve undefined", async () => { expect(registry.types.length).toBe(0); - const result = await resolvePlugin(undefined); + const result = await resolvePlugin(undefined as unknown as Plugin); expect(result).toBe(undefined); expect(registry.types.length).toBe(0); }); diff --git a/chartlets.js/packages/lib/src/actions/configureFramework.ts b/chartlets.js/packages/lib/src/actions/configureFramework.ts index 05a512e..9a732c2 100644 --- a/chartlets.js/packages/lib/src/actions/configureFramework.ts +++ b/chartlets.js/packages/lib/src/actions/configureFramework.ts @@ -1,9 +1,10 @@ import { store } from "@/store"; +import type { FrameworkOptions } from "@/types/state/options"; import type { ComponentRegistration, - FrameworkOptions, + Plugin, PluginLike, -} from "@/types/state/options"; +} from "@/types/state/plugin"; import { registry } from "@/components/registry"; import { isPromise } from "@/utils/isPromise"; import { isFunction } from "@/utils/isFunction"; @@ -38,7 +39,7 @@ export function resolvePlugin(plugin: PluginLike): Promise { registry.register(name, component); }, ); - return Promise.resolve(plugin); + return Promise.resolve(plugin as Plugin); } return Promise.resolve(undefined); } diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index 3175811..31c7c55 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -1,4 +1,3 @@ -import { store } from "@/store"; import type { CallbackRef, CallbackRequest, @@ -10,8 +9,8 @@ import { getInputValues } from "@/actions/helpers/getInputValues"; import { formatObjPath } from "@/utils/objPath"; import { invokeCallbacks } from "@/actions/helpers/invokeCallbacks"; import type { ContributionState } from "@/types/state/contribution"; -import type { HostStore } from "@/types/state/options"; -import type { store } from "@/store"; +import type { HostStore } from "@/types/state/host"; +import { store } from "@/store"; /** * A reference to a property of an input of a callback of a contribution. diff --git a/chartlets.js/packages/lib/src/actions/helpers/applyStateChangeRequests.ts b/chartlets.js/packages/lib/src/actions/helpers/applyStateChangeRequests.ts index e7867d3..81e2eac 100644 --- a/chartlets.js/packages/lib/src/actions/helpers/applyStateChangeRequests.ts +++ b/chartlets.js/packages/lib/src/actions/helpers/applyStateChangeRequests.ts @@ -15,10 +15,7 @@ import { normalizeObjPath, setValue, } from "@/utils/objPath"; -import { - isMutableHostStore, - type MutableHostStore, -} from "@/types/state/options"; +import { isMutableHostStore, type MutableHostStore } from "@/types/state/host"; import { isHostChannel, isComponentChannel, diff --git a/chartlets.js/packages/lib/src/actions/helpers/getInputValues.ts b/chartlets.js/packages/lib/src/actions/helpers/getInputValues.ts index a3be5e6..451bbea 100644 --- a/chartlets.js/packages/lib/src/actions/helpers/getInputValues.ts +++ b/chartlets.js/packages/lib/src/actions/helpers/getInputValues.ts @@ -12,7 +12,7 @@ import { } from "@/types/state/component"; import { formatObjPath, getValue, type ObjPathLike } from "@/utils/objPath"; import { isObject } from "@/utils/isObject"; -import type { HostStore } from "@/types/state/options"; +import type { HostStore } from "@/types/state/host"; export function getInputValues( inputs: Input[], diff --git a/chartlets.js/packages/lib/src/index.ts b/chartlets.js/packages/lib/src/index.ts index c718e26..6271e50 100644 --- a/chartlets.js/packages/lib/src/index.ts +++ b/chartlets.js/packages/lib/src/index.ts @@ -29,10 +29,6 @@ export { } from "@/hooks"; // Application interface -export type { - FrameworkOptions, - HostStore, - MutableHostStore, - Plugin, - PluginLike, -} from "@/types/state/options"; +export type { HostStore, MutableHostStore } from "@/types/state/host"; +export type { Plugin, PluginLike } from "@/types/state/plugin"; +export type { FrameworkOptions } from "@/types/state/options"; diff --git a/chartlets.js/packages/lib/src/types/state/options.test.ts b/chartlets.js/packages/lib/src/types/state/host.test.ts similarity index 97% rename from chartlets.js/packages/lib/src/types/state/options.test.ts rename to chartlets.js/packages/lib/src/types/state/host.test.ts index d83e189..339a064 100644 --- a/chartlets.js/packages/lib/src/types/state/options.test.ts +++ b/chartlets.js/packages/lib/src/types/state/host.test.ts @@ -4,7 +4,7 @@ import { type MutableHostStore, isHostStore, isMutableHostStore, -} from "./options"; +} from "./host"; const hostStore: HostStore = { get: (name: string) => name, diff --git a/chartlets.js/packages/lib/src/types/state/host.ts b/chartlets.js/packages/lib/src/types/state/host.ts new file mode 100644 index 0000000..9d0ebf1 --- /dev/null +++ b/chartlets.js/packages/lib/src/types/state/host.ts @@ -0,0 +1,60 @@ +import { isObject } from "@/utils/isObject"; +import { isFunction } from "@/utils/isFunction"; + +/** + * The host store represents an interface to the state of + * the application that is using Chartlets. + */ +export interface HostStore { + /** + * Let Chartlets listen to changes in the host store that may + * cause different values to be returned from the `get()` method. + * + * @param listener A listener that is called when the + * host store changes + */ + subscribe: (listener: () => void) => void; + + /** + * Get a property value from the host state. + * + * @param property The property name. + * @returns The property value. + */ + get: (property: string) => unknown; + + /** + * **UNSTABLE API** + * + * Set a property value in the host state. + * + * @param property The property name. + * @param value The new property value. + */ + set?: (property: string, value: unknown) => void; +} + +/** + * A mutable host store implements the `set()` method. + */ +export interface MutableHostStore extends HostStore { + /** + * **UNSTABLE API** + * + * Set a property value in the host state. + * + * @param property The property name. + * @param value The new property value. + */ + set: (property: string, value: unknown) => void; +} + +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); +} diff --git a/chartlets.js/packages/lib/src/types/state/options.ts b/chartlets.js/packages/lib/src/types/state/options.ts index 984cd04..0101277 100644 --- a/chartlets.js/packages/lib/src/types/state/options.ts +++ b/chartlets.js/packages/lib/src/types/state/options.ts @@ -1,84 +1,7 @@ -import type { ComponentType } from "react"; - import type { ApiOptions } from "@/types/api"; +import type { PluginLike } from "@/types/state/plugin"; +import type { HostStore } from "@/types/state/host"; import type { LoggingOptions } from "@/actions/helpers/configureLogging"; -import type { ComponentProps } from "@/components/Component"; -import { isObject } from "@/utils/isObject"; -import { isFunction } from "@/utils/isFunction"; - -/** - * The host store represents an interface to the state of - * the application that is using Chartlets. - */ -export interface HostStore { - /** - * Let Chartlets listen to changes in the host store that may - * cause different values to be returned from the `get()` method. - * - * @param listener A listener that is called when the - * host store changes - */ - subscribe: (listener: () => void) => void; - - /** - * Get a property value from the host state. - * - * @param property The property name. - * @returns The property value. - */ - get: (property: string) => unknown; - - /** - * **UNSTABLE API** - * - * Set a property value in the host state. - * - * @param property The property name. - * @param value The new property value. - */ - set?: (property: string, value: unknown) => void; -} - -/** - * A mutable host store implements the `set()` method. - */ -export interface MutableHostStore extends HostStore { - /** - * **UNSTABLE API** - * - * Set a property value in the host state. - * - * @param property The property name. - * @param value The new property value. - */ - set: (property: string, value: unknown) => void; -} - -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); -} - -/** - * A framework plugin. - * Plugins are no-arg functions that are called - * after the framework's initialisation. - * Most typically, a plugin wants to return new components - * in the `components` array: - * `{ components: [["MyComponent", MyComponent]] }`. - */ -export interface Plugin { - components?: ComponentRegistration[]; -} - -export type ComponentRegistration = [string, ComponentType]; - -export type PluginLike = Plugin | (() => Plugin) | Promise; /** * Chartlets options to be provided diff --git a/chartlets.js/packages/lib/src/types/state/plugin.ts b/chartlets.js/packages/lib/src/types/state/plugin.ts new file mode 100644 index 0000000..967ab4c --- /dev/null +++ b/chartlets.js/packages/lib/src/types/state/plugin.ts @@ -0,0 +1,28 @@ +import type { ComponentType } from "react"; +import type { ComponentProps } from "@/components/Component"; + +/** + * A component registration - a pair comprising the component type name + * and the React component. + */ +export type ComponentRegistration = [string, ComponentType]; + +/** + * A framework plugin. + * Plugins are no-arg functions that are called + * after the framework's initialisation. + * Most typically, a plugin wants to return new components + * in the `components` array: + * `{ components: [["MyComponent", MyComponent]] }`. + */ +export interface Plugin { + components?: ComponentRegistration[]; +} + +/** + * An object of type `Plugin`, or a function that returns a + * value that can be resolved to a `Plugin`, + * or a promise that resolves to a value that can be + * resolved to a `Plugin`. + */ +export type PluginLike = Plugin | (() => PluginLike) | Promise;