Skip to content

Commit

Permalink
Merge pull request #73 from bcdev/forman-33-actions_tests
Browse files Browse the repository at this point in the history
Actions test #1
  • Loading branch information
forman authored Dec 11, 2024
2 parents a5f6b52 + d597c8e commit f77b23f
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 103 deletions.
107 changes: 107 additions & 0 deletions chartlets.js/packages/lib/src/actions/configureFramework.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 type { HostStore } from "@/types/state/host";
import type { Plugin } from "@/types/state/plugin";
import type { ComponentProps } from "@/components/Component";

function getComponents(): [string, ComponentType<ComponentProps>][] {
interface DivProps extends ComponentProps {
text: string;
}
const Div: FC<DivProps> = ({ text }) => <div>{text}</div>;
return [
["A", Div as FC<ComponentProps>],
["B", Div as FC<ComponentProps>],
];
}

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: 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: Plugin = { 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 as unknown as Plugin);
expect(result).toBe(undefined);
expect(registry.types.length).toBe(0);
});
});
16 changes: 10 additions & 6 deletions chartlets.js/packages/lib/src/actions/configureFramework.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
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";
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);
}
Expand All @@ -26,16 +28,18 @@ export function configureFramework(options: FrameworkOptions) {
}
}

function resolvePlugin(plugin: PluginLike) {
export function resolvePlugin(plugin: PluginLike): Promise<Plugin | undefined> {
if (isPromise<PluginLike>(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 as Plugin);
}
return Promise.resolve(undefined);
}
14 changes: 9 additions & 5 deletions chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { store } from "@/store";
import type {
CallbackRef,
CallbackRequest,
Expand All @@ -10,7 +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 { HostStore } from "@/types/state/host";
import { store } from "@/store";

/**
* A reference to a property of an input of a callback of a contribution.
Expand All @@ -23,12 +23,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};
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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down
10 changes: 3 additions & 7 deletions chartlets.js/packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type MutableHostStore,
isHostStore,
isMutableHostStore,
} from "./options";
} from "./host";

const hostStore: HostStore = {
get: (name: string) => name,
Expand Down
60 changes: 60 additions & 0 deletions chartlets.js/packages/lib/src/types/state/host.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit f77b23f

Please sign in to comment.